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,38 +1,50 @@
1
+ import"../../shared/8jb4ymh2.js";
1
2
  import {
2
3
  checkPresetNames,
3
4
  duplicatePreset
4
5
  } from "../../shared/nsva2a6b.js";
5
6
  import {
6
7
  checkTrait
7
- } from "../../shared/g1b5zzj9.js";
8
+ } from "../../shared/n0dgs0pb.js";
9
+ import {
10
+ quoteIdent
11
+ } from "../../shared/41kv2xdm.js";
12
+ import {
13
+ functionObject,
14
+ triggerObject
15
+ } from "../../shared/9x25fxhc.js";
8
16
  import {
9
17
  timestamptz2
10
- } from "../../shared/xkz8g6by.js";
18
+ } from "../../shared/1g0drakv.js";
11
19
  import {
12
20
  uuid2
13
- } from "../../shared/ps43134h.js";
21
+ } from "../../shared/dyj86cmj.js";
14
22
  import {
15
23
  snakeCase
16
- } from "../../shared/x9v1qgeb.js";
24
+ } from "../../shared/jsd07prz.js";
17
25
  import {
18
26
  ColumnBuilder2,
19
27
  retarget
20
- } from "../../shared/dd4f8mnx.js";
28
+ } from "../../shared/c9z5g0fm.js";
21
29
  import {
22
30
  definition,
23
31
  unavailable
24
- } from "../../shared/xatfg0gn.js";
32
+ } from "../../shared/tmbdywx0.js";
25
33
  import {
34
+ staticNamespace2,
26
35
  compareText
27
36
  } from "../../shared/t4tbsy9h.js";
28
37
  import {
29
38
  queryHandle,
30
39
  attachHttp,
31
40
  settleCall
32
- } from "../../shared/mxmcdsh5.js";
41
+ } from "../../shared/kkzwp3d2.js";
33
42
  import {
34
43
  installArchiveInspect
35
44
  } from "../../shared/xrfnq5k8.js";
