okmodel 0.2.1 → 0.4.0

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 (148) hide show
  1. package/README.md +882 -48
  2. package/dist/contracts/catalog/document.d.ts +6 -2
  3. package/dist/contracts/catalog/enum.d.ts +31 -4
  4. package/dist/contracts/catalog/extension.d.ts +61 -0
  5. package/dist/contracts/catalog/privilege.d.ts +64 -0
  6. package/dist/contracts/catalog/routine.d.ts +56 -0
  7. package/dist/contracts/catalog/types.d.ts +164 -10
  8. package/dist/contracts/catalog/view.d.ts +73 -0
  9. package/dist/dialects/pg/column.d.ts +3 -1
  10. package/dist/dialects/pg/ddl.d.ts +65 -1
  11. package/dist/dialects/pg/enum.d.ts +10 -8
  12. package/dist/dialects/pg/ext/citext.d.ts +14 -0
  13. package/dist/dialects/pg/ext/citext.js +11 -0
  14. package/dist/dialects/pg/ext/index.d.ts +52 -0
  15. package/dist/dialects/pg/ext/index.js +6 -0
  16. package/dist/dialects/pg/ext/pg-trgm.d.ts +53 -0
  17. package/dist/dialects/pg/ext/pg-trgm.js +42 -0
  18. package/dist/dialects/pg/fn/index.d.ts +107 -0
  19. package/dist/dialects/pg/fn/index.js +175 -0
  20. package/dist/dialects/pg/fn/touch.d.ts +14 -0
  21. package/dist/dialects/pg/index.js +104 -19
  22. package/dist/dialects/pg/introspect.d.ts +10 -1
  23. package/dist/dialects/pg/operators.d.ts +8 -1
  24. package/dist/dialects/pg/role/check.d.ts +45 -0
  25. package/dist/dialects/pg/role/index.d.ts +50 -0
  26. package/dist/dialects/pg/role/sql.d.ts +51 -0
  27. package/dist/dialects/pg/table.d.ts +24 -1
  28. package/dist/dialects/pg/tenancy.d.ts +9 -0
  29. package/dist/dialects/pg/view/client.d.ts +30 -0
  30. package/dist/dialects/pg/view/depend.d.ts +35 -0
  31. package/dist/dialects/pg/view/index.d.ts +114 -0
  32. package/dist/dialects/pg/view/index.js +289 -0
  33. package/dist/dialects/pg/view/scratch.d.ts +22 -0
  34. package/dist/dialects/pg/where.d.ts +2 -2
  35. package/dist/runtime/catalog-cache.d.ts +17 -0
  36. package/dist/runtime/compat-words.d.ts +23 -0
  37. package/dist/runtime/drift-detail.d.ts +20 -4
  38. package/dist/runtime/drift.d.ts +4 -2
  39. package/dist/runtime/pg/bun.js +3 -3
  40. package/dist/runtime/pg/pg.js +3 -3
  41. package/dist/runtime/pg/pglite.js +3 -3
  42. package/dist/runtime/pg/postgresjs.js +3 -3
  43. package/dist/runtime/tenancy/index.js +6 -3
  44. package/dist/runtime/traits/index.d.ts +7 -2
  45. package/dist/runtime/traits/index.js +151 -13
  46. package/dist/runtime/types.d.ts +2 -0
  47. package/dist/runtime/validate/index.js +4 -4
  48. package/dist/shared/{xkz8g6by.js → 1g0drakv.js} +2 -2
  49. package/dist/shared/2321avmn.js +418 -0
  50. package/dist/shared/{7mht49yd.js → 3p4b1fg0.js} +57 -18
  51. package/dist/shared/41kv2xdm.js +318 -0
  52. package/dist/shared/{97g2jnrw.js → 4ef7mmqf.js} +3 -3
  53. package/dist/shared/{sacqha1y.js → 4m9fn52z.js} +2 -2
  54. package/dist/shared/5mtf6cj3.js +94 -0
  55. package/dist/shared/{1dp5knt7.js → 5vdm4sgp.js} +2 -2
  56. package/dist/shared/{a1gdp7j9.js → 6693j9rs.js} +1 -1
  57. package/dist/shared/{38crxbgn.js → 6p5k20sm.js} +3 -3
  58. package/dist/shared/7nk68fj8.js +751 -0
  59. package/dist/shared/84ptn35q.js +71 -0
  60. package/dist/shared/{d2whjz4k.js → 8jb4ymh2.js} +581 -28
  61. package/dist/shared/8ktwd3rh.js +97 -0
  62. package/dist/shared/94ez8kzh.js +19 -0
  63. package/dist/shared/{3a5ne750.js → 9m80mkac.js} +2 -2
  64. package/dist/shared/9x25fxhc.js +150 -0
  65. package/dist/shared/9yn6a2vd.js +11 -0
  66. package/dist/shared/ady1ms74.js +72 -0
  67. package/dist/shared/{4jzzghr6.js → awrsqrn7.js} +6 -6
  68. package/dist/shared/{4b9da9d7.js → axhk06q1.js} +19 -1
  69. package/dist/shared/{d04j53v8.js → c5g81ngb.js} +82 -14
  70. package/dist/shared/{n47rd821.js → c8aghh05.js} +1 -1
  71. package/dist/shared/{dd4f8mnx.js → c9z5g0fm.js} +4 -2
  72. package/dist/shared/{ps43134h.js → dyj86cmj.js} +4 -4
  73. package/dist/shared/{0qkxbm1f.js → fa9vt245.js} +1 -1
  74. package/dist/shared/{qa5dsc7z.js → fb00y8fp.js} +3 -3
  75. package/dist/shared/{qxa0yb9j.js → g7ez901n.js} +30 -1
  76. package/dist/shared/{py3nh7jy.js → gdh9mrtp.js} +2 -2
  77. package/dist/shared/{yw5c610b.js → gjthds9j.js} +2 -2
  78. package/dist/shared/hcmdtwgh.js +102 -0
  79. package/dist/shared/hqjsxnz7.js +13 -0
  80. package/dist/shared/htnaj77v.js +17 -0
  81. package/dist/shared/{v18jva5b.js → jgxzjzcz.js} +1 -1
  82. package/dist/shared/{x9v1qgeb.js → jsd07prz.js} +3 -3
  83. package/dist/shared/{g9f0zqce.js → kafaa5zv.js} +1 -1
  84. package/dist/shared/kj680nxv.js +19 -0
  85. package/dist/shared/{mxmcdsh5.js → kkzwp3d2.js} +9 -9
  86. package/dist/shared/{g1b5zzj9.js → n0dgs0pb.js} +1 -1
  87. package/dist/shared/n5y9zyj1.js +2431 -0
  88. package/dist/shared/{zt8y0wzb.js → n7mcc1gk.js} +1 -1
  89. package/dist/shared/{ge03bfca.js → nxb42q7h.js} +2 -2
  90. package/dist/shared/q6zsyh31.js +2192 -0
  91. package/dist/shared/{sfh4pk75.js → q795afwa.js} +26 -21
  92. package/dist/shared/{10tjenmd.js → qn0srwwq.js} +1 -1
  93. package/dist/shared/s4mb6a4b.js +545 -0
  94. package/dist/shared/{ytg3xf9p.js → sf8m6tfy.js} +3 -3
  95. package/dist/shared/{zd93g3rq.js → sr5ms7b3.js} +1 -1
  96. package/dist/shared/{xatfg0gn.js → tmbdywx0.js} +4 -19
  97. package/dist/shared/tny2t3sm.js +100 -0
  98. package/dist/shared/{e7a268fy.js → v7tpnhnz.js} +1 -1
  99. package/dist/shared/{ecdzej21.js → wqem4jed.js} +7 -7
  100. package/dist/shared/xghkt7a9.js +1144 -0
  101. package/dist/shared/y005b761.js +112 -0
  102. package/dist/shared/ycatkxmq.js +84 -0
  103. package/dist/shared/ygem90rn.js +900 -0
  104. package/dist/shared/{eaxwrgby.js → yjw02enp.js} +6 -3
  105. package/dist/shared/{mrq77sgw.js → zg24x0hm.js} +1 -1
  106. package/dist/shared/{cstgkzdx.js → zvbjc949.js} +1 -1
  107. package/dist/tooling/cli.js +66 -13
  108. package/dist/tooling/migrate/apply.d.ts +75 -5
  109. package/dist/tooling/migrate/backfill.d.ts +136 -0
  110. package/dist/tooling/migrate/check.d.ts +65 -0
  111. package/dist/tooling/migrate/classify.d.ts +108 -0
  112. package/dist/tooling/migrate/commands.d.ts +2 -1
  113. package/dist/tooling/migrate/config.d.ts +30 -0
  114. package/dist/tooling/migrate/doctor.d.ts +28 -0
  115. package/dist/tooling/migrate/drift.d.ts +35 -0
  116. package/dist/tooling/migrate/estimate.d.ts +50 -0
  117. package/dist/tooling/migrate/ext.d.ts +17 -0
  118. package/dist/tooling/migrate/extensions.d.ts +53 -0
  119. package/dist/tooling/migrate/files.d.ts +12 -0
  120. package/dist/tooling/migrate/index.d.ts +3 -2
  121. package/dist/tooling/migrate/index.js +67 -1
  122. package/dist/tooling/migrate/lint-rules.d.ts +51 -0
  123. package/dist/tooling/migrate/lint.d.ts +71 -0
  124. package/dist/tooling/migrate/managed.d.ts +17 -0
  125. package/dist/tooling/migrate/plan.d.ts +78 -10
  126. package/dist/tooling/migrate/project.d.ts +64 -5
  127. package/dist/tooling/migrate/provision.d.ts +18 -0
  128. package/dist/tooling/migrate/reference.d.ts +40 -0
  129. package/dist/tooling/migrate/runner.d.ts +108 -0
  130. package/dist/tooling/migrate/safe.d.ts +77 -0
  131. package/dist/tooling/migrate/seed.d.ts +20 -0
  132. package/dist/tooling/migrate/snapshot.d.ts +78 -0
  133. package/dist/tooling/migrate/status.d.ts +25 -6
  134. package/dist/tooling/migrate/type-name.d.ts +19 -0
  135. package/dist/tooling/testing/factories.d.ts +117 -0
  136. package/dist/tooling/testing/facts.d.ts +72 -0
  137. package/dist/tooling/testing/index.d.ts +89 -0
  138. package/dist/tooling/testing/index.js +8 -0
  139. package/dist/tooling/testing/isolation.d.ts +30 -0
  140. package/dist/tooling/testing/random.d.ts +40 -0
  141. package/dist/tooling/testing/record.d.ts +38 -0
  142. package/package.json +26 -2
  143. package/dist/shared/5z4q2zda.js +0 -2073
  144. package/dist/shared/eqmtn1bj.js +0 -399
  145. package/dist/shared/rtnaqre0.js +0 -37
  146. package/dist/shared/zwmqxb0g.js +0 -79
  147. package/dist/shared/{m9tv7xn7.js → radd3z4g.js} +3 -3
  148. /package/dist/shared/{9k6vetvf.js → rrstp3kb.js} +0 -0