45
+ import {
46
+ fitIdentifier2
47
+ } from "../../shared/nfzc5z0w.js";
36
48
  import {
37
49
  OkmError2,
38
50
  catalogError2,
@@ -256,6 +268,124 @@ function list(names) {
256
268
  return names.join(", ");
257
269
  }
258
270
 
271
+ // src/dialects/pg/fn/touch.ts
272
+ function timestampRecords(built) {
273
+ const columns = updatedColumns(built);
274
+ if (columns.length === 0)
275
+ return [];
276
+ const namespace = staticNamespace2("public");
277
+ const byColumn = new Map;
278
+ for (const column of columns) {
279
+ const list = byColumn.get(column.name) ?? [];
280
+ list.push(column.parent);
281
+ byColumn.set(column.name, list);
282
+ }
283
+ const records = [];
284
+ for (const [column, tables] of byColumn) {
285
+ const name = fitIdentifier2(`okm_touch_${column}`);
286
+ const present = hasFunction(built, name) || records.some((object) => object.kind === "function" && object.identity.name === name);
287
+ if (!present) {
288
+ records.push(functionObject({
289
+ namespace,
290
+ name,
291
+ returns: "trigger",
292
+ language: "plpgsql",
293
+ volatility: "volatile",
294
+ body: `begin new.${quoteIdent(column)} = now(); return new; end`,
295
+ provenance: { origin: "trait", name: "timestamps" },
296
+ dependencies: []
297
+ }));
298
+ }
299
+ for (const parent of tables) {
300
+ const triggerName = fitIdentifier2(`${parent.name}_touch`);
301
+ if (hasTrigger(built, parent.name, triggerName))
302
+ continue;
303
+ if (records.some((object) => object.kind === "trigger" && object.identity.name === triggerName && object.identity.parent.name === parent.name)) {
304
+ continue;
305
+ }
306
+ records.push(triggerObject({
307
+ parent,
308
+ name: triggerName,
309
+ timing: "before",
310
+ events: ["update"],
311
+ level: "row",
312
+ calls: { namespace, name, argTypes: [] },
313
+ provenance: { origin: "trait", name: "timestamps" }
314
+ }));
315
+ }
316
+ }
317
+ return records;
318
+ }
319
+ function updatedColumns(built) {
320
+ if (!Array.isArray(built))
321
+ return [];
322
+ const found = [];
323
+ for (const object of built) {
324
+ if (typeof object !== "object" || object === null)
325
+ continue;
326
+ if (!("kind" in object) || object.kind !== "column")
327
+ continue;
328
+ if (!("provenance" in object) || !("identity" in object))
329
+ continue;
330
+ const provenance = object.provenance;
331
+ const identity = object.identity;
332
+ if (typeof provenance !== "object" || provenance === null)
333
+ continue;
334
+ if (typeof identity !== "object" || identity === null)
335
+ continue;
336
+ if (!("origin" in provenance) || provenance.origin !== "trait")
337
+ continue;
338
+ if (!("name" in provenance) || provenance.name !== "timestamps")
339
+ continue;
340
+ if (!("name" in identity) || identity.name !== "updated_at" && identity.name !== "updatedAt")
341
+ continue;
342
+ if (!("parent" in identity) || typeof identity.parent !== "object" || identity.parent === null)
343
+ continue;
344
+ const parent = identity.parent;
345
+ if (!("name" in parent) || typeof parent.name !== "string")
346
+ continue;
347
+ if (!("namespace" in parent) || typeof parent.namespace !== "object" || parent.namespace === null)
348
+ continue;
349
+ const namespace = parent.namespace;
350
+ if (!("form" in namespace) || namespace.form !== "static")
351
+ continue;
352
+ if (!("name" in namespace) || typeof namespace.name !== "string")
353
+ continue;
354
+ found.push({
355
+ parent: { namespace: staticNamespace2(namespace.name), name: parent.name },
356
+ name: identity.name
357
+ });
358
+ }
359
+ return found;
360
+ }
361
+ function hasFunction(built, name) {
362
+ return named(built, "function", name);
363
+ }
364
+ function hasTrigger(built, table, name) {
365
+ if (!Array.isArray(built))
366
+ return false;
367
+ return built.some((object) => {
368
+ if (!isNamed(object, "trigger", name))
369
+ return false;
370
+ const identity = object.identity;
371
+ return typeof identity === "object" && identity !== null && "parent" in identity && typeof identity.parent === "object" && identity.parent !== null && "name" in identity.parent && identity.parent.name === table;
372
+ });
373
+ }
374
+ function named(built, kind, name) {
375
+ if (!Array.isArray(built))
376
+ return false;
377
+ return built.some((object) => isNamed(object, kind, name));
378
+ }
379
+ function isNamed(object, kind, name) {
380
+ if (typeof object !== "object" || object === null)
381
+ return false;
382
+ if (!("kind" in object) || object.kind !== kind)
383
+ return false;
384
+ if (!("identity" in object) || typeof object.identity !== "object" || object.identity === null)
385
+ return false;
386
+ return "name" in object.identity && object.identity.name === name;
387
+ }
388
+
259
389
  // src/runtime/archive-rules.ts
260
390
  function installArchiveContributions() {
261
391
  installArchiveInspect(archiveContributions);
@@ -329,7 +459,7 @@ function refuse(host, table) {
329
459
  }
330
460
  var archiveMod;
331
461
  function loadArchive() {
332
- archiveMod ??= import("../../shared/m9tv7xn7.js");
462
+ archiveMod ??= import("../../shared/radd3z4g.js");
333
463
  return archiveMod;
334
464
  }
335
465
  async function settle(host, table, mods) {
@@ -376,6 +506,9 @@ function trait(name, input) {
376
506
  addFields(name, checked.fields, model, ctx);
377
507
  if (input.presets !== undefined)
378
508
  addPresets(name, input.presets, model, ctx);
509
+ },
510
+ contribute() {
511
+ return [];
379
512
  }
380
513
  };
381
514
  }
@@ -398,14 +531,11 @@ function timestamps(options) {
398
531
  definition(`timestamps() option ${key} is not supported. Accepted options: enforce.`);
399
532
  }
400
533
  }
401
- if (options.enforce === "trigger") {
402
- unavailable(`timestamps({ enforce: "trigger" }) is not available yet. It arrives in 0.3.`);
403
- }
404
- if (options.enforce !== undefined) {
534
+ if (options.enforce !== undefined && options.enforce !== "trigger") {
405
535
  definition(`timestamps() enforce ${String(options.enforce)} must be trigger.`);
406
536
  }
407
537
  }
408
- return trait("timestamps", {
538
+ const base = trait("timestamps", {
409
539
  fields: {
410
540
  createdAt: stamped(),
411
541
  updatedAt: stamped()
@@ -413,6 +543,14 @@ function timestamps(options) {
413
543
  touch: ["updatedAt"],
414
544
  sealed: ["createdAt", "updatedAt"]
415
545
  });
546
+ if (options?.enforce !== "trigger")
547
+ return base;
548
+ return {
549
+ ...base,
550
+ contribute(_peers, built) {
551
+ return timestampRecords(built);
552
+ }
553
+ };
416
554
  }
417
555
  function stamped() {
418
556
  return timestamptz2().defaultSql("now()").guarded();
@@ -672,6 +672,8 @@ export type ConnectOptions<S extends QuerySchema> = {
672
672
  * Directory of `catalog.hash` and `catalog.json`.
673
673
  *
674
674
  * When omitted, `connect` reads `.okm` under the process cwd if that hash file exists.
675
+ * The hash file is the fast path. A mismatch then reads `catalog.json` once
676
+ * per process and does not rebuild the catalog from the schema.
675
677
  */
676
678
  readonly catalogDir?: string;
677
679
  readonly logger?: {
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  validationGate
3
- } from "../../shared/3a5ne750.js";
3
+ } from "../../shared/9m80mkac.js";
4
4
  import {
5
5
  addSchemaHook
6
- } from "../../shared/7mht49yd.js";
6
+ } from "../../shared/3p4b1fg0.js";
7
7
  import {
8
8
  queryHandle
9
- } from "../../shared/mxmcdsh5.js";
9
+ } from "../../shared/kkzwp3d2.js";
10
10
  import {
11
11
  OkmError2
12
12
  } from "../../shared/52szzakm.js";
@@ -14,7 +14,7 @@ import {
14
14
  // src/runtime/validate/load.ts
15
15
  var engine;
16
16
  function loadEngine() {
17
- engine ??= import("../../shared/n47rd821.js");
17
+ engine ??= import("../../shared/c8aghh05.js");
18
18
  return engine;
19
19
  }
20
20
 
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  required
3
- } from "./dd4f8mnx.js";
3
+ } from "./c9z5g0fm.js";
4
4
  import {
5
5
  definition,
6
6
  rejected
7
- } from "./xatfg0gn.js";
7
+ } from "./tmbdywx0.js";
8
8
 
9
9
  // src/dialects/pg/temporal.ts
10
10
  var TEMPORAL_MISSING = "Date and time codecs need the Temporal global. okmodel does not ship a polyfill. Use a runtime that provides Temporal, or assign a polyfill to globalThis.Temporal before using these columns.";