@@ -1,2073 +0,0 @@
1
- import {
2
- creationOrder2,
3
- renameColumn2,
4
- renameTable2,
5
- serializeCatalog2,
6
- catalogHash2,
7
- parseCatalog2
8
- } from "./d2whjz4k.js";
9
- import {
10
- VALIDATION_NOT_IMPORTED2,
11
- VALIDATION_NOT_IMPORTED_FIX2
12
- } from "./n442h54c.js";
13
- import {
14
- contributedFields
15
- } from "./g1b5zzj9.js";
16
- import {
17
- assertValidation,
18
- writeWouldValidate
19
- } from "./3a5ne750.js";
20
- import {
21
- catalog,
22
- sameEnumLabels
23
- } from "./qxa0yb9j.js";
24
- import {
25
- snakeCase,
26
- emittedTypeName
27
- } from "./x9v1qgeb.js";
28
- import {
29
- ColumnBuilder2
30
- } from "./dd4f8mnx.js";
31
- import {
32
- quoteLiteral
33
- } from "./xatfg0gn.js";
34
- import {
35
- staticNamespace2,
36
- identityKey2,
37
- compareText
38
- } from "./t4tbsy9h.js";
39
- import {
40
- OkmError2
41
- } from "./52szzakm.js";
42
-
43
- // src/dialects/pg/ddl.ts
44
- function quoteIdent(name) {
45
- return `"${name.replaceAll('"', '""')}"`;
46
- }
47
- function dropObjectSql(object, schema) {
48
- if (object.owner === "ignored")
49
- return;
50
- switch (object.kind) {
51
- case "table":
52
- return `drop table ${qualify(schema, object.identity.name)}`;
53
- case "column":
54
- return `alter table ${qualify(schema, object.identity.parent.name)} drop column ${quoteIdent(object.identity.name)}`;
55
- case "index":
56
- return `drop index ${qualify(schema, object.identity.name)}`;
57
- case "constraint":
58
- return `alter table ${qualify(schema, object.identity.parent.name)} drop constraint ${quoteIdent(object.identity.name)}`;
59
- case "sequence":
60
- return `drop sequence ${qualify(schema, object.identity.name)}`;
61
- case "type":
62
- return `drop type ${qualify(schema, object.identity.name)}`;
63
- default:
64
- return;
65
- }
66
- }
67
- function createObjectSql(object, schema) {
68
- switch (object.kind) {
69
- case "column":
70
- return `alter table ${qualify(schema, object.identity.parent.name)} add column ${columnSql(object)}`;
71
- case "index":
72
- return createIndexSql(object, schema);
73
- case "constraint":
74
- return `alter table ${qualify(schema, object.identity.parent.name)} add constraint ${quoteIdent(object.identity.name)} ${constraintBody(object, schema)}`;
75
- case "sequence":
76
- return createSequenceSql(object, schema);
77
- case "type":
78
- return createEnumSql(object, schema);
79
- case "table":
80
- return;
81
- default:
82
- return;
83
- }
84
- }
85
- function alterColumnSql(before, after, schema) {
86
- const statements = [];
87
- const table = qualify(schema, after.identity.parent.name);
88
- const name = quoteIdent(after.identity.name);
89
- const typeChanged = before.definition.dataType !== after.definition.dataType;
90
- const collationChanged = before.definition.collation !== after.definition.collation;
91
- if (typeChanged || collationChanged) {
92
- const using = typeChanged ? ` using ${name}::${after.definition.dataType}` : "";
93
- statements.push(`alter table ${table} alter column ${name} set data type ${after.definition.dataType}${collateSql(after.definition.collation)}${using}`);
94
- }
95
- if (before.definition.nullable !== after.definition.nullable) {
96
- statements.push(`alter table ${table} alter column ${name} ${after.definition.nullable ? "drop not null" : "set not null"}`);
97
- }
98
- if (before.definition.defaultExpression !== after.definition.defaultExpression) {
99
- statements.push(after.definition.defaultExpression === undefined ? `alter table ${table} alter column ${name} drop default` : `alter table ${table} alter column ${name} set default ${after.definition.defaultExpression}`);
100
- }
101
- return statements;
102
- }
103
- function identitySequence(sequence, source) {
104
- const key = identityKey2(sequence.identity);
105
- return source.objects.some((object) => {
106
- if (object.kind !== "column" || object.definition.identity === undefined)
107
- return false;
108
- return object.dependencies.some((edge) => identityKey2(edge.target) === key);
109
- });
110
- }
111
- function createTableSql(tableObject, source, schema) {
112
- const parent = tableObject.identity.name;
113
- const columns = source.objects.filter((object) => object.kind === "column" && object.identity.parent.name === parent);
114
- const primary = source.objects.find((object) => object.kind === "constraint" && object.definition.constraintKind === "primaryKey" && object.identity.parent.name === parent);
115
- const lines = columns.map((column) => columnSql(column));
116
- if (primary !== undefined) {
117
- const cols = primary.definition.columns.map((name) => quoteIdent(name)).join(", ");
118
- lines.push(`primary key (${cols})`);
119
- }
120
- const partition = tableObject.definition.partition;
121
- const tail = partition === undefined ? "" : ` partition by ${partition.method} (${partition.columns.map((name) => quoteIdent(name)).join(", ")})`;
122
- return `create table ${qualify(schema, parent)} (
123
- ${lines.join(`,
124
- `)}
125
- )${tail}`;
126
- }
127
- function collateSql(collation) {
128
- if (collation === undefined)
129
- return "";
130
- return ` collate ${quoteIdent(collation)}`;
131
- }
132
- function createEnumSql(object, schema) {
133
- const labels = object.definition.labels.map((label) => quoteLiteral(label)).join(", ");
134
- return `create type ${qualify(schema, object.identity.name)} as enum (${labels})`;
135
- }
136
- function columnSql(column) {
137
- const definition = column.definition;
138
- let sql = `${quoteIdent(column.identity.name)} ${definition.dataType}${collateSql(definition.collation)}`;
139
- if (definition.identity !== undefined) {
140
- sql += definition.identity.always ? " generated always as identity" : " generated by default as identity";
141
- } else if (definition.generated !== undefined) {
142
- sql += ` generated always as (${definition.generated.expression}) stored`;
143
- }
144
- if (!definition.nullable)
145
- sql += " not null";
146
- if (definition.defaultExpression !== undefined)
147
- sql += ` default ${definition.defaultExpression}`;
148
- return sql;
149
- }
150
- function createIndexSql(object, schema) {
151
- const unique = object.definition.unique ? "unique " : "";
152
- const target = object.definition.expression !== undefined ? `(${object.definition.expression})` : object.definition.columns.map((name) => quoteIdent(name)).join(", ");
153
- const where = object.definition.predicate === undefined ? "" : ` where ${object.definition.predicate}`;
154
- return `create ${unique}index ${quoteIdent(object.identity.name)} on ${qualify(schema, object.identity.parent.name)} (${target})${where}`;
155
- }
156
- function createSequenceSql(object, schema) {
157
- const definition = object.definition;
158
- const cycle = definition.cycle ? " cycle" : "";
159
- return `create sequence ${qualify(schema, object.identity.name)} as ${definition.dataType} start ${definition.start} increment ${definition.increment}${cycle}`;
160
- }
161
- function constraintBody(object, schema) {
162
- const definition = object.definition;
163
- const columns = definition.columns.map((name) => quoteIdent(name)).join(", ");
164
- switch (definition.constraintKind) {
165
- case "primaryKey":
166
- return `primary key (${columns})`;
167
- case "unique": {
168
- const nulls = definition.nullsNotDistinct ? " nulls not distinct" : "";
169
- return `unique${nulls} (${columns})${deferClause(definition)}`;
170
- }
171
- case "check":
172
- return `check (${definition.expression ?? "true"})${deferClause(definition)}`;
173
- case "foreignKey": {
174
- const references = definition.references;
175
- const target = references === undefined ? "" : ` references ${qualify(schema, references.parent.name)} (${references.columns.map((name) => quoteIdent(name)).join(", ")})`;
176
- const onDelete = references?.onDelete === undefined ? "" : ` on delete ${references.onDelete}`;
177
- const onUpdate = references?.onUpdate === undefined ? "" : ` on update ${references.onUpdate}`;
178
- return `foreign key (${columns})${target}${onDelete}${onUpdate}${deferClause(definition)}`;
179
- }
180
- default:
181
- return definition.constraintKind;
182
- }
183
- }
184
- function deferClause(definition) {
185
- if (!definition.deferrable)
186
- return "";
187
- return ` deferrable initially ${definition.initially}`;
188
- }
189
- function qualify(schema, name) {
190
- return `${quoteIdent(schema)}.${quoteIdent(name)}`;
191
- }
192
-
193
- // src/tooling/migrate/values.ts
194
- function parseReplace(flag) {
195
- const eq = flag.indexOf("=");
196
- if (eq <= 0) {
197
- throw new OkmError2("OKM1541", `--replace ${flag} must be table.column.old=new.`, {
198
- fix: {
199
- summary: "Pass --replace <table>.<column>.<old>=<new>, or =null on a nullable column."
200
- }
201
- });
202
- }
203
- const left = flag.slice(0, eq);
204
- const raw = flag.slice(eq + 1);
205
- const first = left.indexOf(".");
206
- const second = first < 0 ? -1 : left.indexOf(".", first + 1);
207
- if (first <= 0 || second <= first + 1 || second === left.length - 1) {
208
- throw new OkmError2("OKM1541", `--replace ${flag} must be table.column.old=new.`, {
209
- fix: {
210
- summary: "Pass --replace <table>.<column>.<old>=<new>, or =null on a nullable column."
211
- }
212
- });
213
- }
214
- const table = left.slice(0, first);
215
- const column = left.slice(first + 1, second);
216
- const from = left.slice(second + 1);
217
- return { table, column, from, to: raw === "null" ? null : raw };
218
- }
219
- function assertNoChains(replacements) {
220
- const removed = new Set(replacements.map((item) => pair(item.table, item.column, item.from)));
221
- for (const item of replacements) {
222
- if (item.to !== null && removed.has(pair(item.table, item.column, item.to))) {
223
- const flag = `--replace ${item.table}.${item.column}.${item.from}=${item.to}`;
224
- throw new OkmError2("OKM1541", `${flag} chains through another removed value.`, {
225
- fix: { summary: "The replacement must be in the new list. No chains." }
226
- });
227
- }
228
- }
229
- }
230
- function pair(table, column, value) {
231
- return `${table}\x00${column}\x00${value}`;
232
- }
233
-
234
- // src/tooling/migrate/plan.ts
235
- var ACCESS = "ACCESS EXCLUSIVE";
236
- var SHARE = "SHARE";
237
- var SHARE_UPDATE = "SHARE UPDATE EXCLUSIVE";
238
- var ROW = "ROW EXCLUSIVE";
239
- function planMigration(request) {
240
- const schema = request.schema ?? "public";
241
- const renames = request.renames ?? [];
242
- const replacements = request.replacements ?? [];
243
- assertNoChains(replacements);
244
- const renamed = applyRenames(request.before, renames);
245
- const beforeList = index(renamed);
246
- const afterList = index(request.after);
247
- const beforeBy = new Map(beforeList.map((item) => [item.key, item.object]));
248
- const afterBy = new Map(afterList.map((item) => [item.key, item.object]));
249
- assertUnambiguous(beforeBy, afterBy);
250
- const dropKeys = new Set;
251
- const createKeys = new Set;
252
- const alters = [];
253
- for (const item of beforeList) {
254
- if (!afterBy.has(item.key))
255
- dropKeys.add(item.key);
256
- }
257
- for (const item of afterList) {
258
- if (!beforeBy.has(item.key))
259
- createKeys.add(item.key);
260
- }
261
- for (const item of afterList) {
262
- const previous = beforeBy.get(item.key);
263
- if (previous === undefined || sameDefinition(previous, item.object))
264
- continue;
265
- if (previous.kind === "type" && item.object.kind === "type")
266
- continue;
267
- if (previous.kind === "column" && item.object.kind === "column" && columnAlterable(previous, item.object)) {
268
- alters.push({ before: previous, after: item.object });
269
- continue;
270
- }
271
- dropKeys.add(item.key);
272
- createKeys.add(item.key);
273
- }
274
- const keptNames = pairSameShape(beforeBy, afterBy, dropKeys, createKeys);
275
- const picklists = picklistChanges(beforeBy, afterBy, request.after);
276
- const enums = enumEdits(beforeBy, afterBy, schema);
277
- requireReplacements(picklists, enums, replacements);
278
- recreateDependents(beforeList, afterList, alters, dropKeys, createKeys, picklists);
279
- omitOwnedSequences(createKeys, afterBy, request.after);
280
- omitOwnedSequences(dropKeys, beforeBy, renamed);
281
- const steps = [];
282
- steps.push(...renameSteps(request.before, renames, schema));
283
- steps.push(...keptNames.map((item) => renameShapeStep(item, schema)));
284
- steps.push(...enumAddSteps(enums));
285
- steps.push(...expandBackfills(picklists, enums, replacements, schema));
286
- const droppedTables = new Set([...dropKeys].map((key) => beforeBy.get(key)).filter((object) => object?.kind === "table").map((object) => object.identity.name));
287
- const deferred = new Set(picklists.map((change) => identityKey2(change.before.identity)));
288
- const typeDrops = [];
289
- for (const object of [...creationOrder2(renamed)].reverse()) {
290
- const key = identityKey2(object.identity);
291
- if (!dropKeys.has(key) || deferred.has(key) || covered(object, droppedTables))
292
- continue;
293
- if (object.kind === "type") {
294
- typeDrops.push(object);
295
- continue;
296
- }
297
- const sql = dropObjectSql(object, schema);
298
- if (sql === undefined)
299
- continue;
300
- steps.push(step(sql, "contract", "ddl", ACCESS));
301
- }
302
- for (const object of typeDrops) {
303
- const sql = dropObjectSql(object, schema);
304
- if (sql === undefined)
305
- continue;
306
- steps.push(step(sql, "contract", "ddl", ACCESS));
307
- }
308
- for (const change of alters) {
309
- for (const sql of alterColumnSql(change.before, change.after, schema)) {
310
- steps.push(step(sql, alterClass(sql), "ddl", ACCESS));
311
- }
312
- const identitySql = identityChangeSql(change.before, change.after, schema);
313
- if (identitySql !== undefined)
314
- steps.push(step(identitySql, "expand", "ddl", ACCESS));
315
- }
316
- const createdTables = new Set([...createKeys].map((key) => afterBy.get(key)).filter((object) => object?.kind === "table").map((object) => object.identity.name));
317
- const emitted = new Set;
318
- for (const object of creationOrder2(request.after)) {
319
- if (object.kind !== "type")
320
- continue;
321
- const key = identityKey2(object.identity);
322
- if (!createKeys.has(key) || emitted.has(key))
323
- continue;
324
- emitted.add(key);
325
- const sql = createObjectSql(object, schema);
326
- if (sql === undefined)
327
- continue;
328
- steps.push(step(sql, "expand", "ddl", ACCESS));
329
- }
330
- for (const object of creationOrder2(request.after)) {
331
- if (object.kind === "type")
332
- continue;
333
- const key = identityKey2(object.identity);
334
- if (deferred.has(key))
335
- continue;
336
- const parent = anchoredParent(object);
337
- const tableIsNew = parent !== undefined && createdTables.has(parent);
338
- if (tableIsNew && folded(object, request.after))
339
- continue;
340
- const wanted = createKeys.has(key) || tableIsNew && !folded(object, request.after);
341
- if (!wanted || emitted.has(key))
342
- continue;
343
- emitted.add(key);
344
- if (object.kind === "table") {
345
- steps.push(step(createTableSql(object, request.after, schema), "expand", "ddl", ACCESS));
346
- continue;
347
- }
348
- const sql = createObjectSql(object, schema);
349
- if (sql === undefined)
350
- continue;
351
- steps.push(step(sql, "expand", "ddl", object.kind === "index" ? SHARE : ACCESS));
352
- }
353
- steps.push(...contractSwaps(picklists, enums, replacements, schema));
354
- return {
355
- name: request.name ?? "migration",
356
- class: overall(steps),
357
- steps
358
- };
359
- }
360
- function formatPlan(plan) {
361
- const lines = [`-- class: ${plan.class}`, `-- name: ${plan.name}`, ""];
362
- if (plan.steps.length === 0) {
363
- lines.push("-- no steps");
364
- lines.push("");
365
- return lines.join(`
366
- `);
367
- }
368
- for (const item of plan.steps) {
369
- lines.push(`-- class: ${item.class}`);
370
- lines.push(`-- action: ${item.action}`);
371
- lines.push(`-- lock: ${item.lock}`);
372
- if (!item.transactional)
373
- lines.push("-- transactional: false");
374
- lines.push(`${item.sql};`);
375
- lines.push("");
376
- }
377
- return lines.join(`
378
- `);
379
- }
380
- function parsePlan(text) {
381
- const lines = text.split(`
382
- `);
383
- let name = "migration";
384
- let planClass = "expand";
385
- let index = 0;
386
- while (index < lines.length && lines[index] !== "") {
387
- const line = lines[index] ?? "";
388
- if (line.startsWith("-- name: "))
389
- name = line.slice("-- name: ".length);
390
- if (line.startsWith("-- class: "))
391
- planClass = readClass(line.slice("-- class: ".length));
392
- index += 1;
393
- }
394
- const steps = [];
395
- while (index < lines.length) {
396
- if ((lines[index] ?? "").trim() === "") {
397
- index += 1;
398
- continue;
399
- }
400
- if (lines[index] === "-- no steps")
401
- break;
402
- let stepClass = "expand";
403
- let action = "ddl";
404
- let lock = "";
405
- let transactional = true;
406
- const sql = [];
407
- while (index < lines.length && (lines[index] ?? "") !== "") {
408
- const line = lines[index] ?? "";
409
- index += 1;
410
- if (line.startsWith("-- class: "))
411
- stepClass = readClass(line.slice("-- class: ".length));
412
- else if (line.startsWith("-- action: "))
413
- action = line.endsWith("backfill") ? "backfill" : "ddl";
414
- else if (line.startsWith("-- lock: "))
415
- lock = line.slice("-- lock: ".length);
416
- else if (line === "-- transactional: false")
417
- transactional = false;
418
- else if (!line.startsWith("--"))
419
- sql.push(line);
420
- }
421
- const statement = sql.join(`
422
- `).replace(/;\s*$/, "");
423
- if (statement.length > 0) {
424
- steps.push({ sql: statement, class: stepClass, action, lock, transactional });
425
- }
426
- }
427
- return { name, class: steps.length === 0 ? planClass : overall(steps), steps };
428
- }
429
- function readClass(value) {
430
- if (value === "contract" || value === "unclassified")
431
- return value;
432
- return "expand";
433
- }
434
- function staleRenames(previous, renames) {
435
- if (previous === undefined)
436
- return [];
437
- const messages = [];
438
- for (const rename of renames) {
439
- if (rename.kind === "table") {
440
- if (!hasTable(previous, rename.from)) {
441
- messages.push(`Table renamedFrom ${rename.from} is not in the previous snapshot.`);
442
- }
443
- continue;
444
- }
445
- const tableRename = renames.find((item) => item.kind === "table" && item.to === rename.table);
446
- const previousTable = tableRename?.kind === "table" ? tableRename.from : rename.table;
447
- if (!hasColumn(previous, previousTable, rename.from)) {
448
- messages.push(`Column ${rename.table}.${rename.from} renamedFrom is not in the previous snapshot.`);
449
- }
450
- }
451
- return messages;
452
- }
453
- function applyRenames(source, renames) {
454
- let current = source;
455
- const namespace = staticNamespace2("public");
456
- for (const rename of renames) {
457
- if (rename.kind !== "table")
458
- continue;
459
- if (!hasTable(current, rename.from))
460
- continue;
461
- current = renameTable2(current, { namespace, from: rename.from, to: rename.to });
462
- }
463
- for (const rename of renames) {
464
- if (rename.kind !== "column")
465
- continue;
466
- const parent = { namespace, name: rename.table };
467
- if (!hasColumn(current, rename.table, rename.from))
468
- continue;
469
- current = renameColumn2(current, { parent, from: rename.from, to: rename.to });
470
- }
471
- return current;
472
- }
473
- function renameSteps(source, renames, schema) {
474
- const steps = [];
475
- const tables = new Map;
476
- for (const rename of renames) {
477
- if (rename.kind !== "table" || !hasTable(source, rename.from))
478
- continue;
479
- tables.set(rename.to, rename.from);
480
- steps.push(step(`alter table ${qualify2(schema, rename.from)} rename to ${quoteIdent(rename.to)}`, "contract", "ddl", ACCESS));
481
- }
482
- for (const rename of renames) {
483
- if (rename.kind !== "column")
484
- continue;
485
- const previousTable = tables.get(rename.table) ?? rename.table;
486
- if (!hasColumn(source, previousTable, rename.from))
487
- continue;
488
- steps.push(step(`alter table ${qualify2(schema, rename.table)} rename column ${quoteIdent(rename.from)} to ${quoteIdent(rename.to)}`, "contract", "ddl", ACCESS));
489
- }
490
- return steps;
491
- }
492
- function pairSameShape(beforeBy, afterBy, dropKeys, createKeys) {
493
- const dropped = [...dropKeys].map((key) => beforeBy.get(key)).filter((object) => object !== undefined);
494
- const created = [...createKeys].map((key) => afterBy.get(key)).filter((object) => object !== undefined);
495
- const pairs = [];
496
- const used = new Set;
497
- for (const left of dropped) {
498
- if (left.kind !== "index" && left.kind !== "constraint")
499
- continue;
500
- const leftKey = identityKey2(left.identity);
501
- const hits = created.filter((right) => {
502
- const key = identityKey2(right.identity);
503
- return key !== leftKey && !used.has(key) && sameShape(left, right);
504
- });
505
- const right = hits.length === 1 ? hits[0] : undefined;
506
- if (right === undefined)
507
- continue;
508
- const rivals = dropped.filter((item) => sameShape(item, right));
509
- if (rivals.length !== 1)
510
- continue;
511
- const rightKey = identityKey2(right.identity);
512
- used.add(rightKey);
513
- dropKeys.delete(identityKey2(left.identity));
514
- createKeys.delete(rightKey);
515
- if (left.identity.name !== right.identity.name)
516
- pairs.push({ from: left, to: right });
517
- }
518
- return pairs;
519
- }
520
- function sameShape(left, right) {
521
- if (left.kind !== right.kind)
522
- return false;
523
- if (left.kind !== "index" && left.kind !== "constraint" || right.kind !== "index" && right.kind !== "constraint") {
524
- return false;
525
- }
526
- if (left.identity.parent.name !== right.identity.parent.name)
527
- return false;
528
- return stable(withoutNameKey(left.definition)) === stable(withoutNameKey(right.definition));
529
- }
530
- function withoutNameKey(value) {
531
- const copy = { ...value };
532
- delete copy.nameKey;
533
- return copy;
534
- }
535
- function renameShapeStep(pair, schema) {
536
- if (pair.from.kind === "index" && pair.to.kind === "index") {
537
- return step(`alter index ${qualify2(schema, pair.from.identity.name)} rename to ${quoteIdent(pair.to.identity.name)}`, "contract", "ddl", ACCESS);
538
- }
539
- if (pair.from.kind !== "constraint" || pair.to.kind !== "constraint") {
540
- return step("select 1", "unclassified", "ddl", ACCESS);
541
- }
542
- return step(`alter table ${qualify2(schema, pair.from.identity.parent.name)} rename constraint ${quoteIdent(pair.from.identity.name)} to ${quoteIdent(pair.to.identity.name)}`, "contract", "ddl", ACCESS);
543
- }
544
- function assertUnambiguous(beforeBy, afterBy) {
545
- const dropped = new Map;
546
- const added = new Map;
547
- for (const [key, object] of beforeBy) {
548
- if (afterBy.has(key))
549
- continue;
550
- const group = groupOf(object);
551
- if (group === undefined)
552
- continue;
553
- const list = dropped.get(group) ?? [];
554
- list.push(object.identity.name);
555
- dropped.set(group, list);
556
- }
557
- for (const [key, object] of afterBy) {
558
- if (beforeBy.has(key))
559
- continue;
560
- const group = groupOf(object);
561
- if (group === undefined)
562
- continue;
563
- const list = added.get(group) ?? [];
564
- list.push(object.identity.name);
565
- added.set(group, list);
566
- }
567
- for (const [group, names] of added) {
568
- const gone = dropped.get(group);
569
- if (gone === undefined || gone.length === 0 || names.length === 0)
570
- continue;
571
- const from = gone[0] ?? "";
572
- const to = names[0] ?? "";
573
- const line = group.startsWith("table:") ? `Add renamedFrom: "${from}" on ${to}.` : `Add .renamedFrom("${from}") on ${to}.`;
574
- throw new OkmError2("OKM1530", `Drop ${from} and add ${to} could be a rename. ${line}`, {
575
- fix: { summary: line }
576
- });
577
- }
578
- }
579
- function groupOf(object) {
580
- if (object.kind === "table")
581
- return "table:public";
582
- if (object.kind === "column")
583
- return `column:${object.identity.parent.name}`;
584
- return;
585
- }
586
- function recreateDependents(beforeList, afterList, alters, dropKeys, createKeys, picklists) {
587
- const changed = new Set(alters.filter((item) => item.before.definition.dataType !== item.after.definition.dataType).map((item) => identityKey2(item.after.identity)));
588
- if (changed.size === 0)
589
- return;
590
- const deferred = new Set(picklists.map((change) => identityKey2(change.before.identity)));
591
- const afterBy = new Map(afterList.map((item) => [item.key, item.object]));
592
- for (const item of beforeList) {
593
- const key = identityKey2(item.object.identity);
594
- if (deferred.has(key) || !afterBy.has(key))
595
- continue;
596
- if (!dependsOnChanged(item.object, changed))
597
- continue;
598
- dropKeys.add(key);
599
- createKeys.add(key);
600
- }
601
- }
602
- function dependsOnChanged(object, columns) {
603
- if (object.kind !== "index" && object.kind !== "constraint")
604
- return false;
605
- return object.dependencies.some((edge) => columns.has(identityKey2(edge.target)));
606
- }
607
- function enumAddSteps(enums) {
608
- const steps = [];
609
- for (const change of enums) {
610
- if (change.kind !== "add")
611
- continue;
612
- for (const sql of change.statements) {
613
- steps.push(step(sql, "expand", "ddl", ACCESS, false));
614
- }
615
- }
616
- return steps;
617
- }
618
- function expandBackfills(picklists, enums, replacements, schema) {
619
- return dataSteps(picklists, enums, replacements, schema, "expand");
620
- }
621
- function contractSwaps(picklists, enums, replacements, schema) {
622
- const steps = [];
623
- steps.push(...dataSteps(picklists, enums, replacements, schema, "contract"));
624
- for (const change of picklists) {
625
- const table = qualify2(schema, change.after.identity.parent.name);
626
- const name = quoteIdent(change.after.identity.name);
627
- steps.push(step(`alter table ${table} drop constraint ${name}`, change.removed.length > 0 ? "contract" : "expand", "ddl", ACCESS));
628
- const expression = change.after.definition.expression ?? "true";
629
- steps.push(step(`alter table ${table} add constraint ${name} check (${expression}) not valid`, change.removed.length > 0 ? "contract" : "expand", "ddl", ACCESS));
630
- steps.push(step(`alter table ${table} validate constraint ${name}`, change.removed.length > 0 ? "contract" : "expand", "ddl", SHARE_UPDATE));
631
- }
632
- for (const change of enums) {
633
- if (change.kind !== "replace")
634
- continue;
635
- const type = qualify2(schema, change.name);
636
- const old = quoteIdent(`${change.name}_old`);
637
- const labels = change.labels.map((label) => sqlString(label)).join(", ");
638
- steps.push(step(`alter type ${type} rename to ${old}`, "contract", "ddl", ACCESS));
639
- steps.push(step(`create type ${type} as enum (${labels})`, "contract", "ddl", ACCESS));
640
- for (const column of change.columns) {
641
- const table = qualify2(schema, column.table);
642
- const name = quoteIdent(column.column);
643
- steps.push(step(`alter table ${table} alter column ${name} type ${type} using ${name}::text::${type}`, "contract", "ddl", ACCESS));
644
- }
645
- steps.push(step(`drop type ${qualify2(schema, `${change.name}_old`)}`, "contract", "ddl", ACCESS));
646
- }
647
- return steps;
648
- }
649
- function dataSteps(picklists, enums, replacements, schema, phase) {
650
- const steps = [];
651
- for (const change of picklists) {
652
- if (change.removed.length === 0)
653
- continue;
654
- const table = change.after.identity.parent.name;
655
- for (const value of change.removed) {
656
- const replacement = findReplacement(replacements, table, change.column, value);
657
- if (replacement === undefined)
658
- continue;
659
- steps.push(step(updateSql(schema, table, change.column, value, replacement.to), phase, "backfill", ROW));
660
- }
661
- }
662
- for (const change of enums) {
663
- if (change.kind !== "replace" || change.removed.length === 0)
664
- continue;
665
- for (const column of change.columns) {
666
- if (!column.existed)
667
- continue;
668
- for (const value of change.removed) {
669
- const replacement = findReplacement(replacements, column.table, column.column, value);
670
- if (replacement === undefined)
671
- continue;
672
- steps.push(step(updateSql(schema, column.table, column.column, value, replacement.to, true), phase, "backfill", ROW));
673
- }
674
- }
675
- }
676
- return steps;
677
- }
678
- function requireReplacements(picklists, enums, replacements) {
679
- const missing = [];
680
- for (const change of picklists) {
681
- for (const value of change.removed) {
682
- const table = change.after.identity.parent.name;
683
- const found = findReplacement(replacements, table, change.column, value);
684
- if (found === undefined) {
685
- missing.push(`--replace ${table}.${change.column}.${value}=<new>`);
686
- continue;
687
- }
688
- assertReplacement(found, change.after.definition.expression, change.nullable, parseValues(change.after.definition.expression));
689
- }
690
- }
691
- for (const change of enums) {
692
- if (change.kind !== "replace")
693
- continue;
694
- for (const column of change.columns) {
695
- if (!column.existed)
696
- continue;
697
- for (const value of change.removed) {
698
- const found = findReplacement(replacements, column.table, column.column, value);
699
- if (found === undefined) {
700
- missing.push(`--replace ${column.table}.${column.column}.${value}=<new>`);
701
- continue;
702
- }
703
- assertReplacement(found, undefined, column.nullable, change.labels);
704
- }
705
- }
706
- }
707
- if (missing.length > 0) {
708
- throw new OkmError2("OKM1541", `Removed values need a replacement: ${missing.join(", ")}.`, {
709
- fix: { summary: missing.join(" ") }
710
- });
711
- }
712
- }
713
- function assertReplacement(replacement, expression, nullable, allowed) {
714
- if (replacement.to === null) {
715
- if (!nullable) {
716
- throw new OkmError2("OKM1541", `--replace ${replacement.table}.${replacement.column}.${replacement.from}=null needs a nullable column.`, { fix: { summary: "Pass a value that is in the new list, or make the column nullable." } });
717
- }
718
- return;
719
- }
720
- const list = allowed ?? (expression === undefined ? [] : parseValues(expression) ?? []);
721
- if (!list.includes(replacement.to)) {
722
- const flag = `--replace ${replacement.table}.${replacement.column}.${replacement.from}=${replacement.to}`;
723
- throw new OkmError2("OKM1541", `${flag} is not in the new list.`, {
724
- fix: { summary: "The replacement must be in the new list. No chains." }
725
- });
726
- }
727
- }
728
- function parseValues(expression) {
729
- if (expression === undefined)
730
- return;
731
- return parseInList(expression)?.values;
732
- }
733
- function findReplacement(replacements, table, column, from) {
734
- return replacements.find((item) => item.table === table && item.column === column && item.from === from);
735
- }
736
- function picklistChanges(beforeBy, afterBy, after) {
737
- const changes = [];
738
- for (const [key, object] of beforeBy) {
739
- if (object.kind !== "constraint" || object.definition.constraintKind !== "check")
740
- continue;
741
- const next = afterBy.get(key);
742
- if (next === undefined || next.kind !== "constraint")
743
- continue;
744
- const left = parseInList(object.definition.expression ?? "");
745
- const right = parseInList(next.definition.expression ?? "");
746
- if (left === undefined || right === undefined || left.column !== right.column)
747
- continue;
748
- const removed = left.values.filter((value) => !right.values.includes(value));
749
- const added = right.values.filter((value) => !left.values.includes(value));
750
- if (removed.length === 0 && added.length === 0)
751
- continue;
752
- changes.push({
753
- before: object,
754
- after: next,
755
- column: right.column,
756
- removed,
757
- added,
758
- nullable: columnNullable(after, next.identity.parent.name, right.column)
759
- });
760
- }
761
- return changes;
762
- }
763
- function enumEdits(beforeBy, afterBy, schema) {
764
- const edits = [];
765
- for (const [key, next] of afterBy) {
766
- if (next.kind !== "type")
767
- continue;
768
- const previous = beforeBy.get(key);
769
- if (previous === undefined || previous.kind !== "type")
770
- continue;
771
- if (sameEnumLabels(previous.definition.labels, next.definition.labels))
772
- continue;
773
- const removed = previous.definition.labels.filter((label) => !next.definition.labels.includes(label));
774
- const columns = enumColumns(previous.identity.name, beforeBy, afterBy);
775
- if (removed.length === 0 && labelSubsequence(previous.definition.labels, next.definition.labels)) {
776
- edits.push({
777
- kind: "add",
778
- name: next.identity.name,
779
- statements: addValueStatements(schema, next.identity.name, previous.definition.labels, next.definition.labels)
780
- });
781
- continue;
782
- }
783
- edits.push({
784
- kind: "replace",
785
- name: next.identity.name,
786
- labels: next.definition.labels,
787
- removed,
788
- columns
789
- });
790
- }
791
- return edits;
792
- }
793
- function enumColumns(typeName, beforeBy, afterBy) {
794
- const columns = [];
795
- for (const object of afterBy.values()) {
796
- if (object.kind !== "column")
797
- continue;
798
- if (!usesType(object, typeName))
799
- continue;
800
- columns.push({
801
- table: object.identity.parent.name,
802
- column: object.identity.name,
803
- nullable: object.definition.nullable,
804
- existed: beforeBy.has(identityKey2(object.identity))
805
- });
806
- }
807
- columns.sort((left, right) => {
808
- const table = compareText(left.table, right.table);
809
- return table === 0 ? compareText(left.column, right.column) : table;
810
- });
811
- return columns;
812
- }
813
- function usesType(object, typeName) {
814
- return object.dependencies.some((edge) => edge.target.kind === "type" && edge.target.name === typeName);
815
- }
816
- function labelSubsequence(before, after) {
817
- let index = 0;
818
- for (const label of after) {
819
- if (before[index] === label)
820
- index += 1;
821
- }
822
- return index === before.length;
823
- }
824
- function addValueStatements(schema, name, before, after) {
825
- const present = new Set(before);
826
- const statements = [];
827
- const type = qualify2(schema, name);
828
- for (let index = 0;index < after.length; index += 1) {
829
- const label = after[index];
830
- if (label === undefined || present.has(label))
831
- continue;
832
- let beforeNeighbor;
833
- for (let cursor = index + 1;cursor < after.length; cursor += 1) {
834
- const next = after[cursor];
835
- if (next !== undefined && present.has(next)) {
836
- beforeNeighbor = next;
837
- break;
838
- }
839
- }
840
- let sql = `alter type ${type} add value ${sqlString(label)}`;
841
- if (beforeNeighbor !== undefined) {
842
- sql += ` before ${sqlString(beforeNeighbor)}`;
843
- } else {
844
- let afterNeighbor;
845
- for (let cursor = index - 1;cursor >= 0; cursor -= 1) {
846
- const previous = after[cursor];
847
- if (previous !== undefined && present.has(previous)) {
848
- afterNeighbor = previous;
849
- break;
850
- }
851
- }
852
- if (afterNeighbor !== undefined)
853
- sql += ` after ${sqlString(afterNeighbor)}`;
854
- }
855
- statements.push(sql);
856
- present.add(label);
857
- }
858
- return statements;
859
- }
860
- function parseInList(expression) {
861
- const match = /^\(\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.*)\)\s*\)$/i.exec(expression.trim());
862
- const column = match?.[1];
863
- const body = match?.[2];
864
- if (column === undefined || body === undefined)
865
- return;
866
- const values = [];
867
- let index = 0;
868
- while (index < body.length) {
869
- const char = body[index] ?? "";
870
- if (char === " " || char === ",") {
871
- index += 1;
872
- continue;
873
- }
874
- if (char !== "'")
875
- return;
876
- let end = index + 1;
877
- let value = "";
878
- while (end < body.length) {
879
- const next = body[end] ?? "";
880
- if (next === "'" && body[end + 1] === "'") {
881
- value += "'";
882
- end += 2;
883
- continue;
884
- }
885
- if (next === "'") {
886
- end += 1;
887
- break;
888
- }
889
- value += next;
890
- end += 1;
891
- }
892
- values.push(value);
893
- index = end;
894
- }
895
- return { column, values };
896
- }
897
- function columnNullable(source, table, column) {
898
- for (const object of source.objects) {
899
- if (object.kind === "column" && object.identity.parent.name === table && object.identity.name === column) {
900
- return object.definition.nullable;
901
- }
902
- }
903
- return false;
904
- }
905
- function columnAlterable(before, after) {
906
- return stable(before.definition.generated) === stable(after.definition.generated);
907
- }
908
- function identityChangeSql(before, after, schema) {
909
- if (stable(before.definition.identity) === stable(after.definition.identity))
910
- return;
911
- const table = qualify2(schema, after.identity.parent.name);
912
- const name = quoteIdent(after.identity.name);
913
- const next = after.definition.identity;
914
- const previous = before.definition.identity;
915
- if (next === undefined)
916
- return `alter table ${table} alter column ${name} drop identity`;
917
- if (previous === undefined) {
918
- const generated = next.always ? "always" : "by default";
919
- return `alter table ${table} alter column ${name} add generated ${generated} as identity`;
920
- }
921
- return `alter table ${table} alter column ${name} set generated ${next.always ? "always" : "by default"}`;
922
- }
923
- function omitOwnedSequences(keys, by, source) {
924
- for (const key of keys) {
925
- const object = by.get(key);
926
- if (object?.kind === "sequence" && folded(object, source))
927
- keys.delete(key);
928
- }
929
- }
930
- function sameDefinition(left, right) {
931
- return stable(left.definition) === stable(right.definition);
932
- }
933
- function covered(object, tables) {
934
- if (object.kind === "column" || object.kind === "index" || object.kind === "constraint") {
935
- return tables.has(object.identity.parent.name);
936
- }
937
- if (object.kind === "sequence") {
938
- return object.dependencies.some((edge) => edge.target.kind === "table" && tables.has(edge.target.name));
939
- }
940
- return false;
941
- }
942
- function folded(object, source) {
943
- if (object.kind === "column")
944
- return true;
945
- if (object.kind === "constraint" && object.definition.constraintKind === "primaryKey")
946
- return true;
947
- if (object.kind !== "sequence")
948
- return false;
949
- if (identitySequence(object, source))
950
- return true;
951
- const name = object.identity.name;
952
- return source.objects.some((column) => {
953
- if (column.kind !== "column" || column.definition.identity === undefined)
954
- return false;
955
- return name === `${column.identity.parent.name}_${column.identity.name}_seq`;
956
- });
957
- }
958
- function anchoredParent(object) {
959
- if (object.kind === "column" || object.kind === "index" || object.kind === "constraint") {
960
- return object.identity.parent.name;
961
- }
962
- return;
963
- }
964
- function alterClass(sql) {
965
- if (sql.includes("set data type") || sql.includes("set not null"))
966
- return "contract";
967
- return "expand";
968
- }
969
- function overall(steps) {
970
- if (steps.some((item) => item.class === "contract"))
971
- return "contract";
972
- if (steps.some((item) => item.class === "unclassified"))
973
- return "unclassified";
974
- return "expand";
975
- }
976
- function updateSql(schema, table, column, from, to, cast = false) {
977
- const value = to === null ? "null" : sqlString(to);
978
- const compare = cast ? `${quoteIdent(column)}::text` : quoteIdent(column);
979
- return `update ${qualify2(schema, table)} set ${quoteIdent(column)} = ${value} where ${compare} = ${sqlString(from)}`;
980
- }
981
- function hasTable(source, name) {
982
- return source.objects.some((object) => object.kind === "table" && object.identity.name === name);
983
- }
984
- function hasColumn(source, table, name) {
985
- return source.objects.some((object) => object.kind === "column" && object.identity.parent.name === table && object.identity.name === name);
986
- }
987
- function index(source) {
988
- return source.objects.map((object) => ({ key: identityKey2(object.identity), object }));
989
- }
990
- function step(sql, classification, action, lock, transactional = true) {
991
- return { sql, class: classification, action, lock, transactional };
992
- }
993
- function qualify2(schema, name) {
994
- return `${quoteIdent(schema)}.${quoteIdent(name)}`;
995
- }
996
- function sqlString(value) {
997
- return `'${value.replaceAll("'", "''")}'`;
998
- }
999
- function stable(value) {
1000
- return JSON.stringify(order(value));
1001
- }
1002
- function order(value) {
1003
- if (Array.isArray(value))
1004
- return value.map((item) => order(item));
1005
- if (value !== null && typeof value === "object") {
1006
- const record = value;
1007
- const next = {};
1008
- for (const key of Object.keys(record).sort())
1009
- next[key] = order(record[key]);
1010
- return next;
1011
- }
1012
- return value;
1013
- }
1014
-
1015
- // src/tooling/migrate/policy.ts
1016
- var READ = new Set([
1017
- "plan",
1018
- "status",
1019
- "check",
1020
- "drift",
1021
- "verify",
1022
- "pull",
1023
- "catalog-export",
1024
- "inspect"
1025
- ]);
1026
- var ALLOWED = new Set(["expand", "reference", "provision"]);
1027
- var FORWARD_ONLY = new Set(["drop", "rollback"]);
1028
- function assertTargetPolicy(target, operation, allowProtected = false) {
1029
- if (FORWARD_ONLY.has(operation)) {
1030
- throw new OkmError2("invalid", "There is no down migration.", {
1031
- fix: { summary: "Recovery is a new forward migration." }
1032
- });
1033
- }
1034
- if (!target.protected || allowProtected)
1035
- return;
1036
- if (READ.has(operation) || ALLOWED.has(operation))
1037
- return;
1038
- throw new OkmError2("OKM1850", `${operation} is blocked on protected target ${target.name}.`, {
1039
- kind: "forbidden",
1040
- fix: {
1041
- summary: "Run a read-only command or an expand migration, or pass --allow-protected for this invocation."
1042
- }
1043
- });
1044
- }
1045
- function assertTargetAlias(targets) {
1046
- const groups = new Map;
1047
- for (const target of targets) {
1048
- const key = `${target.host}
1049
- ${target.port}
1050
- ${target.database}`;
1051
- const group = groups.get(key);
1052
- if (group === undefined)
1053
- groups.set(key, [target]);
1054
- else
1055
- group.push(target);
1056
- }
1057
- for (const group of groups.values()) {
1058
- const protectedTarget = group.find((target) => target.protected);
1059
- const open = group.find((target) => !target.protected);
1060
- if (protectedTarget !== undefined && open !== undefined) {
1061
- throw new OkmError2("OKM1852", `${open.name} and ${protectedTarget.name} resolve to the same database and disagree about protection.`, {
1062
- fix: {
1063
- summary: "Give those targets the same protection flag. okm check and okm doctor both report this."
1064
- }
1065
- });
1066
- }
1067
- }
1068
- }
1069
- function assertDirectConnection(url, allowPooler) {
1070
- if (allowPooler)
1071
- return;
1072
- const endpoint = endpointOf(url);
1073
- const parsed = new URL(url);
1074
- const host = endpoint.host;
1075
- if (host.includes("pooler") || host.includes("pgbouncer") || host.includes("-pooler") || endpoint.port === "6543" || parsed.searchParams.get("pgbouncer") === "true") {
1076
- throw new OkmError2("invalid", `Refusing pooler host ${host}. Apply needs a direct connection.`, { fix: { summary: "Use a direct connection, or pass --allow-pooler." } });
1077
- }
1078
- }
1079
- function listTargets(config) {
1080
- if (config.database !== undefined && config.targets !== undefined) {
1081
- throw new OkmError2("invalid", "Set database or targets, not both.", {
1082
- fix: { summary: "database is the one target named default. Use targets for more than one." }
1083
- });
1084
- }
1085
- const entries = [];
1086
- if (config.database !== undefined)
1087
- entries.push(["default", config.database]);
1088
- if (config.targets !== undefined) {
1089
- for (const name of Object.keys(config.targets).sort()) {
1090
- const input = config.targets[name];
1091
- if (input !== undefined)
1092
- entries.push([name, input]);
1093
- }
1094
- }
1095
- return entries.map(([name, input]) => resolveTarget(name, input));
1096
- }
1097
- function selectTarget(config, name) {
1098
- const targets = listTargets(config);
1099
- if (targets.length === 0) {
1100
- throw new OkmError2("OKM1845", "No target is configured.", {
1101
- fix: { summary: "Set database or targets in okmodel.config.ts." }
1102
- });
1103
- }
1104
- if (name === undefined) {
1105
- if (targets.length === 1) {
1106
- const only = targets[0];
1107
- if (only !== undefined)
1108
- return only;
1109
- }
1110
- throw new OkmError2("OKM1853", "Several targets are configured and --target was not passed.", {
1111
- fix: { summary: "Pass --target <name>. The command does not guess." }
1112
- });
1113
- }
1114
- const found = targets.find((target) => target.name === name);
1115
- if (found === undefined) {
1116
- throw new OkmError2("OKM1845", `Target ${name} is not configured.`, {
1117
- fix: { summary: "Pass a name from targets, or database for the target named default." }
1118
- });
1119
- }
1120
- return found;
1121
- }
1122
- function resolveTarget(name, input) {
1123
- const url = typeof input === "string" ? input : input.url;
1124
- const protectedTarget = typeof input === "string" ? false : input.protected === true;
1125
- if (url === undefined || url.length === 0) {
1126
- throw new OkmError2("OKM1845", `Target ${name} has no url.`, {
1127
- fix: { summary: "Set the target url. A target is not inferred from the environment." }
1128
- });
1129
- }
1130
- const endpoint = endpointOf(url);
1131
- return {
1132
- name,
1133
- url,
1134
- protected: protectedTarget,
1135
- host: endpoint.host,
1136
- port: endpoint.port,
1137
- database: endpoint.database
1138
- };
1139
- }
1140
- function endpointOf(url) {
1141
- let parsed;
1142
- try {
1143
- parsed = new URL(url);
1144
- } catch {
1145
- throw new OkmError2("OKM1845", "The target URL is not a Postgres URL.", {
1146
- fix: { summary: "Pass a postgres:// URL for the target." }
1147
- });
1148
- }
1149
- const database = decodeURIComponent(parsed.pathname.replace(/^\//, ""));
1150
- return {
1151
- host: parsed.hostname.toLowerCase(),
1152
- port: parsed.port.length > 0 ? parsed.port : "5432",
1153
- database
1154
- };
1155
- }
1156
-
1157
- // src/tooling/migrate/project.ts
1158
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1159
- import { basename, join } from "node:path";
1160
- import { pathToFileURL } from "node:url";
1161
-
1162
- // src/tooling/errors/registry.ts
1163
- var ERROR_DOCS = [
1164
- {
1165
- code: "OKM1012",
1166
- title: "Trait field conflict",
1167
- summary: "A trait added a field that the table already declares.",
1168
- fix: "Rename the table field or drop the trait that introduces it. The error names both."
1169
- },
1170
- {
1171
- code: "OKM1020",
1172
- title: "Unknown table",
1173
- summary: "A table, dependency, or catalog object is missing or does not match its identity.",
1174
- fix: "Use a name that is in the schema. The error lists the accepted names and a nearest-name hint when one is close."
1175
- },
1176
- {
1177
- code: "OKM1021",
1178
- title: "Foreign key target",
1179
- summary: "A foreign key is missing its target table or column, or the target is ambiguous.",
1180
- fix: "Name the referenced table and columns. The error lists the columns that exist on the target."
1181
- },
1182
- {
1183
- code: "OKM1022",
1184
- title: "Foreign key type",
1185
- summary: "A foreign key column and the column it references have different types.",
1186
- fix: "Give both columns the same type. The error names the type that is accepted."
1187
- },
1188
- {
1189
- code: "OKM1023",
1190
- title: "Duplicate name",
1191
- summary: "Two catalog objects share an identity, or two tables emit the same type name.",
1192
- fix: "Rename one of them. Table names and emitted type names must be unique in the schema."
1193
- },
1194
- {
1195
- code: "OKM1024",
1196
- title: "Table file not in schema",
1197
- summary: "A table file is not passed to schema().",
1198
- fix: "Add the file's table to the schema() call that okm check reads."
1199
- },
1200
- {
1201
- code: "OKM1025",
1202
- title: "Multiple Register",
1203
- summary: "More than one Register augmentation is in the project.",
1204
- fix: 'Keep a single declare module "okmodel" that sets schema. Pass an explicit schema type everywhere else.'
1205
- },
1206
- {
1207
- code: "OKM1026",
1208
- title: "Dependency cycle",
1209
- summary: "The catalog dependency graph contains a cycle, so objects cannot be ordered.",
1210
- fix: "Break the cycle. The error is raised while ordering the catalog."
1211
- },
1212
- {
1213
- code: "OKM1027",
1214
- title: "Unreadable catalog",
1215
- summary: "The catalog document is malformed or uses a version this build cannot read.",
1216
- fix: "Regenerate the catalog with this version of okmodel. Do not hand-edit the document."
1217
- },
1218
- {
1219
- code: "OKM1030",
1220
- title: "Validation in two places",
1221
- summary: "A field is validated both on the column and somewhere else.",
1222
- fix: "Keep one validation for the field. The error names the field."
1223
- },
1224
- {
1225
- code: "OKM1040",
1226
- title: "Preset name collision",
1227
- summary: "A preset uses the name of a client method or a reserved name, or a table and its traits (or two traits) define the same preset.",
1228
- fix: "Rename the preset. The error names both sources when two define it. okm upgrade renames a preset when a later release claims its name."
1229
- },
1230
- {
1231
- code: "OKM1051",
1232
- title: "Archive foreign key",
1233
- summary: "A foreign key into a table-strategy archivable table uses CASCADE, SET NULL, or SET DEFAULT and is not listed in cascade.",
1234
- fix: "List the child table, the foreign key, and its action in cascade, or use RESTRICT or NO ACTION."
1235
- },
1236
- {
1237
- code: "OKM1052",
1238
- title: "Not archivable",
1239
- summary: "archive or restore was called on a table that is not archivable.",
1240
- fix: "Call it only on an archivable table. The methods are absent from the types of other tables."
1241
- },
1242
- {
1243
- code: "OKM1060",
1244
- title: "Invalid column",
1245
- summary: "A column definition has a bad length, precision, scale, array rank, interval qualifier, or an empty or repeated picklist or enum.",
1246
- fix: "Use a value the error lists as accepted."
1247
- },
1248
- {
1249
- code: "OKM1061",
1250
- title: "Reserved option",
1251
- summary: "The option exists in the types but this version does not implement it.",
1252
- fix: "Remove the option until the prompt or milestone named in the error adds it."
1253
- },
1254
- {
1255
- code: "OKM1101",
1256
- title: "Find without limit",
1257
- summary: "find was called without a limit.",
1258
- fix: "Pass limit, or call .all(reason) when the full set is intentional."
1259
- },
1260
- {
1261
- code: "OKM1102",
1262
- title: "Unfiltered write",
1263
- summary: "update or delete would run with an empty where.",
1264
- fix: "Pass a filter. undefined in a write filter does not mean every row."
1265
- },
1266
- {
1267
- code: "OKM1104",
1268
- title: "Upsert target",
1269
- summary: "onConflict names columns that are not a unique constraint.",
1270
- fix: "Set on to the columns of a unique constraint or the primary key."
1271
- },
1272
- {
1273
- code: "OKM1105",
1274
- title: "Include without limit",
1275
- summary: "A to-many include has no limit.",
1276
- fix: "Pass limit on the include, or call .all(reason)."
1277
- },
1278
- {
1279
- code: "OKM1110",
1280
- title: "Engine version",
1281
- summary: "The feature needs a newer engine than schema({ requires }) allows.",
1282
- fix: "Raise requires, or stop using the feature. The error names both versions."
1283
- },
1284
- {
1285
- code: "OKM1111",
1286
- title: "Missing capability",
1287
- summary: "The call needs a driver capability this driver does not have.",
1288
- fix: "Use a driver that declares the capability, or drop the call. The error names the flag."
1289
- },
1290
- {
1291
- code: "OKM1120",
1292
- title: "Unknown field",
1293
- summary: "A field name in where, select, orderBy, or include is not on the table.",
1294
- fix: "Use a field from the catalog. The error includes a nearest-name hint when one is close."
1295
- },
1296
- {
1297
- code: "OKM1121",
1298
- title: "Object as value",
1299
- summary: "A plain object was passed where a value is required. JSON cannot create an operator.",
1300
- fix: "Wrap the value with eq. eq is the equality form for json and jsonb."
1301
- },
1302
- {
1303
- code: "OKM1122",
1304
- title: "Bad identifier",
1305
- summary: "An identifier is empty, too long, contains NUL or another control character, or is an unquoted reserved word.",
1306
- fix: "Use a shorter name without control characters. Quote is not accepted here. The error names the rule that failed."
1307
- },
1308
- {
1309
- code: "OKM1123",
1310
- title: "Hidden field allowlist",
1311
- summary: "A hidden field was added to a filter or sort allowlist.",
1312
- fix: "Remove the hidden field from the allowlist. Hidden fields stay out of caller filters and sorts."
1313
- },
1314
- {
1315
- code: "OKM1124",
1316
- title: "Operator mismatch",
1317
- summary: "An operator does not apply to the column type.",
1318
- fix: "Use an operator the error lists for that column type. Text search needs a tsvector column."
1319
- },
1320
- {
1321
- code: "OKM1130",
1322
- title: "Cursor order",
1323
- summary: "A cursor was reused with a different orderBy than the one that created it, or is not a cursor page() returned.",
1324
- fix: "Request the next page with the same orderBy and the exact next value the previous page returned. The cursor encodes that order."
1325
- },
1326
- {
1327
- code: "OKM1190",
1328
- title: "Safety violation",
1329
- summary: "A query composition broke a core invariant. violations lists every broken rule in stable order.",
1330
- fix: "Change the contribution named on the first violation, or pass an explicit escape hatch (unscoped, all, trusted, allow) and record the reason."
1331
- },
1332
- {
1333
- code: "OKM1191",
1334
- title: "No snapshot plan",
1335
- summary: "A multi-statement read was requested inside READ COMMITTED and no single-statement plan exists.",
1336
- fix: "Run the read in REPEATABLE READ or SERIALIZABLE, or shape it so it is one statement."
1337
- },
1338
- {
1339
- code: "OKM1200",
1340
- title: "Validation failed",
1341
- summary: "A write or a standalone check failed a validation rule. issues lists each path and message key.",
1342
- fix: "Fix the fields named in issues. Each issue message is a key, not display text."
1343
- },
1344
- {
1345
- code: "OKM1201",
1346
- title: "Validation not imported",
1347
- summary: VALIDATION_NOT_IMPORTED2,
1348
- fix: VALIDATION_NOT_IMPORTED_FIX2
1349
- },
1350
- {
1351
- code: "OKM1210",
1352
- title: "Codec rejected a value",
1353
- summary: "A codec refused a value: not numeric text, not a valid temporal value, or the wrong shape.",
1354
- fix: "Pass a value the error lists as accepted. A missing Temporal global tells you to assign one to globalThis.Temporal."
1355
- },
1356
- {
1357
- code: "OKM1401",
1358
- title: "Outcome unknown",
1359
- summary: "A commit or batch was sent and its result never arrived, so the write may or may not have happened.",
1360
- fix: "Check whether the write landed, using an idempotency key, before retrying. This error is never retried automatically."
1361
- },
1362
- {
1363
- code: "OKM1510",
1364
- title: "Unsafe migration",
1365
- summary: "A migration is not expand-safe and has no reason.",
1366
- fix: "Split the change into expand and contract, or pass a reason that CI can record."
1367
- },
1368
- {
1369
- code: "OKM1520",
1370
- title: "Schema drift",
1371
- summary: "The database has drifted past what an expand migration can repair.",
1372
- fix: "Inspect the diff and repair the database, or generate a migration that matches the drift."
1373
- },
1374
- {
1375
- code: "OKM1521",
1376
- title: "Snapshot mismatch",
1377
- summary: "Provisioning from the snapshot does not match the replayed migration history.",
1378
- fix: "Regenerate the snapshot from the history. okm migrate check is the command that reports this."
1379
- },
1380
- {
1381
- code: "OKM1522",
1382
- title: "Apply lock held",
1383
- summary: "Another apply already holds the target's migration lock.",
1384
- fix: "Wait for that apply to finish. Do not start a second apply against the same target."
1385
- },
1386
- {
1387
- code: "OKM1530",
1388
- title: "Ambiguous rename",
1389
- summary: "A rename could match more than one object.",
1390
- fix: "Declare the rename explicitly so the plan has one pairing."
1391
- },
1392
- {
1393
- code: "OKM1541",
1394
- title: "Picklist removal",
1395
- summary: "A picklist or enum value was removed and the replacement was not given.",
1396
- fix: "Pass --replace <table>.<column>.<old>=<new>, or =null on a nullable column. The replacement must be in the new list."
1397
- },
1398
- {
1399
- code: "OKM1542",
1400
- title: "Data statement",
1401
- summary: "A migration contains a data statement outside backfill().",
1402
- fix: "Move the statement into backfill(). Data changes are batched and resumable there."
1403
- },
1404
- {
1405
- code: "OKM1601",
1406
- title: "Stale typed SQL",
1407
- summary: "A typed SQL signature no longer matches the database.",
1408
- fix: "Regenerate the typed SQL and commit the new signature."
1409
- },
1410
- {
1411
- code: "OKM1701",
1412
- title: "Tenant context missing",
1413
- summary: "A tenant table was used on a client that has no tenant context.",
1414
- fix: "Open the client with the tenant context before reading or writing that table."
1415
- },
1416
- {
1417
- code: "OKM1702",
1418
- title: "Unverifiable tenant SQL",
1419
- summary: "Raw SQL touches a tenant table and the tenant predicate cannot be proved.",
1420
- fix: "Use the query builder, or mark the fragment trusted with a reason if it is reviewed."
1421
- },
1422
- {
1423
- code: "OKM1704",
1424
- title: "Tenant key change",
1425
- summary: "An update tried to change the tenant key.",
1426
- fix: "Remove the tenant key from the update. Moving a row between tenants is not an update."
1427
- },
1428
- {
1429
- code: "OKM1705",
1430
- title: "Global references tenant",
1431
- summary: "A global table references a tenant table.",
1432
- fix: "Drop the reference, or make the referencing table a tenant table."
1433
- },
1434
- {
1435
- code: "OKM1706",
1436
- title: "Tenant index",
1437
- summary: "An index on a tenant table does not lead with the tenant key.",
1438
- fix: "Put the tenant key first in the index columns."
1439
- },
1440
- {
1441
- code: "OKM1707",
1442
- title: "RLS role",
1443
- summary: "connect() was given a role that owns the tables or is a superuser, under the rls strategy.",
1444
- fix: "Connect as a role that is neither the owner nor a superuser."
1445
- },
1446
- {
1447
- code: "OKM1801",
1448
- title: "Dialect mismatch",
1449
- summary: "The schema dialect and the driver dialect are not the same.",
1450
- fix: "Open the schema with the driver package that matches it."
1451
- },
1452
- {
1453
- code: "OKM1802",
1454
- title: "Server version",
1455
- summary: "The connected server does not satisfy schema({ requires }).",
1456
- fix: "Upgrade the server, or lower requires to a version the server meets."
1457
- },
1458
- {
1459
- code: "OKM1803",
1460
- title: "Postgres floor",
1461
- summary: "The connected server is older than PostgreSQL 15, and the schema does not set requires to that older major.",
1462
- fix: "Upgrade the server to PostgreSQL 15 or newer, or set schema({ requires }) to the older major on purpose."
1463
- },
1464
- {
1465
- code: "OKM1810",
1466
- title: "Extension not declared",
1467
- summary: "An extension builder was used but the extension is not declared on the schema.",
1468
- fix: "Declare the extension next to the schema that uses it."
1469
- },
1470
- {
1471
- code: "OKM1811",
1472
- title: "Extension unavailable",
1473
- summary: "A declared extension is not installed on the server.",
1474
- fix: "Install the extension, or remove the declaration. okm doctor reports this against the connected server."
1475
- },
1476
- {
1477
- code: "OKM1812",
1478
- title: "Feature version",
1479
- summary: "A feature needs a newer Postgres or extension version than the one declared or connected.",
1480
- fix: 'Raise the declared version, or avoid the feature. uuidv7() below 18 can use t.id({ default: "uuidv4" }), defaults.id set to "uuidv4", or a client generator. Core features are never gated on an extension.'
1481
- },
1482
- {
1483
- code: "OKM1813",
1484
- title: "Extension defined twice",
1485
- summary: "The same extension definition was registered twice.",
1486
- fix: "Keep one definition. A second copy fails at build."
1487
- },
1488
- {
1489
- code: "OKM1814",
1490
- title: "Extension change refused",
1491
- summary: "The plan would downgrade an extension, move a non-relocatable one, or drop one that still has dependents.",
1492
- fix: "Drop or move the dependents first. Postgres cannot lower an extension version."
1493
- },
1494
- {
1495
- code: "OKM1820",
1496
- title: "View tenancy",
1497
- summary: "A view over tenant tables does not expose the tenant key and is not marked global.",
1498
- fix: "Expose the tenant key so the predicate can be applied, or declare global(reason)."
1499
- },
1500
- {
1501
- code: "OKM1821",
1502
- title: "Replace needs recreate",
1503
- summary: "An incompatible replace would have to recreate dependents. CASCADE is never used.",
1504
- fix: "Review the dependents the plan lists and recreate them explicitly."
1505
- },
1506
- {
1507
- code: "OKM1822",
1508
- title: "Concurrent refresh",
1509
- summary: "REFRESH CONCURRENTLY was declared without a unique index on the materialized view.",
1510
- fix: "Add a unique index, or refresh without CONCURRENTLY."
1511
- },
1512
- {
1513
- code: "OKM1823",
1514
- title: "Security definer path",
1515
- summary: "A SECURITY DEFINER function has no search_path.",
1516
- fix: "Set search_path on the function to the schemas it is allowed to see."
1517
- },
1518
- {
1519
- code: "OKM1824",
1520
- title: "Function dependencies",
1521
- summary: "A plpgsql function has no dependsOn. Postgres does not record dependencies inside the body.",
1522
- fix: "List dependsOn. LANGUAGE sql with BEGIN ATOMIC is inferred and does not need the list."
1523
- },
1524
- {
1525
- code: "OKM1825",
1526
- title: "Missing privilege",
1527
- summary: "The application role lacks a privilege on a managed object.",
1528
- fix: "Grant the privilege, or change the role. okm doctor reports the object and the privilege."
1529
- },
1530
- {
1531
- code: "OKM1830",
1532
- title: "Lock outside transaction",
1533
- summary: "find was called with a row lock outside tx().",
1534
- fix: "Take the lock inside tx(). Advisory locks are transaction-scoped too."
1535
- },
1536
- {
1537
- code: "OKM1840",
1538
- title: "Replica not allowed",
1539
- summary: ".replica() was used on an operation that requires the primary, or inside tx().",
1540
- fix: "Run the operation on the primary. Writes, batch, locks, and tx() never go to a replica."
1541
- },
1542
- {
1543
- code: "OKM1841",
1544
- title: "External object differs",
1545
- summary: "An external object does not match the owner's exported catalog.",
1546
- fix: "Update the dependency's export, or stop treating the object as external."
1547
- },
1548
- {
1549
- code: "OKM1842",
1550
- title: "Cross-database foreign key",
1551
- summary: "A foreign key points at a table in another database.",
1552
- fix: "Keep the foreign key inside one database. Cross-catalog keys are allowed only there."
1553
- },
1554
- {
1555
- code: "OKM1843",
1556
- title: "No eligible replica",
1557
- summary: "A replica was required and none is eligible: unhealthy, behind the session, or none configured.",
1558
- fix: "Configure a replica or drop .replica(). This call does not read the primary."
1559
- },
1560
- {
1561
- code: "OKM1844",
1562
- title: "Fallback refused",
1563
- summary: "An automatic read found no eligible replica and fallback is error.",
1564
- fix: "Restore a replica, or set fallback to primary if the primary should absorb the read."
1565
- },
1566
- {
1567
- code: "OKM1845",
1568
- title: "Target unresolved",
1569
- summary: "The target could not be resolved. The tenant is unknown or the resolver failed.",
1570
- fix: "Check the tenant id and the resolver. Connection details are not guessed."
1571
- },
1572
- {
1573
- code: "OKM1846",
1574
- title: "Acquire timeout",
1575
- summary: "The pool did not hand out a connection within timeouts.acquire.",
1576
- fix: "Raise timeouts.acquire, or stop holding connections. The wait does not spill into another pool."
1577
- },
1578
- {
1579
- code: "OKM1850",
1580
- title: "Protected target",
1581
- summary: "The operation is blocked on a protected target and --allow-protected was not passed.",
1582
- fix: "Run a read-only command or an expand migration, or pass --allow-protected for this invocation."
1583
- },
1584
- {
1585
- code: "OKM1851",
1586
- title: "Target not empty",
1587
- summary: "Provisioning was asked to fill a target that already has objects.",
1588
- fix: "Provision an empty schema or an empty database. A non-empty target is refused."
1589
- },
1590
- {
1591
- code: "OKM1852",
1592
- title: "Protection mismatch",
1593
- summary: "Targets that resolve to the same database disagree about protection, or tenants of one schema-per-tenant database do.",
1594
- fix: "Give those targets the same protection flag. okm check and okm doctor both report this."
1595
- },
1596
- {
1597
- code: "OKM1853",
1598
- title: "Target not named",
1599
- summary: "Several targets are configured and the command was not given --target.",
1600
- fix: "Pass --target <name>. The command does not guess."
1601
- }
1602
- ];
1603
- var byCode;
1604
- function errorDoc(code) {
1605
- const index = byCode ?? indexDocs();
1606
- return index.get(code);
1607
- }
1608
- function indexDocs() {
1609
- const index = new Map;
1610
- for (const doc of ERROR_DOCS)
1611
- index.set(doc.code, doc);
1612
- byCode = index;
1613
- return index;
1614
- }
1615
-
1616
- // src/dialects/pg/declarations.ts
1617
- function schemaDeclarations(source) {
1618
- const renames = [];
1619
- for (const item of source.tables) {
1620
- const tableName = sqlTableName(item, source.casing);
1621
- const renamed = tableRenamedFrom(item);
1622
- if (renamed !== undefined) {
1623
- renames.push({ kind: "table", from: renamed, to: tableName });
1624
- }
1625
- for (const [field, value] of Object.entries(item.columns)) {
1626
- if (!(value instanceof ColumnBuilder2))
1627
- continue;
1628
- const state = value.state;
1629
- const columnName = state.sqlName ?? sqlIdentifier(field, source.casing);
1630
- if (state.renamedFrom !== undefined) {
1631
- renames.push({
1632
- kind: "column",
1633
- table: tableName,
1634
- from: state.renamedFrom,
1635
- to: columnName
1636
- });
1637
- }
1638
- }
1639
- }
1640
- return { renames };
1641
- }
1642
- function sqlTableName(item, casing) {
1643
- const options = item.options;
1644
- return options?.sqlName ?? sqlIdentifier(item.name, casing);
1645
- }
1646
- function tableRenamedFrom(item) {
1647
- const options = item.options;
1648
- return options?.renamedFrom;
1649
- }
1650
- function sqlIdentifier(name, casing) {
1651
- return casing === "snake" ? snakeCase(name) : name;
1652
- }
1653
-
1654
- // src/dialects/pg/emit.ts
1655
- function emitRowTypes(source) {
1656
- const lines = [
1657
- "/**",
1658
- " * Row types emitted from the schema.",
1659
- ` * Mode: ${source.types}.`,
1660
- " */",
1661
- ""
1662
- ];
1663
- const rows = [];
1664
- const inserts = [];
1665
- const updates = [];
1666
- for (const item of source.tables) {
1667
- const name = emittedTypeName(item.name);
1668
- const key = propertyName(item.name);
1669
- rows.push(` readonly ${key}: ${name};`);
1670
- inserts.push(` readonly ${key}: ${name}Insert;`);
1671
- updates.push(` readonly ${key}: ${name}Update;`);
1672
- lines.push(`export interface ${name} {`);
1673
- lines.push(fields(item, "row", source));
1674
- lines.push("}", "");
1675
- lines.push(`export interface ${name}Insert {`);
1676
- lines.push(fields(item, "insert", source));
1677
- lines.push("}", "");
1678
- lines.push(`export interface ${name}Update {`);
1679
- lines.push(fields(item, "update", source));
1680
- lines.push("}", "");
1681
- }
1682
- lines.push("export interface Rows {");
1683
- lines.push(rows.join(`
1684
- `));
1685
- lines.push("}", "");
1686
- lines.push("export interface Inserts {");
1687
- lines.push(inserts.join(`
1688
- `));
1689
- lines.push("}", "");
1690
- lines.push("export interface Updates {");
1691
- lines.push(updates.join(`
1692
- `));
1693
- lines.push("}");
1694
- return `${lines.join(`
1695
- `)}
1696
- `;
1697
- }
1698
- function propertyName(name) {
1699
- return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : JSON.stringify(name);
1700
- }
1701
- function fields(item, kind, source) {
1702
- const printed = [];
1703
- const options = item.options;
1704
- const entries = Object.entries(item.columns);
1705
- for (const pair of contributedFields(source.traits, options)) {
1706
- if (!Object.hasOwn(item.columns, pair[0]))
1707
- entries.push([pair[0], pair[1]]);
1708
- }
1709
- if (source.tenancy !== undefined && options?.tenancy?.kind !== "global" && !Object.hasOwn(item.columns, source.tenancy.key)) {
1710
- entries.push([source.tenancy.key, source.tenancy.column()]);
1711
- }
1712
- for (const [field, builder] of entries) {
1713
- if (!(builder instanceof ColumnBuilder2)) {
1714
- continue;
1715
- }
1716
- const state = builder.state;
1717
- if (kind === "row") {
1718
- if (!state.hidden) {
1719
- printed.push(` readonly ${field}: ${valueType(state, "required")};`);
1720
- }
1721
- continue;
1722
- }
1723
- const written = kind === "update" ? updateMode(state) : insertMode(state);
1724
- if (written === "omit") {
1725
- continue;
1726
- }
1727
- const mode = kind === "update" ? "optional" : written;
1728
- printed.push(` readonly ${field}: ${valueType(state, mode)};`);
1729
- }
1730
- return printed.join(`
1731
- `);
1732
- }
1733
- function insertMode(state) {
1734
- if (state.omitWrite || state.generated !== undefined || state.guarded) {
1735
- return "omit";
1736
- }
1737
- if (state.hasDefault || state.nullable) {
1738
- return "optional";
1739
- }
1740
- return "required";
1741
- }
1742
- function updateMode(state) {
1743
- if (state.omitUpdate || insertMode(state) === "omit")
1744
- return "omit";
1745
- return "optional";
1746
- }
1747
- function valueType(state, mode) {
1748
- let label = scalarLabel(state);
1749
- if (state.dims > 0) {
1750
- for (let rank = 0;rank < state.dims; rank += 1) {
1751
- label = `readonly (${label})[]`;
1752
- }
1753
- }
1754
- if (state.nullable) {
1755
- label += " | null";
1756
- }
1757
- if (mode === "optional") {
1758
- label += " | undefined";
1759
- }
1760
- return label;
1761
- }
1762
- function scalarLabel(state) {
1763
- if (state.picklist !== undefined) {
1764
- return state.picklist.values.map((value) => JSON.stringify(value)).join(" | ");
1765
- }
1766
- if (state.typeLabel !== undefined) {
1767
- return state.typeLabel;
1768
- }
1769
- const base = baseName(state.baseType);
1770
- switch (base) {
1771
- case "smallint":
1772
- case "integer":
1773
- case "real":
1774
- case "double precision":
1775
- return "number";
1776
- case "bigint":
1777
- case "numeric":
1778
- case "text":
1779
- case "varchar":
1780
- case "char":
1781
- case "citext":
1782
- case "uuid":
1783
- case "inet":
1784
- case "cidr":
1785
- case "macaddr":
1786
- case "macaddr8":
1787
- case "tsvector":
1788
- case "ltree":
1789
- return "string";
1790
- case "boolean":
1791
- return "boolean";
1792
- case "bytea":
1793
- return "Uint8Array";
1794
- case "json":
1795
- case "jsonb":
1796
- return "unknown";
1797
- case "timestamptz":
1798
- return "Temporal.Instant";
1799
- case "timestamp":
1800
- return "Temporal.PlainDateTime";
1801
- case "date":
1802
- return "Temporal.PlainDate";
1803
- case "time":
1804
- return "Temporal.PlainTime";
1805
- case "timetz":
1806
- return "{ readonly time: Temporal.PlainTime; readonly offset: string }";
1807
- case "interval":
1808
- return "Temporal.Duration";
1809
- case "tstzrange":
1810
- return rangeLabel("Temporal.Instant");
1811
- case "daterange":
1812
- return rangeLabel("Temporal.PlainDate");
1813
- case "int4range":
1814
- return rangeLabel("number");
1815
- case "int8range":
1816
- case "numrange":
1817
- return rangeLabel("string");
1818
- case "point":
1819
- return "{ readonly x: number; readonly y: number }";
1820
- case "line":
1821
- return "{ readonly a: number; readonly b: number; readonly c: number }";
1822
- default:
1823
- return "unknown";
1824
- }
1825
- }
1826
- function rangeLabel(bound) {
1827
- return `{ readonly empty: true } | { readonly empty: false; readonly lower: ${bound} | null; readonly upper: ${bound} | null; readonly lowerInclusive: boolean; readonly upperInclusive: boolean }`;
1828
- }
1829
- function baseName(dataType) {
1830
- if (dataType.startsWith("double precision")) {
1831
- return "double precision";
1832
- }
1833
- const space = dataType.indexOf(" ");
1834
- const head = space === -1 ? dataType : dataType.slice(0, space);
1835
- const paren = head.indexOf("(");
1836
- return paren === -1 ? head : head.slice(0, paren);
1837
- }
1838
-
1839
- // src/tooling/migrate/project.ts
1840
- async function buildProject(cwd) {
1841
- const config = await loadConfig(cwd);
1842
- const built = await loadSchema(cwd, config.schema);
1843
- return writeArtifact(cwd, config, built);
1844
- }
1845
- async function generateProject(cwd, name, flags) {
1846
- const prepared = await prepare(cwd, name, flags);
1847
- writeArtifact(cwd, prepared.config, prepared.built);
1848
- if (prepared.plan.steps.length === 0)
1849
- return;
1850
- const directory = join(cwd, prepared.config.migrations ?? "migrations");
1851
- mkdirSync(directory, { recursive: true });
1852
- const base = `${nextNumber(directory)}_${safeName(name)}`;
1853
- const sqlPath = join(directory, `${base}.sql`);
1854
- writeFileSync(sqlPath, formatPlan(prepared.plan));
1855
- writeFileSync(join(directory, `${base}.catalog.json`), serializeCatalog2(prepared.built.catalog));
1856
- return sqlPath;
1857
- }
1858
- async function planProject(cwd, name, flags) {
1859
- const prepared = await prepare(cwd, name, flags);
1860
- return prepared.plan;
1861
- }
1862
- async function checkProject(cwd) {
1863
- const opened = await openProject(cwd);
1864
- assertValidation(opened.built);
1865
- if (schemaRequestsValidation(opened.built) && !projectImportsValidate(cwd)) {
1866
- const doc = errorDoc("OKM1201");
1867
- if (doc === undefined)
1868
- throw new Error("OKM1201 missing from the error registry.");
1869
- throw new OkmError2(doc.code, doc.summary, { fix: { summary: doc.fix } });
1870
- }
1871
- assertTargetAlias(listTargets(opened.config));
1872
- if (opened.config.tables !== undefined) {
1873
- const directory = join(cwd, opened.config.tables);
1874
- const files = existsSync(directory) ? readdirSync(directory) : [];
1875
- const source = readFileSync(join(cwd, opened.config.schema), "utf8");
1876
- const missing = unlistedTableFiles(source, files);
1877
- if (missing.length > 0) {
1878
- throw new OkmError2("OKM1024", `Table file not in schema: ${missing.join(", ")}.`, {
1879
- fix: { summary: "Add the file's table to the schema() call that okm check reads." }
1880
- });
1881
- }
1882
- }
1883
- }
1884
- function unlistedTableFiles(schemaSource, fileNames) {
1885
- const missing = [];
1886
- for (const file of fileNames) {
1887
- if (!file.endsWith(".ts"))
1888
- continue;
1889
- const stem = basename(file, ".ts");
1890
- if (stem.length === 0 || stem.startsWith("."))
1891
- continue;
1892
- if (!schemaSource.includes(stem))
1893
- missing.push(file);
1894
- }
1895
- return missing;
1896
- }
1897
- async function projectHead(cwd) {
1898
- const opened = await openProject(cwd);
1899
- return { previous: opened.previous, catalog: opened.built.catalog, renames: opened.renames };
1900
- }
1901
- async function prepare(cwd, name, flags) {
1902
- const opened = await openProject(cwd);
1903
- const plan = planMigration({
1904
- before: opened.previous,
1905
- after: opened.built.catalog,
1906
- renames: opened.renames,
1907
- replacements: flags.map((flag) => parseReplace(flag)),
1908
- name
1909
- });
1910
- return { config: opened.config, built: opened.built, plan };
1911
- }
1912
- async function openProject(cwd) {
1913
- const config = await loadConfig(cwd);
1914
- const built = await loadSchema(cwd, config.schema);
1915
- const declarations = schemaDeclarations(built);
1916
- const previous = readPrevious(join(cwd, config.migrations ?? "migrations"));
1917
- const stale = staleRenames(previous?.catalog, declarations.renames);
1918
- if (stale.length > 0) {
1919
- throw new OkmError2("OKM1020", stale.join(" "), {
1920
- fix: { summary: "Remove renamedFrom once the migration that used it has been applied." }
1921
- });
1922
- }
1923
- return {
1924
- config,
1925
- built,
1926
- previous: previous?.catalog ?? catalog([]),
1927
- renames: declarations.renames
1928
- };
1929
- }
1930
- function writeArtifact(cwd, config, built) {
1931
- const directory = join(cwd, config.out ?? ".okm");
1932
- mkdirSync(directory, { recursive: true });
1933
- const text = serializeCatalog2(built.catalog);
1934
- writeFileSync(join(directory, "catalog.json"), text);
1935
- writeFileSync(join(directory, "catalog.hash"), `${catalogHash2(built.catalog)}
1936
- `);
1937
- writeFileSync(join(directory, "types.d.ts"), `${emitRowTypes(built)}${referenceAugmentation(built)}`);
1938
- return directory;
1939
- }
1940
- async function loadConfig(cwd) {
1941
- const path = join(cwd, "okmodel.config.ts");
1942
- const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
1943
- const record = isRecord(imported) ? imported : {};
1944
- const config = record.default ?? record.config;
1945
- if (!isConfig(config)) {
1946
- throw new OkmError2("invalid", "okmodel.config.ts must default-export defineConfig(...).");
1947
- }
1948
- return config;
1949
- }
1950
- async function loadSchema(cwd, spec) {
1951
- const path = join(cwd, spec);
1952
- const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
1953
- const record = isRecord(imported) ? imported : {};
1954
- for (const value of Object.values(record)) {
1955
- if (!isBuilt(value))
1956
- continue;
1957
- value.catalog;
1958
- return value;
1959
- }
1960
- throw new OkmError2("invalid", `${spec} must export a schema().`);
1961
- }
1962
- function readPrevious(directory) {
1963
- if (!existsSync(directory))
1964
- return;
1965
- const catalogs = readdirSync(directory).filter((file) => file.endsWith(".catalog.json")).sort();
1966
- const latest = catalogs.at(-1);
1967
- if (latest === undefined)
1968
- return;
1969
- const text = readFileSync(join(directory, latest), "utf8");
1970
- return { catalog: parseCatalog2(text) };
1971
- }
1972
- function referenceAugmentation(source) {
1973
- const names = source.tables.map((item) => JSON.stringify(item.name));
1974
- const union = names.length === 0 ? "never" : names.join(" | ");
1975
- return [
1976
- 'import type { ColumnFlags, ReferenceOptions } from "okmodel/pg";',
1977
- "",
1978
- 'declare module "okmodel/pg" {',
1979
- " interface ColumnBuilder<TValue, TFlags extends ColumnFlags> {",
1980
- " references(",
1981
- ` table: ${union},`,
1982
- " options?: ReferenceOptions,",
1983
- " ): ColumnBuilder<TValue, TFlags>;",
1984
- " }",
1985
- "}",
1986
- "",
1987
- `export type TableName = ${union};`,
1988
- ""
1989
- ].join(`
1990
- `);
1991
- }
1992
- function nextNumber(directory) {
1993
- let max = 0;
1994
- if (existsSync(directory)) {
1995
- for (const file of readdirSync(directory)) {
1996
- const match = /^(\d+)_/.exec(file);
1997
- const value = match?.[1];
1998
- if (value !== undefined)
1999
- max = Math.max(max, Number(value));
2000
- }
2001
- }
2002
- return String(max + 1).padStart(4, "0");
2003
- }
2004
- function safeName(name) {
2005
- if (!/^[A-Za-z0-9_-]+$/.test(name)) {
2006
- throw new OkmError2("invalid", `Migration name ${name} must be letters, digits, _ or -.`);
2007
- }
2008
- return name;
2009
- }
2010
- function isBuilt(value) {
2011
- if (!isRecord(value) || !Array.isArray(value.tables))
2012
- return false;
2013
- return "catalog" in value;
2014
- }
2015
- function isConfig(value) {
2016
- return isRecord(value) && typeof value.schema === "string";
2017
- }
2018
- function isRecord(value) {
2019
- return typeof value === "object" && value !== null;
2020
- }
2021
- var SKIP_SCAN = new Set(["node_modules", "dist", "coverage"]);
2022
- function schemaRequestsValidation(built) {
2023
- const flag = built.validation;
2024
- for (const item of built.tables) {
2025
- if (writeWouldValidate(flag, item, {}))
2026
- return true;
2027
- }
2028
- return false;
2029
- }
2030
- function projectImportsValidate(cwd) {
2031
- return directoryImportsValidate(cwd);
2032
- }
2033
- function directoryImportsValidate(dir) {
2034
- let entries;
2035
- try {
2036
- entries = readdirSync(dir, { withFileTypes: true });
2037
- } catch {
2038
- return false;
2039
- }
2040
- for (const entry of entries) {
2041
- if (entry.name.startsWith(".") || SKIP_SCAN.has(entry.name))
2042
- continue;
2043
- const path = join(dir, entry.name);
2044
- if (entry.isDirectory()) {
2045
- if (directoryImportsValidate(path))
2046
- return true;
2047
- continue;
2048
- }
2049
- if (!/\.(?:ts|tsx|mts|cts)$/.test(entry.name))
2050
- continue;
2051
- if (runtimeImportsValidate(readFileSync(path, "utf8")))
2052
- return true;
2053
- }
2054
- return false;
2055
- }
2056
- function runtimeImportsValidate(source) {
2057
- const text = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
2058
- const pattern = /["']okmodel\/validate(?:\.js)?["']/g;
2059
- for (const match of text.matchAll(pattern)) {
2060
- const index = match.index ?? 0;
2061
- const lead = text.slice(Math.max(0, index - 240), index);
2062
- const start = Math.max(lead.lastIndexOf(`
2063
- `), lead.lastIndexOf(";"));
2064
- const head = lead.slice(start + 1);
2065
- if (/\bimport\s+type\b/.test(head) || /\bexport\s+type\b/.test(head))
2066
- continue;
2067
- if (/\bimport\b/.test(head) || /\bexport\b/.test(head))
2068
- return true;
2069
- }
2070
- return false;
2071
- }
2072
-
2073
- export { quoteIdent, planMigration, formatPlan, parsePlan, assertTargetPolicy, assertDirectConnection, listTargets, selectTarget, buildProject, generateProject, planProject, checkProject, projectHead, loadConfig };