@@ -0,0 +1,418 @@
1
+ import {
2
+ parseCatalog2
3
+ } from "./8jb4ymh2.js";
4
+ import"./wqem4jed.js";
5
+ import"./n442h54c.js";
6
+ import {
7
+ applyTarget2,
8
+ backfillTiming2
9
+ } from "./s4mb6a4b.js";
10
+ import {
11
+ introspectSchema,
12
+ sealViews,
13
+ catalogQuery2,
14
+ managedRoleOptions2
15
+ } from "./xghkt7a9.js";
16
+ import {
17
+ loadMigrations,
18
+ catalogsEqual,
19
+ snapshotDifference
20
+ } from "./7nk68fj8.js";
21
+ import {
22
+ lintMigrationDirectory,
23
+ hasError,
24
+ lintRefusal,
25
+ selectTarget,
26
+ openProject
27
+ } from "./q6zsyh31.js";
28
+ import {
29
+ canonicalTypeName,
30
+ classOf,
31
+ strictestClass,
32
+ planMigration,
33
+ parsePlan
34
+ } from "./n5y9zyj1.js";
35
+ import {
36
+ quoteIdent
37
+ } from "./41kv2xdm.js";
38
+ import {
39
+ catalog,
40
+ isDomain
41
+ } from "./g7ez901n.js";
42
+ import {
43
+ identityKey2
44
+ } from "./t4tbsy9h.js";
45
+ import {
46
+ OkmError2
47
+ } from "./52szzakm.js";
48
+
49
+ // src/tooling/migrate/check.ts
50
+ import { readFileSync } from "node:fs";
51
+ import { join } from "node:path";
52
+ import postgres from "postgres";
53
+ var CHECK_SCHEMA_PREFIX = "okm_check_";
54
+ var HISTORY_CODE = "OKM1547";
55
+ var PREVIOUS_CODE = "OKM1548";
56
+ var STALE_CODE = "OKM1549";
57
+ var HISTORY_FIX = "Regenerate the migration so the stored catalog matches the schema the SQL applies.";
58
+ var FORK_FIX = "Generate the later migration from the previous migration's catalog, not from an older parent.";
59
+ var PREVIOUS_FIX = "Keep every table, column, constraint, and type the previous catalog relies on, or classify the migration as contract.";
60
+ var STALE_FIX = "Run okm generate.";
61
+ async function checkMigrations(cwd, flags, options) {
62
+ const opened = await openProject(cwd);
63
+ const target = selectTarget(opened.config, flags.target);
64
+ if (target.protected) {
65
+ throw new OkmError2("OKM1850", `okm migrate check writes a scratch schema and refuses protected target ${target.name}. Point it at a throwaway Postgres.`, {
66
+ kind: "forbidden",
67
+ fix: {
68
+ summary: "Point okm migrate check at a throwaway Postgres. --allow-protected does not apply."
69
+ }
70
+ });
71
+ }
72
+ const directory = join(cwd, opened.config.migrations ?? "migrations");
73
+ const history = readHistory(directory);
74
+ if (options?.provision === true) {
75
+ const equivalence = await proveSnapshot(opened.config, target.url, target.name, flags, history);
76
+ if (equivalence !== undefined)
77
+ throwFailure([equivalence]);
78
+ }
79
+ const previous = previousFailures(history);
80
+ const stale = staleFailure(history, opened.built.catalog);
81
+ const lint = lintFailure(directory, history, opened.config.lintFrom);
82
+ const replayed = await replayHistory(opened.config, target.url, target.name, flags, history);
83
+ const failures = [...replayed, ...previous, ...stale, ...lint];
84
+ if (failures.length > 0)
85
+ throwFailure(failures);
86
+ return `ok ${String(history.length)} migrations
87
+ `;
88
+ }
89
+ function previousCatalogGaps(before, after) {
90
+ const afterBy = new Map(after.objects.map((object) => [identityKey2(object.identity), object]));
91
+ const gaps = [];
92
+ for (const object of before.objects) {
93
+ if (!checkedKind(object.kind))
94
+ continue;
95
+ const next = afterBy.get(identityKey2(object.identity));
96
+ if (next === undefined || next.kind !== object.kind) {
97
+ gaps.push(`${objectLabel(object)} is missing`);
98
+ continue;
99
+ }
100
+ if (object.kind === "column" && next.kind === "column") {
101
+ gaps.push(...columnGaps(object, next));
102
+ }
103
+ if (object.kind === "type" && next.kind === "type" && !sameType(object.definition, next.definition)) {
104
+ gaps.push(`${objectLabel(object)} changed`);
105
+ }
106
+ }
107
+ return gaps;
108
+ }
109
+ function recomputedMigrationClass(plan, before, after) {
110
+ if (plan.steps.length === 0)
111
+ return "expand";
112
+ if (plan.steps.some((step) => step.kind === undefined)) {
113
+ return planMigration({ before, after, name: plan.name }).class;
114
+ }
115
+ return strictestClass(plan.steps.map((step) => classOf(step.kind ?? "raw-sql")));
116
+ }
117
+ function migrationAllowsPreviousGaps(plan, before, after) {
118
+ return recomputedMigrationClass(plan, before, after) === "contract" && plan.class !== "expand";
119
+ }
120
+ function previousFailures(history) {
121
+ const failures = [];
122
+ let before = catalog([]);
123
+ for (const migration of history) {
124
+ const gaps = previousCatalogGaps(before, migration.catalog);
125
+ if (gaps.length > 0 && !migrationAllowsPreviousGaps(migration.plan, before, migration.catalog)) {
126
+ const stated = migration.plan.class === "expand" ? "expand" : recomputedMigrationClass(migration.plan, before, migration.catalog);
127
+ failures.push({
128
+ code: PREVIOUS_CODE,
129
+ message: `${migration.id}: previous catalog is not satisfied (${gaps.join("; ")}). The migration is classified ${stated}.`,
130
+ fix: PREVIOUS_FIX
131
+ });
132
+ }
133
+ before = migration.catalog;
134
+ }
135
+ return failures;
136
+ }
137
+ function staleFailure(history, head) {
138
+ const last = history.at(-1)?.catalog ?? catalog([]);
139
+ if (catalogsEqual(last, head))
140
+ return [];
141
+ return [
142
+ {
143
+ code: STALE_CODE,
144
+ message: "The last migration's catalog does not match the schema.",
145
+ fix: STALE_FIX
146
+ }
147
+ ];
148
+ }
149
+ function lintFailure(directory, history, lintFrom) {
150
+ const findings = lintMigrationDirectory(directory, lintIds(history, lintFrom));
151
+ if (!hasError(findings))
152
+ return [];
153
+ const refusal = lintRefusal(findings);
154
+ return [{ code: refusal.code, message: refusal.message, fix: refusal.fix.summary }];
155
+ }
156
+ function lintIds(history, lintFrom) {
157
+ if (lintFrom === undefined)
158
+ return;
159
+ const ids = history.map((migration) => migration.id);
160
+ const start = ids.indexOf(lintFrom);
161
+ if (start < 0) {
162
+ throw new OkmError2("invalid", `lintFrom ${lintFrom} is not a migration id.`, {
163
+ fix: {
164
+ summary: "Set lintFrom to a migration id. Files before that id are the adoption baseline and are not linted."
165
+ }
166
+ });
167
+ }
168
+ return new Set(ids.slice(start));
169
+ }
170
+ async function proveSnapshot(config, url, target, flags, history) {
171
+ const head = history.at(-1);
172
+ if (head === undefined)
173
+ return;
174
+ const sql = postgres(url, {
175
+ max: 1,
176
+ connect_timeout: 5,
177
+ idle_timeout: 1,
178
+ onnotice: () => {}
179
+ });
180
+ const provided = `${CHECK_SCHEMA_PREFIX}p${crypto.randomUUID().replaceAll("-", "")}`;
181
+ const replayed = `${CHECK_SCHEMA_PREFIX}r${crypto.randomUUID().replaceAll("-", "")}`;
182
+ const timing = {
183
+ ...flags.lockTimeoutMs !== undefined ? { lockTimeoutMs: flags.lockTimeoutMs } : config.timeouts?.lock !== undefined ? { lockTimeoutMs: config.timeouts.lock } : {},
184
+ ...flags.statementTimeoutMs !== undefined ? { statementTimeoutMs: flags.statementTimeoutMs } : config.timeouts?.statement !== undefined ? { statementTimeoutMs: config.timeouts.statement } : {},
185
+ ...backfillTiming2(config.backfill)
186
+ };
187
+ try {
188
+ await sql.unsafe(`create schema ${quoteIdent(provided)}`);
189
+ await sql.unsafe(`create schema ${quoteIdent(replayed)}`);
190
+ const role = config.roles === undefined ? {} : { migrationRole: config.roles.migration, schema: provided };
191
+ await applyTarget2({
192
+ url,
193
+ target,
194
+ protected: false,
195
+ allowProtected: false,
196
+ allowPooler: flags.allowPooler || config.allowPooler === true,
197
+ searchPath: provided,
198
+ ...timing,
199
+ migrations: history.map((migration) => ({
200
+ id: migration.id,
201
+ catalogHash: migration.catalogHash,
202
+ steps: migration.steps
203
+ })),
204
+ snapshot: {
205
+ catalog: head.catalog,
206
+ migrationId: head.id,
207
+ catalogHash: head.catalogHash,
208
+ reference: [],
209
+ only: true
210
+ },
211
+ ...role
212
+ });
213
+ await applyTarget2({
214
+ url,
215
+ target,
216
+ protected: false,
217
+ allowProtected: false,
218
+ allowPooler: flags.allowPooler || config.allowPooler === true,
219
+ searchPath: replayed,
220
+ ...timing,
221
+ migrations: history.map((migration) => ({
222
+ id: migration.id,
223
+ catalogHash: migration.catalogHash,
224
+ steps: retargetSteps(migration.steps, replayed)
225
+ })),
226
+ ...config.roles !== undefined ? { migrationRole: config.roles.migration, schema: replayed } : {}
227
+ });
228
+ const runner = catalogQuery2(sql);
229
+ const liveProvided = await introspectSchema(runner, provided, "public", managedRoleOptions2(config.roles));
230
+ const liveReplayed = await introspectSchema(runner, replayed, "public", managedRoleOptions2(config.roles));
231
+ const difference = snapshotDifference(liveProvided, liveReplayed);
232
+ if (difference === undefined)
233
+ return;
234
+ return {
235
+ code: "OKM1521",
236
+ message: `Provisioning from the snapshot does not match the replayed history.
237
+ ${difference}`,
238
+ fix: "Regenerate the snapshot from the history. okm migrate check --provision is the command that reports this."
239
+ };
240
+ } finally {
241
+ await sql.unsafe(`drop schema if exists ${quoteIdent(provided)} cascade`).catch(() => {
242
+ return;
243
+ });
244
+ await sql.unsafe(`drop schema if exists ${quoteIdent(replayed)} cascade`).catch(() => {
245
+ return;
246
+ });
247
+ await sql.end({ timeout: 5 });
248
+ }
249
+ }
250
+ async function replayHistory(config, url, target, flags, history) {
251
+ if (history.length === 0)
252
+ return [];
253
+ const sql = postgres(url, {
254
+ max: 1,
255
+ connect_timeout: 5,
256
+ idle_timeout: 1,
257
+ onnotice: () => {}
258
+ });
259
+ const scratch = `${CHECK_SCHEMA_PREFIX}${crypto.randomUUID().replaceAll("-", "")}`;
260
+ const failures = [];
261
+ try {
262
+ await sql.unsafe(`create schema ${quoteIdent(scratch)}`);
263
+ const runner = catalogQuery2(sql);
264
+ for (let index = 0;index < history.length; index += 1) {
265
+ const migration = history[index];
266
+ if (migration === undefined)
267
+ continue;
268
+ await applyTarget2({
269
+ url,
270
+ target,
271
+ protected: false,
272
+ allowProtected: false,
273
+ allowPooler: flags.allowPooler || config.allowPooler === true,
274
+ searchPath: scratch,
275
+ ...flags.lockTimeoutMs !== undefined ? { lockTimeoutMs: flags.lockTimeoutMs } : config.timeouts?.lock !== undefined ? { lockTimeoutMs: config.timeouts.lock } : {},
276
+ ...flags.statementTimeoutMs !== undefined ? { statementTimeoutMs: flags.statementTimeoutMs } : config.timeouts?.statement !== undefined ? { statementTimeoutMs: config.timeouts.statement } : {},
277
+ ...backfillTiming2(config.backfill),
278
+ migrations: [
279
+ {
280
+ id: migration.id,
281
+ catalogHash: migration.catalogHash,
282
+ steps: retargetSteps(migration.steps, scratch)
283
+ }
284
+ ],
285
+ ...config.roles !== undefined ? { migrationRole: config.roles.migration, schema: scratch } : {}
286
+ });
287
+ const live = await introspectSchema(runner, scratch, "public", managedRoleOptions2(config.roles));
288
+ const sealed = await sealViews(runner, migration.catalog);
289
+ const back = planMigration({ before: live, after: sealed, name: migration.id });
290
+ if (back.steps.length === 0)
291
+ continue;
292
+ failures.push(historyFailure(history, index, back));
293
+ break;
294
+ }
295
+ } finally {
296
+ await sql.unsafe(`drop schema if exists ${quoteIdent(scratch)} cascade`).catch(() => {
297
+ return;
298
+ });
299
+ await sql.end({ timeout: 5 });
300
+ }
301
+ return failures;
302
+ }
303
+ function historyFailure(history, index, back) {
304
+ const migration = history[index];
305
+ const id = migration?.id ?? "migration";
306
+ const fork = migration === undefined ? undefined : forkMessage(history, index);
307
+ if (fork !== undefined)
308
+ return { code: HISTORY_CODE, message: fork, fix: FORK_FIX };
309
+ const steps = back.steps.map((step) => step.sql).join(`
310
+ `);
311
+ return {
312
+ code: HISTORY_CODE,
313
+ message: `${id}: history differs from the stored catalog
314
+ ${steps}`,
315
+ fix: HISTORY_FIX
316
+ };
317
+ }
318
+ function forkMessage(history, index) {
319
+ const current = history[index];
320
+ const previous = history[index - 1];
321
+ if (current === undefined || previous === undefined)
322
+ return;
323
+ if (samePlan(previous.catalog, current.catalog, current.plan))
324
+ return;
325
+ for (let parentIndex = 0;parentIndex < index; parentIndex += 1) {
326
+ const parent = parentIndex === 0 ? catalog([]) : history[parentIndex - 1]?.catalog;
327
+ if (parent === undefined)
328
+ continue;
329
+ if (!samePlan(parent, current.catalog, current.plan))
330
+ continue;
331
+ return `migration ${current.id} was generated from a different parent than ${previous.id}`;
332
+ }
333
+ return;
334
+ }
335
+ function samePlan(before, after, file) {
336
+ try {
337
+ return sameSql(planMigration({ before, after, name: "parent" }), file);
338
+ } catch (error) {
339
+ if (error instanceof OkmError2 && error.code === "OKM1530")
340
+ return false;
341
+ throw error;
342
+ }
343
+ }
344
+ function sameSql(planned, file) {
345
+ if (planned.steps.length !== file.steps.length)
346
+ return false;
347
+ return planned.steps.every((step, index) => step.sql === file.steps[index]?.sql);
348
+ }
349
+ function retargetSteps(steps, schema) {
350
+ const from = `${quoteIdent("public")}.`;
351
+ const to = `${quoteIdent(schema)}.`;
352
+ return steps.map((step) => ({
353
+ ...step,
354
+ sql: step.sql.replaceAll(from, to),
355
+ ...step.backfill === undefined ? {} : { backfill: { ...step.backfill, table: step.backfill.table.replaceAll(from, to) } }
356
+ }));
357
+ }
358
+ function readHistory(directory) {
359
+ return loadMigrations(directory).map((migration) => ({
360
+ id: migration.id,
361
+ catalogHash: migration.catalogHash,
362
+ steps: migration.steps,
363
+ plan: parsePlan(readFileSync(join(directory, `${migration.id}.sql`), "utf8")),
364
+ catalog: parseCatalog2(readFileSync(join(directory, `${migration.id}.catalog.json`), "utf8"))
365
+ }));
366
+ }
367
+ function throwFailure(failures) {
368
+ const first = failures[0];
369
+ if (first === undefined)
370
+ throw new Error("okm migrate check has no failure to report.");
371
+ const message = failures.length === 1 ? first.message : failures.map((failure) => `${failure.code} ${failure.message}`).join(`
372
+ `);
373
+ throw new OkmError2(first.code, message, { fix: { summary: first.fix } });
374
+ }
375
+ function checkedKind(kind) {
376
+ return kind === "table" || kind === "column" || kind === "constraint" || kind === "type";
377
+ }
378
+ function columnGaps(before, after) {
379
+ const gaps = [];
380
+ const place = objectLabel(before);
381
+ if (canonicalTypeName(before.definition.dataType) !== canonicalTypeName(after.definition.dataType)) {
382
+ gaps.push(`${place} changed type from ${before.definition.dataType} to ${after.definition.dataType}`);
383
+ }
384
+ if (before.definition.nullable && !after.definition.nullable && !columnHasDefault(after)) {
385
+ gaps.push(`${place} nullability tightened without a default`);
386
+ }
387
+ return gaps;
388
+ }
389
+ function columnHasDefault(column) {
390
+ return column.definition.defaultExpression !== undefined || column.definition.identity !== undefined || column.definition.generated !== undefined;
391
+ }
392
+ function sameType(before, after) {
393
+ const beforeDomain = isDomain(before);
394
+ const afterDomain = isDomain(after);
395
+ if (beforeDomain !== afterDomain)
396
+ return false;
397
+ if (beforeDomain && afterDomain) {
398
+ return canonicalTypeName(before.base) === canonicalTypeName(after.base) && before.check === after.check;
399
+ }
400
+ if (!beforeDomain && !afterDomain) {
401
+ if (before.labels.length !== after.labels.length)
402
+ return false;
403
+ return before.labels.every((label, index) => label === after.labels[index]);
404
+ }
405
+ return false;
406
+ }
407
+ function objectLabel(object) {
408
+ if (object.kind === "column" || object.kind === "constraint") {
409
+ return `${object.kind} ${object.identity.parent.name}.${object.identity.name}`;
410
+ }
411
+ if (object.kind === "table" || object.kind === "type") {
412
+ return `${object.kind} ${object.identity.name}`;
413
+ }
414
+ return object.kind;
415
+ }
416
+ export {
417
+ checkMigrations
418
+ };