okmodel 0.5.0 → 0.6.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 (169) hide show
  1. package/README.md +36 -18
  2. package/dist/adapters/pg/no-inflight-timeout.d.ts +17 -0
  3. package/dist/adapters/pg/postgres-extra.d.ts +22 -0
  4. package/dist/contracts/catalog/document.d.ts +19 -5
  5. package/dist/contracts/catalog/object.d.ts +2 -1
  6. package/dist/contracts/catalog/policy.d.ts +48 -0
  7. package/dist/contracts/catalog/types.d.ts +25 -1
  8. package/dist/contracts/catalog/view.d.ts +2 -0
  9. package/dist/contracts/internal.js +1 -1
  10. package/dist/contracts/location.d.ts +10 -0
  11. package/dist/dialects/pg/column.d.ts +13 -2
  12. package/dist/dialects/pg/ddl.d.ts +37 -1
  13. package/dist/dialects/pg/emit.d.ts +19 -1
  14. package/dist/dialects/pg/errors.d.ts +32 -1
  15. package/dist/dialects/pg/ext/citext.js +1 -1
  16. package/dist/dialects/pg/ext/index.js +1 -1
  17. package/dist/dialects/pg/ext/pg-trgm.js +1 -1
  18. package/dist/dialects/pg/filter-parse.d.ts +10 -3
  19. package/dist/dialects/pg/fn/index.js +10 -2
  20. package/dist/dialects/pg/index.d.ts +2 -1
  21. package/dist/dialects/pg/index.js +29 -17
  22. package/dist/dialects/pg/introspect.d.ts +7 -0
  23. package/dist/dialects/pg/model.d.ts +7 -2
  24. package/dist/dialects/pg/ops/or.d.ts +1 -1
  25. package/dist/dialects/pg/owner.d.ts +42 -0
  26. package/dist/dialects/pg/path-endpoint.d.ts +20 -0
  27. package/dist/dialects/pg/role/check.d.ts +3 -0
  28. package/dist/dialects/pg/role/index.d.ts +71 -6
  29. package/dist/dialects/pg/role/sql.d.ts +11 -0
  30. package/dist/dialects/pg/schema.d.ts +2 -0
  31. package/dist/dialects/pg/table.d.ts +25 -3
  32. package/dist/dialects/pg/temporal.d.ts +5 -76
  33. package/dist/dialects/pg/tenancy.d.ts +45 -3
  34. package/dist/dialects/pg/view/client.d.ts +19 -1
  35. package/dist/dialects/pg/view/index.d.ts +4 -0
  36. package/dist/dialects/pg/view/index.js +21 -178
  37. package/dist/dialects/pg/view/refresh.d.ts +24 -0
  38. package/dist/runtime/batch.d.ts +1 -1
  39. package/dist/runtime/client.d.ts +6 -1
  40. package/dist/runtime/conflict.d.ts +2 -1
  41. package/dist/runtime/drift.d.ts +9 -0
  42. package/dist/runtime/inflight.d.ts +14 -0
  43. package/dist/runtime/pg/bun.js +6 -5
  44. package/dist/runtime/pg/pg.js +5 -5
  45. package/dist/runtime/pg/pglite.js +6 -5
  46. package/dist/runtime/pg/postgresjs.js +5 -5
  47. package/dist/runtime/plan.d.ts +22 -1
  48. package/dist/runtime/predicate.d.ts +19 -0
  49. package/dist/runtime/safety/index.js +1 -1
  50. package/dist/runtime/tenancy/composite.d.ts +26 -0
  51. package/dist/runtime/tenancy/context.d.ts +16 -0
  52. package/dist/runtime/tenancy/index.d.ts +9 -5
  53. package/dist/runtime/tenancy/index.js +1508 -37
  54. package/dist/runtime/tenancy/path.d.ts +87 -0
  55. package/dist/runtime/tenancy/rls.d.ts +67 -0
  56. package/dist/runtime/tenancy/via.d.ts +22 -0
  57. package/dist/runtime/traits/index.js +17 -11
  58. package/dist/runtime/types.d.ts +13 -1
  59. package/dist/runtime/validate/index.js +4 -4
  60. package/dist/runtime/write.d.ts +1 -1
  61. package/dist/shared/09p049xa.js +702 -0
  62. package/dist/shared/{ykv7wh5b.js → 1f0xa3cb.js} +5 -5
  63. package/dist/shared/1nw1f3vs.js +23 -0
  64. package/dist/shared/{6p5k20sm.js → 1z04fa85.js} +21 -11
  65. package/dist/shared/{p7pfx624.js → 2cg3wnxc.js} +314 -65
  66. package/dist/shared/{dyj86cmj.js → 2jd1sn0e.js} +2 -2
  67. package/dist/shared/{qgvt8nx1.js → 2z7wv5tj.js} +21 -5
  68. package/dist/shared/{kyxb62mc.js → 31vm914h.js} +9 -1
  69. package/dist/shared/{sf8m6tfy.js → 32cnm7kn.js} +8 -3
  70. package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
  71. package/dist/shared/{jdgqqx31.js → 3fqwmr09.js} +38 -14
  72. package/dist/shared/{922qbgnw.js → 3qnhc7w5.js} +16 -9
  73. package/dist/shared/{zvbjc949.js → 4az561fn.js} +1 -1
  74. package/dist/shared/{k3ted12y.js → 54tqkxja.js} +20 -2
  75. package/dist/shared/{k7cc9nzk.js → 5d8t84hg.js} +15 -3
  76. package/dist/shared/{fa9vt245.js → 5j36z8dn.js} +17 -6
  77. package/dist/shared/{1g0drakv.js → 5yv4k6a6.js} +1 -1
  78. package/dist/shared/8dqhth8c.js +12 -0
  79. package/dist/shared/8etn7d8w.js +62 -0
  80. package/dist/shared/{g516zdsb.js → 8x72s86k.js} +476 -30
  81. package/dist/shared/{nkc9sfqb.js → 929h19xp.js} +145 -7
  82. package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
  83. package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
  84. package/dist/shared/{b066g80h.js → ag4c7516.js} +2 -2
  85. package/dist/shared/b3awxn7h.js +45 -0
  86. package/dist/shared/b4v57v3x.js +9 -0
  87. package/dist/shared/bdk4sty6.js +58 -0
  88. package/dist/shared/{s6gqfn5j.js → c58secgj.js} +125 -8
  89. package/dist/shared/{rrstp3kb.js → cgbamx3e.js} +2 -2
  90. package/dist/shared/d5azex6s.js +6 -0
  91. package/dist/shared/{5fqx9yh4.js → dk8v79p3.js} +3 -1
  92. package/dist/shared/{4m9fn52z.js → ed0b8k4s.js} +1 -1
  93. package/dist/shared/{z2rnngj4.js → ek2wt2dz.js} +8 -5
  94. package/dist/shared/{wxn4b7pp.js → f0f1n281.js} +59 -12
  95. package/dist/shared/{gdh9mrtp.js → g90m9b0g.js} +3 -2
  96. package/dist/shared/{tbszx8mz.js → gwn6v667.js} +2 -2
  97. package/dist/shared/{c8aghh05.js → h15jx9bc.js} +8 -2
  98. package/dist/shared/{3p4b1fg0.js → hazqqecq.js} +17 -93
  99. package/dist/shared/{4ef7mmqf.js → j3rkk835.js} +34 -11
  100. package/dist/shared/{bt9sfq0z.js → j60w0dvr.js} +56 -21
  101. package/dist/shared/{ycatkxmq.js → jgqj11w4.js} +1 -1
  102. package/dist/shared/k68eedmy.js +85 -0
  103. package/dist/shared/{szm0cksa.js → knmes2kh.js} +34 -6
  104. package/dist/shared/{ckjbyrt9.js → mb2ycs1p.js} +7 -9
  105. package/dist/shared/{tq9afp8d.js → n076ydsc.js} +1 -1
  106. package/dist/shared/{xhh1srw3.js → nx9hh281.js} +50 -32
  107. package/dist/shared/{kdkxh3kb.js → p248q79y.js} +58 -59
  108. package/dist/shared/{990a291j.js → p6bjzq9q.js} +14 -9
  109. package/dist/shared/{h4nqvjkp.js → pjr6xrm5.js} +2 -2
  110. package/dist/shared/pz6rj8ay.js +212 -0
  111. package/dist/shared/{5vdm4sgp.js → q3j1tq7a.js} +36 -23
  112. package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
  113. package/dist/shared/{dxjp14rk.js → r11n8grd.js} +2 -2
  114. package/dist/shared/r1fdd0f6.js +19 -0
  115. package/dist/shared/{8ktwd3rh.js → r3j8s5y0.js} +1 -1
  116. package/dist/shared/rhs3xe4k.js +7 -0
  117. package/dist/shared/{jsd07prz.js → rpycarcr.js} +6 -6
  118. package/dist/shared/{xsq7v6wp.js → sbdme3vy.js} +14 -2
  119. package/dist/shared/{xdpj5kkm.js → t3g0k88x.js} +775 -264
  120. package/dist/shared/{ayz64vzj.js → ttmg09vd.js} +708 -19
  121. package/dist/shared/{r6pj4fq2.js → venqrjna.js} +8 -4
  122. package/dist/shared/{fancm5pt.js → vjpnzwjz.js} +138 -15
  123. package/dist/shared/{9tgmr1gh.js → vmfkhszw.js} +44 -9
  124. package/dist/shared/{wqem4jed.js → wkme6tep.js} +11 -9
  125. package/dist/shared/{qy9rdq0g.js → wt5rzv1h.js} +15 -11
  126. package/dist/shared/wxr8cwns.js +6 -0
  127. package/dist/shared/{bzsjtd7c.js → wynzeggs.js} +19 -14
  128. package/dist/shared/x3jc53kg.js +220 -0
  129. package/dist/shared/{7p2wbz7f.js → x72jb7ad.js} +1 -1
  130. package/dist/shared/{fyqkk1m0.js → xq0mvhts.js} +10 -9
  131. package/dist/shared/xqve9598.js +53 -0
  132. package/dist/shared/{47xzb9fc.js → y2t3jktd.js} +2 -2
  133. package/dist/shared/{9x25fxhc.js → y34a8714.js} +1 -1
  134. package/dist/shared/{k7dwkhvq.js → y6jdds7g.js} +4 -1
  135. package/dist/shared/{h8ne16fq.js → ys0xzyaj.js} +1 -1
  136. package/dist/shared/{c9z5g0fm.js → ywr8hmey.js} +5 -2
  137. package/dist/shared/{g7ez901n.js → ywspezzx.js} +1 -1
  138. package/dist/shared/{9m80mkac.js → ze4hxsmy.js} +1 -1
  139. package/dist/shared/{fb00y8fp.js → zm0mafk4.js} +12 -6
  140. package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
  141. package/dist/tooling/cli.js +97 -24
  142. package/dist/tooling/migrate/apply.d.ts +14 -0
  143. package/dist/tooling/migrate/check.d.ts +3 -4
  144. package/dist/tooling/migrate/classify.d.ts +4 -0
  145. package/dist/tooling/migrate/commands.d.ts +1 -2
  146. package/dist/tooling/migrate/config.d.ts +3 -2
  147. package/dist/tooling/migrate/drift.d.ts +1 -1
  148. package/dist/tooling/migrate/estimate.d.ts +14 -1
  149. package/dist/tooling/migrate/files.d.ts +11 -1
  150. package/dist/tooling/migrate/index.js +17 -16
  151. package/dist/tooling/migrate/lint.d.ts +4 -4
  152. package/dist/tooling/migrate/lost.d.ts +28 -0
  153. package/dist/tooling/migrate/paths.d.ts +14 -0
  154. package/dist/tooling/migrate/plan.d.ts +10 -3
  155. package/dist/tooling/migrate/policy.d.ts +11 -0
  156. package/dist/tooling/migrate/runner.d.ts +25 -0
  157. package/dist/tooling/migrate/sandbox.d.ts +57 -0
  158. package/dist/tooling/migrate/sql-lex.d.ts +39 -0
  159. package/dist/tooling/migrate/text.d.ts +42 -0
  160. package/dist/tooling/node-version.d.ts +21 -0
  161. package/dist/tooling/testing/factories.d.ts +10 -0
  162. package/dist/tooling/testing/facts.d.ts +33 -5
  163. package/dist/tooling/testing/index.d.ts +1 -1
  164. package/dist/tooling/testing/index.js +3 -3
  165. package/dist/tooling/testing/isolation.d.ts +15 -8
  166. package/package.json +10 -6
  167. package/dist/shared/0v1g424p.js +0 -26
  168. package/dist/shared/r73pt594.js +0 -426
  169. package/dist/shared/rasvvzgz.js +0 -21
@@ -9,46 +9,71 @@ import {
9
9
  serializeCatalog2,
10
10
  catalogHash2,
11
11
  startupCatalog2,
12
+ catalogFileText2,
12
13
  parseCatalog2
13
- } from "./8bwbwy8h.js";
14
+ } from "./qq3899ht.js";
14
15
  import {
15
16
  open2
16
- } from "./wqem4jed.js";
17
+ } from "./wkme6tep.js";
17
18
  import {
19
+ sqlStatements,
20
+ transactionControl,
18
21
  canonicalTypeName,
22
+ prepareMigrationText,
23
+ assertMigrationSteps,
24
+ columnPrivilege,
19
25
  batchSizeField,
20
26
  aboutBatchCount,
21
27
  parseReplace,
22
28
  planMigration,
23
29
  formatPlan,
24
30
  parsePlan,
25
- staleRenames
26
- } from "./ayz64vzj.js";
31
+ staleRenames,
32
+ sessionFailure,
33
+ finishQuiet,
34
+ assertTargetPolicy,
35
+ assertTargetAlias,
36
+ listTargets,
37
+ selectTarget
38
+ } from "./ttmg09vd.js";
27
39
  import {
28
40
  contributedFields
29
41
  } from "./n0dgs0pb.js";
30
42
  import {
31
43
  assertValidation,
32
44
  writeWouldValidate
33
- } from "./9m80mkac.js";
45
+ } from "./ze4hxsmy.js";
46
+ import {
47
+ tableOwner,
48
+ stampOwnership
49
+ } from "./xqve9598.js";
34
50
  import {
35
51
  catalog,
36
52
  isDomain
37
- } from "./g7ez901n.js";
53
+ } from "./ywspezzx.js";
38
54
  import {
39
55
  snakeCase,
40
56
  emittedTypeName
41
- } from "./jsd07prz.js";
57
+ } from "./rpycarcr.js";
42
58
  import {
43
59
  ColumnBuilder2
44
- } from "./c9z5g0fm.js";
60
+ } from "./ywr8hmey.js";
45
61
  import {
46
62
  staticNamespace2
47
- } from "./t4tbsy9h.js";
63
+ } from "./358zsv4r.js";
64
+ import {
65
+ sha2562
66
+ } from "./nfzc5z0w.js";
48
67
  import {
49
68
  OkmError2
50
69
  } from "./52szzakm.js";
51
70
 
71
+ // src/tooling/migrate/paths.ts
72
+ import { resolve } from "node:path";
73
+ function projectPath(cwd, ...parts) {
74
+ return resolve(cwd, ...parts);
75
+ }
76
+
52
77
  // src/tooling/errors/registry.ts
53
78
  var ERROR_DOCS = [
54
79
  {
@@ -150,8 +175,8 @@ var ERROR_DOCS = [
150
175
  {
151
176
  code: "OKM1102",
152
177
  title: "Unfiltered write",
153
- summary: "update or delete would run with an empty where.",
154
- fix: "Pass a filter. undefined in a write filter does not mean every row."
178
+ summary: "A write with no effective predicate, including undefined values, is OKM1102.",
179
+ fix: "Pass a predicate, or .all(reason). An undefined-only where is the same as {}."
155
180
  },
156
181
  {
157
182
  code: "OKM1104",
@@ -180,14 +205,14 @@ var ERROR_DOCS = [
180
205
  {
181
206
  code: "OKM1120",
182
207
  title: "Unknown field",
183
- summary: "A field name in where, select, orderBy, or include is not on the table.",
184
- fix: "Use a field from the catalog. The error includes a nearest-name hint when one is close."
208
+ summary: "The name is not a field or an option. timeout and signal go in the second argument of update. Options go first: tx(options, fn).",
209
+ fix: "Use a field from the catalog, or move the option. The error includes a nearest-name hint when one is close."
185
210
  },
186
211
  {
187
212
  code: "OKM1121",
188
213
  title: "Object as value",
189
- summary: "A plain object was passed where a value is required. JSON cannot create an operator.",
190
- fix: "Wrap the value with eq. eq is the equality form for json and jsonb."
214
+ summary: "An empty or() branch is OKM1121. or() takes one array: or([a, b]). A plain object was passed where a value is required.",
215
+ fix: "Give every or() branch a predicate. Pass an array: or([a, b]). Wrap a json value with eq."
191
216
  },
192
217
  {
193
218
  code: "OKM1122",
@@ -252,8 +277,8 @@ var ERROR_DOCS = [
252
277
  {
253
278
  code: "OKM1510",
254
279
  title: "Unsafe migration",
255
- summary: "A migration has an unresolved lint error, or an override with an empty reason or a code the statement did not trigger.",
256
- fix: "Put `-- okm-allow OKM15xx: reason` on the line above the statement. The code must be one that statement triggered, and the reason must not be empty."
280
+ summary: "A migration has an unresolved lint error, or an override with an empty reason or a code no statement in the step triggered.",
281
+ fix: "Put `-- okm-allow OKM15xx: reason` on the step. The code must be one a statement in that step triggered, and the reason must not be empty. The line silences that code on every statement of the step."
257
282
  },
258
283
  {
259
284
  code: "OKM1511",
@@ -444,8 +469,8 @@ var ERROR_DOCS = [
444
469
  {
445
470
  code: "OKM1542",
446
471
  title: "Data statement",
447
- summary: "A migration contains a data statement outside backfill().",
448
- fix: "Move the statement into backfill(). Data changes are batched and resumable there."
472
+ summary: "A migration contains a data statement outside backfill(), or a DO or CALL whose body is not analysed.",
473
+ fix: "Move insert, update, delete, merge, and truncate into backfill(). DO and CALL are refused because the body is not analysed. Allow OKM1542 with a reason to keep one."
449
474
  },
450
475
  {
451
476
  code: "OKM1543",
@@ -489,6 +514,18 @@ var ERROR_DOCS = [
489
514
  summary: "The last migration's catalog does not match the schema.",
490
515
  fix: "Run okm generate."
491
516
  },
517
+ {
518
+ code: "OKM1550",
519
+ title: "Row-level security weakened",
520
+ summary: "A statement disables row-level security, drops or widens a policy, grants BYPASSRLS, or changes the owner of a tenant table.",
521
+ fix: "Remove the statement, or allow OKM1550 with a reason when the change is reviewed."
522
+ },
523
+ {
524
+ code: "OKM1551",
525
+ title: "Transaction control in a step",
526
+ summary: "A step contains COMMIT, ROLLBACK, or other transaction control, including inside a DO body. A function or procedure body is not this.",
527
+ fix: "Remove the transaction control. okm migrate check replays one transaction and rolls it back. Apply accepts the step only with -- okm-allow OKM1551: reason."
528
+ },
492
529
  {
493
530
  code: "OKM1601",
494
531
  title: "Stale typed SQL",
@@ -528,8 +565,14 @@ var ERROR_DOCS = [
528
565
  {
529
566
  code: "OKM1707",
530
567
  title: "RLS role",
531
- summary: "connect() was given a role that owns the tables or is a superuser, under the rls strategy.",
532
- fix: "Connect as a role that is neither the owner nor a superuser."
568
+ summary: "A scoped call under rls ran as a role that owns the tenant tables, is a superuser, or has BYPASSRLS. The reason field says which. The message names the endpoint.",
569
+ fix: "Connect as a role that is not the table owner, a superuser, or BYPASSRLS. okm seed under rlsTenancy runs as the application role. The owner gets this error."
570
+ },
571
+ {
572
+ code: "OKM1708",
573
+ title: "RLS policy",
574
+ summary: "A scoped call under rls found a tenant table whose row-level security is missing, not forced, or whose policies are not the ones okm generate emits. The message names the table and the endpoint.",
575
+ fix: "Restore the policies okm generate emits for this rls schema."
533
576
  },
534
577
  {
535
578
  code: "OKM1801",
@@ -612,7 +655,7 @@ var ERROR_DOCS = [
612
655
  {
613
656
  code: "OKM1825",
614
657
  title: "Missing privilege",
615
- summary: "The application role lacks a privilege on a managed object.",
658
+ summary: "A role lacks a privilege on a managed object.",
616
659
  fix: "Grant the privilege, or change the role. okm doctor reports the object and the privilege."
617
660
  },
618
661
  {
@@ -686,6 +729,12 @@ var ERROR_DOCS = [
686
729
  title: "Target not named",
687
730
  summary: "Several targets are configured and the command was not given --target.",
688
731
  fix: "Pass --target <name>. The command does not guess."
732
+ },
733
+ {
734
+ code: "OKM1854",
735
+ title: "Pooler session",
736
+ summary: "Two statements on the same connection saw different backend pids. A transaction-mode pooler is in the way.",
737
+ fix: "Use a direct connection, or pass --allow-pooler."
689
738
  }
690
739
  ];
691
740
  var byCode;
@@ -701,9 +750,53 @@ function indexDocs() {
701
750
  return index;
702
751
  }
703
752
 
704
- // src/tooling/migrate/lint.ts
753
+ // src/tooling/migrate/files.ts
705
754
  import { existsSync, readdirSync, readFileSync } from "node:fs";
706
755
  import { join } from "node:path";
756
+ function loadMigrations(directory) {
757
+ if (!existsSync(directory))
758
+ return [];
759
+ const names = readdirSync(directory).filter((file) => file.endsWith(".sql")).sort();
760
+ const migrations = [];
761
+ for (const file of names) {
762
+ const id = file.slice(0, -".sql".length);
763
+ const catalogPath = join(directory, `${id}.catalog.json`);
764
+ if (!existsSync(catalogPath)) {
765
+ throw new OkmError2("invalid", `Migration ${id} is missing its catalog snapshot.`, {
766
+ fix: {
767
+ summary: "Generate the migration again so the SQL and the catalog are written together."
768
+ }
769
+ });
770
+ }
771
+ const plan = readMigrationPlan(join(directory, file));
772
+ const stored = parseCatalog2(readFileSync(catalogPath, "utf8"));
773
+ const startup = startupCatalog2(stored);
774
+ const hash = catalogHash2(startup);
775
+ migrations.push(startup === stored ? { id, catalogHash: hash, steps: plan.steps } : { id, catalogHash: hash, legacyHash: catalogHash2(stored), steps: plan.steps });
776
+ }
777
+ return migrations;
778
+ }
779
+ function readMigrationPlan(path) {
780
+ const text = prepareMigrationText(readFileSync(path, "utf8"), path);
781
+ const plan = parsePlan(text);
782
+ assertMigrationSteps(text, plan.steps.length, path);
783
+ return plan;
784
+ }
785
+ function loadHeadSnapshot(directory) {
786
+ const migrations = loadMigrations(directory);
787
+ const last = migrations.at(-1);
788
+ if (last === undefined)
789
+ return;
790
+ return {
791
+ id: last.id,
792
+ catalogHash: last.catalogHash,
793
+ catalog: parseCatalog2(readFileSync(join(directory, `${last.id}.catalog.json`), "utf8"))
794
+ };
795
+ }
796
+
797
+ // src/tooling/migrate/lint.ts
798
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
799
+ import { join as join2 } from "node:path";
707
800
 
708
801
  // src/tooling/migrate/lint-rules.ts
709
802
  var INTEGER_WIDTH = {
@@ -740,7 +833,12 @@ var STEP_RULES = [
740
833
  rule("OKM1536", "locking", "error", "Adding a foreign key without NOT VALID locks the table while it scans.", (input) => hit(foreignKeyWithoutNotValid(input), "adds a foreign key without NOT VALID")),
741
834
  rule("OKM1537", "locking", "error", "SET NOT NULL locks the table while it scans for nulls.", (input) => hit(unsafeSetNotNull(input), "sets NOT NULL")),
742
835
  rule("OKM1538", "locking", "warning", "A type change that rewrites the table locks it for the rewrite.", (input) => typeChange(input, "rewrite")),
743
- rule("OKM1542", "data-dependent", "error", "A data statement outside backfill() is not a migration step.", (input) => hit(dataStatement(input.step), "changes rows outside backfill()"))
836
+ rule("OKM1542", "data-dependent", "error", "A data statement outside backfill(), or DO or CALL, is not a migration step.", (input) => dataStatement(input.step)),
837
+ rule("OKM1550", "destructive", "error", "Disabling row-level security, dropping a policy, creating or altering one that is not the emitted tenant predicate, granting BYPASSRLS, or changing the owner of a tenant table removes the rlsTenancy backstop.", (input) => weakensRls(input)),
838
+ rule("OKM1551", "destructive", "error", "Transaction control inside a step commits or leaves the migration transaction.", (input) => {
839
+ const word = transactionControl(input.step.sql);
840
+ return word === undefined ? [] : [`contains ${word}`];
841
+ })
744
842
  ];
745
843
  var COLUMN_RULES = [
746
844
  {
@@ -799,13 +897,159 @@ function normalized(sql) {
799
897
  }
800
898
  return out;
801
899
  }
802
- function dataStatement(step) {
803
- if (step.backfill !== undefined)
900
+ function weakensRls(input) {
901
+ const text = normalized(input.step.sql);
902
+ if (/\bdisable row level security\b/.test(text))
903
+ return ["disables row level security"];
904
+ if (/\bno force row level security\b/.test(text))
905
+ return ["stops forcing row level security"];
906
+ if (/^drop policy\b/.test(text))
907
+ return ["drops a policy"];
908
+ const drifted = policyDrift(input, text);
909
+ if (drifted !== undefined)
910
+ return [drifted];
911
+ if (/^(?:alter|create) (?:role|user)\b/.test(text) && /(?<!no)bypassrls\b/.test(text)) {
912
+ return ["grants BYPASSRLS"];
913
+ }
914
+ if (changesTenantOwner(input, text))
915
+ return ["changes the owner of a tenant table"];
916
+ return [];
917
+ }
918
+ function policyDrift(input, text) {
919
+ const header = /^(create|alter) policy\s+(\S+)\s+on\s+(\S+)/.exec(text);
920
+ const kind = header?.[1];
921
+ const table = lastName(header?.[3]);
922
+ if (kind === undefined || table === undefined)
923
+ return;
924
+ if (!hasPolicy(input.before, table) && !hasPolicy(input.after, table))
925
+ return;
926
+ const name = lastName(header?.[2]) ?? "";
927
+ const using = clause(text, "using");
928
+ const check = clause(text, "with check");
929
+ if (isTrue(using) || isTrue(check)) {
930
+ return kind === "alter" ? "widens a policy to match every row" : "creates a policy that matches every row";
931
+ }
932
+ if (kind === "alter" && using === undefined && check === undefined)
933
+ return;
934
+ const tenant = policyExpression(input, table, `${table}_tenant`);
935
+ const unscoped = policyExpression(input, table, `${table}_unscoped_select`);
936
+ const emitted = !/\bas restrictive\b/.test(text) && (matchesEmitted(name, `${table}_tenant`, using, check, tenant) || matchesEmitted(name, `${table}_unscoped_select`, using, check, unscoped));
937
+ if (emitted)
938
+ return;
939
+ return kind === "alter" ? "widens a policy away from the tenant predicate" : "creates a policy that is not the tenant predicate";
940
+ }
941
+ function matchesEmitted(name, expectedName, using, check, expression) {
942
+ if (name !== expectedName || expression === undefined || using === undefined)
804
943
  return false;
944
+ if (!sameExpression(using, expression))
945
+ return false;
946
+ return check === undefined || sameExpression(check, expression);
947
+ }
948
+ function sameExpression(found, expected) {
949
+ return found === expected || unwrap(found) === unwrap(expected);
950
+ }
951
+ function unwrap(expression) {
952
+ const body = expression.trim();
953
+ if (!body.startsWith("(") || !body.endsWith(")"))
954
+ return body;
955
+ let depth = 0;
956
+ for (let index = 0;index < body.length; index += 1) {
957
+ const char = body[index];
958
+ if (char === "(")
959
+ depth += 1;
960
+ else if (char === ")") {
961
+ depth -= 1;
962
+ if (depth === 0)
963
+ return index === body.length - 1 ? body.slice(1, -1).trim() : body;
964
+ }
965
+ }
966
+ return body;
967
+ }
968
+ function isTrue(expression) {
969
+ return expression !== undefined && expression.replaceAll(" ", "") === "(true)";
970
+ }
971
+ function clause(text, keyword) {
972
+ const needle = keyword === "using" ? /\busing\s*\(/g : /\bwith check\s*\(/g;
973
+ const found = needle.exec(text);
974
+ if (found === null)
975
+ return;
976
+ return parenthesized(text, found.index + found[0].length - 1);
977
+ }
978
+ function parenthesized(text, open) {
979
+ let depth = 0;
980
+ let quoted = false;
981
+ let single = false;
982
+ for (let index = open;index < text.length; index += 1) {
983
+ const char = text[index];
984
+ if (single) {
985
+ if (char === "'" && text[index + 1] === "'") {
986
+ index += 1;
987
+ continue;
988
+ }
989
+ if (char === "'")
990
+ single = false;
991
+ continue;
992
+ }
993
+ if (quoted) {
994
+ if (char === '"')
995
+ quoted = false;
996
+ continue;
997
+ }
998
+ if (char === "'") {
999
+ single = true;
1000
+ continue;
1001
+ }
1002
+ if (char === '"') {
1003
+ quoted = true;
1004
+ continue;
1005
+ }
1006
+ if (char === "(")
1007
+ depth += 1;
1008
+ else if (char === ")") {
1009
+ depth -= 1;
1010
+ if (depth === 0)
1011
+ return text.slice(open, index + 1);
1012
+ }
1013
+ }
1014
+ return;
1015
+ }
1016
+ function policyExpression(input, table, name) {
1017
+ const expression = expressionOf(input.after, table, name) ?? expressionOf(input.before, table, name);
1018
+ return expression === undefined ? undefined : normalized(expression);
1019
+ }
1020
+ function expressionOf(source, table, name) {
1021
+ for (const object of source.objects) {
1022
+ if (object.kind !== "policy")
1023
+ continue;
1024
+ if (object.identity.parent.name !== table || object.identity.name !== name)
1025
+ continue;
1026
+ return object.definition.expression;
1027
+ }
1028
+ return;
1029
+ }
1030
+ function changesTenantOwner(input, text) {
1031
+ const match = /^alter table\s+(\S+)\s+owner to\b/.exec(text);
1032
+ const table = lastName(match?.[1]);
1033
+ if (table === undefined)
1034
+ return false;
1035
+ return hasPolicy(input.before, table) || hasPolicy(input.after, table);
1036
+ }
1037
+ function hasPolicy(source, table) {
1038
+ return source.objects.some((object) => object.kind === "policy" && object.identity.parent.name === table);
1039
+ }
1040
+ function dataStatement(step) {
805
1041
  const text = normalized(step.sql);
806
- if (/^(?:insert|update|delete|merge|truncate)\b/.test(text))
807
- return true;
808
- return /^with\b/.test(text) && /\b(?:insert|update|delete|merge)\b/.test(text);
1042
+ if (/^(?:do|call)\b/.test(text))
1043
+ return ["the body is not analysed"];
1044
+ if (step.backfill !== undefined)
1045
+ return [];
1046
+ if (/^(?:insert|update|delete|merge|truncate)\b/.test(text)) {
1047
+ return ["changes rows outside backfill()"];
1048
+ }
1049
+ if (/^with\b/.test(text) && /\b(?:insert|update|delete|merge)\b/.test(text)) {
1050
+ return ["changes rows outside backfill()"];
1051
+ }
1052
+ return [];
809
1053
  }
810
1054
  function matches(step, kind, pattern) {
811
1055
  return pattern.test(normalized(step.sql)) || step.kind === kind;
@@ -980,9 +1224,10 @@ function validates(input, kind) {
980
1224
  return true;
981
1225
  return false;
982
1226
  }
1227
+ var TYPE_CHANGE = /^alter table\s+(\S+)\s+alter column\s+(\S+)\s+(?:set data type|type)\b/;
983
1228
  function typeChange(input, mode) {
984
1229
  const text = normalized(input.step.sql);
985
- const changing = /^alter table\s+\S+\s+alter column\s+\S+\s+set data type\b/.test(text) || input.step.kind === "set-column-type";
1230
+ const changing = TYPE_CHANGE.test(text) || input.step.kind === "set-column-type";
986
1231
  if (!changing)
987
1232
  return [];
988
1233
  const compared = columnTypes(input);
@@ -1009,7 +1254,7 @@ function typeChange(input, mode) {
1009
1254
  }
1010
1255
  function columnTypes(input) {
1011
1256
  const text = normalized(input.step.sql);
1012
- const match = /^alter table\s+(\S+)\s+alter column\s+(\S+)\s+set data type\b/.exec(text);
1257
+ const match = TYPE_CHANGE.exec(text);
1013
1258
  const table = lastName(match?.[1]);
1014
1259
  const column = lastName(match?.[2]);
1015
1260
  if (table === undefined || column === undefined)
@@ -1143,15 +1388,15 @@ function lintCatalog(source) {
1143
1388
  return findings;
1144
1389
  }
1145
1390
  function lintMigrationDirectory(directory, pending) {
1146
- if (!existsSync(directory))
1391
+ if (!existsSync2(directory))
1147
1392
  return [];
1148
- const names = readdirSync(directory).filter((file) => file.endsWith(".sql")).sort();
1393
+ const names = readdirSync2(directory).filter((file) => file.endsWith(".sql")).sort();
1149
1394
  let before = catalog([]);
1150
1395
  const findings = [];
1151
1396
  for (const file of names) {
1152
1397
  const id = file.slice(0, -".sql".length);
1153
- const plan = parsePlan(readFileSync(join(directory, file), "utf8"));
1154
- const after = parseCatalog2(readFileSync(join(directory, `${id}.catalog.json`), "utf8"));
1398
+ const plan = readMigrationPlan(join2(directory, file));
1399
+ const after = parseCatalog2(readFileSync2(join2(directory, `${id}.catalog.json`), "utf8"));
1155
1400
  if (pending === undefined || pending.has(id)) {
1156
1401
  for (const item of lintPlan(plan, before, after)) {
1157
1402
  findings.push({ ...item, place: `${id} ${item.place}` });
@@ -1178,34 +1423,76 @@ function lintRefusal(findings) {
1178
1423
  });
1179
1424
  }
1180
1425
  function lintStep(step, number, before, after, existingTables, earlier, later) {
1181
- const hits = [];
1182
- for (const rule of STEP_RULES) {
1183
- for (const reason of rule.check({ step, before, after, existingTables, earlier, later })) {
1184
- hits.push({ code: rule.code, severity: rule.severity, reason });
1426
+ const texts = statementTexts(step);
1427
+ const multi = texts.length > 1;
1428
+ const earlierStatements = earlier.flatMap(contextSteps);
1429
+ const laterStatements = later.flatMap(contextSteps);
1430
+ const perStatement = [];
1431
+ const triggered = new Set;
1432
+ texts.forEach((sql, index) => {
1433
+ const statement = multi ? withoutKind(step, sql) : withSql(step, sql);
1434
+ const hits = [];
1435
+ for (const rule of STEP_RULES) {
1436
+ for (const reason of rule.check({
1437
+ step: statement,
1438
+ before,
1439
+ after,
1440
+ existingTables,
1441
+ earlier: earlierStatements,
1442
+ later: laterStatements
1443
+ })) {
1444
+ const hit = { code: rule.code, severity: rule.severity, reason };
1445
+ hits.push(hit);
1446
+ triggered.add(hit.code);
1447
+ }
1185
1448
  }
1186
- }
1187
- const triggered = new Set(hits.map((item) => item.code));
1449
+ const place = multi ? `step ${String(number)} statement ${String(index + 1)}` : `step ${String(number)}`;
1450
+ perStatement.push({ place, hits });
1451
+ });
1188
1452
  const silenced = new Set;
1189
1453
  const findings = [];
1190
- const place = `step ${String(number)}`;
1454
+ const stepPlace = `step ${String(number)}`;
1191
1455
  for (const allow of step.allows ?? []) {
1192
1456
  if (allow.reason.trim().length === 0 || allow.code.length === 0) {
1193
- findings.push(finding("error", "OKM1510", place, "override needs a reason"));
1457
+ findings.push(finding("error", "OKM1510", stepPlace, "override needs a reason"));
1194
1458
  continue;
1195
1459
  }
1196
1460
  if (!triggered.has(allow.code)) {
1197
- findings.push(finding("error", "OKM1510", place, `override ${allow.code} does not match this statement`));
1461
+ findings.push(finding("error", "OKM1510", stepPlace, `override ${allow.code} does not match this statement`));
1198
1462
  continue;
1199
1463
  }
1200
1464
  silenced.add(allow.code);
1201
1465
  }
1202
- for (const hit of hits) {
1203
- if (silenced.has(hit.code))
1204
- continue;
1205
- findings.push(finding(hit.severity, hit.code, place, hit.reason));
1466
+ for (const item of perStatement) {
1467
+ for (const hit of item.hits) {
1468
+ if (silenced.has(hit.code))
1469
+ continue;
1470
+ findings.push(finding(hit.severity, hit.code, item.place, hit.reason));
1471
+ }
1206
1472
  }
1207
1473
  return findings;
1208
1474
  }
1475
+ function statementTexts(step) {
1476
+ const statements = sqlStatements(step.sql);
1477
+ return statements.length === 0 ? [step.sql] : statements;
1478
+ }
1479
+ function contextSteps(step) {
1480
+ const statements = sqlStatements(step.sql);
1481
+ if (statements.length === 0)
1482
+ return [step];
1483
+ if (statements.length === 1 && statements[0] === step.sql)
1484
+ return [step];
1485
+ return statements.map((sql) => withSql(step, sql));
1486
+ }
1487
+ function withSql(step, sql) {
1488
+ if (sql === step.sql)
1489
+ return step;
1490
+ return { ...step, sql };
1491
+ }
1492
+ function withoutKind(step, sql) {
1493
+ const { kind: _kind, ...rest } = step;
1494
+ return { ...rest, sql };
1495
+ }
1209
1496
  function finding(severity, code, place, reason) {
1210
1497
  const doc = errorDoc(code);
1211
1498
  if (doc === undefined)
@@ -1227,161 +1514,9 @@ function columnPlace(column) {
1227
1514
  return `${column.identity.parent.name}.${column.identity.name}`;
1228
1515
  }
1229
1516
 
1230
- // src/tooling/migrate/policy.ts
1231
- var READ = new Set([
1232
- "plan",
1233
- "status",
1234
- "check",
1235
- "drift",
1236
- "verify",
1237
- "pull",
1238
- "catalog-export",
1239
- "inspect"
1240
- ]);
1241
- var ALLOWED = new Set(["expand", "reference", "provision"]);
1242
- var FORWARD_ONLY = new Set(["drop", "rollback"]);
1243
- function assertTargetPolicy(target, operation, allowProtected = false) {
1244
- if (FORWARD_ONLY.has(operation)) {
1245
- throw new OkmError2("invalid", "There is no down migration.", {
1246
- fix: { summary: "Recovery is a new forward migration." }
1247
- });
1248
- }
1249
- if (!target.protected || allowProtected)
1250
- return;
1251
- if (READ.has(operation) || ALLOWED.has(operation))
1252
- return;
1253
- throw new OkmError2("OKM1850", `${operation} is blocked on protected target ${target.name}.`, {
1254
- kind: "forbidden",
1255
- fix: {
1256
- summary: "Run a read-only command or an expand migration, or pass --allow-protected for this invocation."
1257
- }
1258
- });
1259
- }
1260
- function assertTargetAlias(targets) {
1261
- const groups = new Map;
1262
- for (const target of targets) {
1263
- const key = `${target.host}
1264
- ${target.port}
1265
- ${target.database}`;
1266
- const group = groups.get(key);
1267
- if (group === undefined)
1268
- groups.set(key, [target]);
1269
- else
1270
- group.push(target);
1271
- }
1272
- for (const group of groups.values()) {
1273
- const protectedTarget = group.find((target) => target.protected);
1274
- const open = group.find((target) => !target.protected);
1275
- if (protectedTarget !== undefined && open !== undefined) {
1276
- throw new OkmError2("OKM1852", `${open.name} and ${protectedTarget.name} resolve to the same database and disagree about protection.`, {
1277
- fix: {
1278
- summary: "Give those targets the same protection flag. okm check and okm doctor both report this."
1279
- }
1280
- });
1281
- }
1282
- }
1283
- }
1284
- function assertDirectConnection(url, allowPooler) {
1285
- if (allowPooler)
1286
- return;
1287
- const endpoint = endpointOf(url);
1288
- const parsed = new URL(url);
1289
- const host = endpoint.host;
1290
- if (host.includes("pooler") || host.includes("pgbouncer") || host.includes("-pooler") || endpoint.port === "6543" || parsed.searchParams.get("pgbouncer") === "true") {
1291
- throw new OkmError2("invalid", `Refusing pooler host ${host}. Apply needs a direct connection.`, { fix: { summary: "Use a direct connection, or pass --allow-pooler." } });
1292
- }
1293
- }
1294
- function listTargets(config) {
1295
- if (config.database !== undefined && config.targets !== undefined) {
1296
- throw new OkmError2("invalid", "Set database or targets, not both.", {
1297
- fix: { summary: "database is the one target named default. Use targets for more than one." }
1298
- });
1299
- }
1300
- const entries = [];
1301
- if (config.database !== undefined)
1302
- entries.push(["default", config.database]);
1303
- if (config.targets !== undefined) {
1304
- for (const name of Object.keys(config.targets).sort()) {
1305
- const input = config.targets[name];
1306
- if (input !== undefined)
1307
- entries.push([name, input]);
1308
- }
1309
- }
1310
- return entries.map(([name, input]) => resolveTarget(name, input));
1311
- }
1312
- function selectTarget(config, name) {
1313
- const targets = listTargets(config);
1314
- if (targets.length === 0) {
1315
- throw new OkmError2("OKM1845", "No target is configured.", {
1316
- fix: { summary: "Set database or targets in okmodel.config.ts." }
1317
- });
1318
- }
1319
- if (name === undefined) {
1320
- if (targets.length === 1) {
1321
- const only = targets[0];
1322
- if (only !== undefined)
1323
- return only;
1324
- }
1325
- throw new OkmError2("OKM1853", "Several targets are configured and --target was not passed.", {
1326
- fix: { summary: "Pass --target <name>. The command does not guess." }
1327
- });
1328
- }
1329
- const found = targets.find((target) => target.name === name);
1330
- if (found === undefined) {
1331
- throw new OkmError2("OKM1845", `Target ${name} is not configured.`, {
1332
- fix: { summary: "Pass a name from targets, or database for the target named default." }
1333
- });
1334
- }
1335
- return found;
1336
- }
1337
- function resolveTarget(name, input) {
1338
- if (typeof input !== "string" && isReplicaTarget(input)) {
1339
- throw new OkmError2("OKM1845", `Target ${name} is a replica. Migrations run on the primary only.`, {
1340
- fix: {
1341
- summary: "Set database or targets to the primary URL. A replica is not a migration target."
1342
- }
1343
- });
1344
- }
1345
- const url = typeof input === "string" ? input : input.url;
1346
- const protectedTarget = typeof input === "string" ? false : input.protected === true;
1347
- if (url === undefined || url.length === 0) {
1348
- throw new OkmError2("OKM1845", `Target ${name} has no url.`, {
1349
- fix: { summary: "Set the target url. A target is not inferred from the environment." }
1350
- });
1351
- }
1352
- const endpoint = endpointOf(url);
1353
- return {
1354
- name,
1355
- url,
1356
- protected: protectedTarget,
1357
- host: endpoint.host,
1358
- port: endpoint.port,
1359
- database: endpoint.database
1360
- };
1361
- }
1362
- function isReplicaTarget(input) {
1363
- return "primary" in input || "replicas" in input || "weight" in input || "pool" in input;
1364
- }
1365
- function endpointOf(url) {
1366
- let parsed;
1367
- try {
1368
- parsed = new URL(url);
1369
- } catch {
1370
- throw new OkmError2("OKM1845", "The target URL is not a Postgres URL.", {
1371
- fix: { summary: "Pass a postgres:// URL for the target." }
1372
- });
1373
- }
1374
- const database = decodeURIComponent(parsed.pathname.replace(/^\//, ""));
1375
- return {
1376
- host: parsed.hostname.toLowerCase(),
1377
- port: parsed.port.length > 0 ? parsed.port : "5432",
1378
- database
1379
- };
1380
- }
1381
-
1382
1517
  // src/tooling/migrate/project.ts
1383
- import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
1384
- import { basename, join as join2 } from "node:path";
1518
+ import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync, writeFileSync } from "node:fs";
1519
+ import { basename, dirname, join as join3 } from "node:path";
1385
1520
  import { pathToFileURL } from "node:url";
1386
1521
 
1387
1522
  // src/dialects/pg/declarations.ts
@@ -1423,7 +1558,7 @@ function sqlIdentifier(name, casing) {
1423
1558
  }
1424
1559
 
1425
1560
  // src/dialects/pg/emit.ts
1426
- function emitRowTypes(source) {
1561
+ function emitRowTypes(source, jsonTypes) {
1427
1562
  const lines = [
1428
1563
  "/**",
1429
1564
  " * Row types emitted from the schema.",
@@ -1441,13 +1576,13 @@ function emitRowTypes(source) {
1441
1576
  inserts.push(` readonly ${key}: ${name}Insert;`);
1442
1577
  updates.push(` readonly ${key}: ${name}Update;`);
1443
1578
  lines.push(`export interface ${name} {`);
1444
- lines.push(fields(item, "row", source));
1579
+ lines.push(fields(item, "row", source, jsonTypes));
1445
1580
  lines.push("}", "");
1446
1581
  lines.push(`export interface ${name}Insert {`);
1447
- lines.push(fields(item, "insert", source));
1582
+ lines.push(fields(item, "insert", source, jsonTypes));
1448
1583
  lines.push("}", "");
1449
1584
  lines.push(`export interface ${name}Update {`);
1450
- lines.push(fields(item, "update", source));
1585
+ lines.push(fields(item, "update", source, jsonTypes));
1451
1586
  lines.push("}", "");
1452
1587
  }
1453
1588
  lines.push("export interface Rows {");
@@ -1462,14 +1597,92 @@ function emitRowTypes(source) {
1462
1597
  lines.push(updates.join(`
1463
1598
  `));
1464
1599
  lines.push("}");
1465
- return `${lines.join(`
1600
+ const body = `${lines.join(`
1466
1601
  `)}
1467
1602
  `;
1603
+ const aliases = [];
1604
+ if (body.includes("jsonb<"))
1605
+ aliases.push("export type jsonb<T> = T;");
1606
+ if (body.includes("json<"))
1607
+ aliases.push("export type json<T> = T;");
1608
+ if (aliases.length === 0)
1609
+ return body;
1610
+ return `${aliases.join(`
1611
+ `)}
1612
+
1613
+ ${body}`;
1468
1614
  }
1469
1615
  function propertyName(name) {
1470
1616
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : JSON.stringify(name);
1471
1617
  }
1472
- function fields(item, kind, source) {
1618
+ function jsonColumnTypes(source) {
1619
+ const text = source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
1620
+ const found = new Map;
1621
+ const pattern = /(?:\bt\s*\.\s*)?(jsonb|json)\s*</g;
1622
+ for (const match of text.matchAll(pattern)) {
1623
+ const open = (match.index ?? 0) + match[0].length - 1;
1624
+ const argument = readGeneric(text, open);
1625
+ const field = propertyBefore(text, match.index ?? 0);
1626
+ const table = tableBefore(text, match.index ?? 0);
1627
+ if (argument === undefined || field === undefined || table === undefined)
1628
+ continue;
1629
+ found.set(`${table}.${field}`, argument);
1630
+ }
1631
+ return found;
1632
+ }
1633
+ function readGeneric(source, open) {
1634
+ if (source[open] !== "<")
1635
+ return;
1636
+ let depth = 0;
1637
+ let quote;
1638
+ for (let index = open;index < source.length; index += 1) {
1639
+ const char = source[index];
1640
+ if (char === undefined)
1641
+ return;
1642
+ if (quote !== undefined) {
1643
+ if (char === "\\") {
1644
+ index += 1;
1645
+ continue;
1646
+ }
1647
+ if (char === quote)
1648
+ quote = undefined;
1649
+ continue;
1650
+ }
1651
+ if (char === "'" || char === '"' || char === "`") {
1652
+ quote = char;
1653
+ continue;
1654
+ }
1655
+ if (char === "<")
1656
+ depth += 1;
1657
+ else if (char === ">") {
1658
+ depth -= 1;
1659
+ if (depth === 0)
1660
+ return source.slice(open + 1, index).trim().replace(/\s+/g, " ");
1661
+ }
1662
+ }
1663
+ return;
1664
+ }
1665
+ function propertyBefore(source, index) {
1666
+ let cursor = index;
1667
+ while (cursor > 0 && /\s/.test(source[cursor - 1] ?? ""))
1668
+ cursor -= 1;
1669
+ if (source[cursor - 1] !== ":")
1670
+ return;
1671
+ cursor -= 1;
1672
+ while (cursor > 0 && /\s/.test(source[cursor - 1] ?? ""))
1673
+ cursor -= 1;
1674
+ const end = cursor;
1675
+ while (cursor > 0 && /[\w$]/.test(source[cursor - 1] ?? ""))
1676
+ cursor -= 1;
1677
+ const name = source.slice(cursor, end);
1678
+ return /^[$A-Za-z_][\w$]*$/.test(name) ? name : undefined;
1679
+ }
1680
+ function tableBefore(source, index) {
1681
+ const head = source.slice(0, index);
1682
+ const tables = [...head.matchAll(/table\(\s*["']([^"']+)["']/g)];
1683
+ return tables.at(-1)?.[1];
1684
+ }
1685
+ function fields(item, kind, source, jsonTypes) {
1473
1686
  const printed = [];
1474
1687
  const options = item.options;
1475
1688
  const entries = Object.entries(item.columns);
@@ -1487,7 +1700,7 @@ function fields(item, kind, source) {
1487
1700
  const state = builder.state;
1488
1701
  if (kind === "row") {
1489
1702
  if (!state.hidden) {
1490
- printed.push(` readonly ${field}: ${valueType(state, "required")};`);
1703
+ printed.push(` readonly ${field}: ${valueType(state, jsonTypes?.get(`${item.name}.${field}`))};`);
1491
1704
  }
1492
1705
  continue;
1493
1706
  }
@@ -1496,7 +1709,8 @@ function fields(item, kind, source) {
1496
1709
  continue;
1497
1710
  }
1498
1711
  const mode = kind === "update" ? "optional" : written;
1499
- printed.push(` readonly ${field}: ${valueType(state, mode)};`);
1712
+ const label = valueType(state, jsonTypes?.get(`${item.name}.${field}`));
1713
+ printed.push(mode === "optional" ? ` readonly ${field}?: ${label};` : ` readonly ${field}: ${label};`);
1500
1714
  }
1501
1715
  return printed.join(`
1502
1716
  `);
@@ -1515,8 +1729,8 @@ function updateMode(state) {
1515
1729
  return "omit";
1516
1730
  return "optional";
1517
1731
  }
1518
- function valueType(state, mode) {
1519
- let label = scalarLabel(state);
1732
+ function valueType(state, jsonArg) {
1733
+ let label = scalarLabel(state, jsonArg);
1520
1734
  if (state.dims > 0) {
1521
1735
  for (let rank = 0;rank < state.dims; rank += 1) {
1522
1736
  label = `readonly (${label})[]`;
@@ -1525,12 +1739,9 @@ function valueType(state, mode) {
1525
1739
  if (state.nullable) {
1526
1740
  label += " | null";
1527
1741
  }
1528
- if (mode === "optional") {
1529
- label += " | undefined";
1530
- }
1531
1742
  return label;
1532
1743
  }
1533
- function scalarLabel(state) {
1744
+ function scalarLabel(state, jsonArg) {
1534
1745
  if (state.picklist !== undefined) {
1535
1746
  return state.picklist.values.map((value) => JSON.stringify(value)).join(" | ");
1536
1747
  }
@@ -1563,8 +1774,9 @@ function scalarLabel(state) {
1563
1774
  case "bytea":
1564
1775
  return "Uint8Array";
1565
1776
  case "json":
1777
+ return `json<${jsonArg ?? "unknown"}>`;
1566
1778
  case "jsonb":
1567
- return "unknown";
1779
+ return `jsonb<${jsonArg ?? "unknown"}>`;
1568
1780
  case "timestamptz":
1569
1781
  return "Temporal.Instant";
1570
1782
  case "timestamp":
@@ -1608,17 +1820,34 @@ function baseName(dataType) {
1608
1820
  }
1609
1821
 
1610
1822
  // src/dialects/pg/role/index.ts
1823
+ class RoleDeclarationError extends OkmError2 {
1824
+ reason;
1825
+ constructor(reason, message) {
1826
+ super("invalid", message, {
1827
+ kind: "invalid",
1828
+ fix: {
1829
+ summary: reason === "reporting_write" ? "Grant select, or use the application role for writes." : "Give the reporting role its own name."
1830
+ }
1831
+ });
1832
+ this.name = "OkmError";
1833
+ this.reason = reason;
1834
+ }
1835
+ }
1611
1836
  var PROVENANCE = { origin: "file", name: "roles" };
1612
1837
  var RELATION = ["SELECT", "INSERT", "UPDATE", "DELETE"];
1613
- var READ2 = ["SELECT"];
1838
+ var READ = ["SELECT"];
1614
1839
  var ROUTINE = ["EXECUTE"];
1615
1840
  var SEQUENCE = ["USAGE", "SELECT"];
1616
- function attachRoles(source, input) {
1617
- return catalog([...source.objects, ...contributeRoles(input, source.objects)]);
1841
+ function attachRoles(source, input, exposure) {
1842
+ return catalog([...source.objects, ...contributeRoles(input, source.objects, exposure)]);
1618
1843
  }
1619
- function contributeRoles(input, built) {
1844
+ function contributeRoles(input, built, exposure) {
1620
1845
  const migration = requireName(input.migration, "migration");
1621
1846
  const app = requireName(input.app, "app");
1847
+ const reporting = input.reporting === undefined ? undefined : requireName(input.reporting, "reporting");
1848
+ if (reporting !== undefined && (reporting === app || reporting === migration)) {
1849
+ throw new RoleDeclarationError("reporting_distinct", `Role ${reporting} cannot be the reporting role and also migration or app.`);
1850
+ }
1622
1851
  const managed = new Map;
1623
1852
  for (const role of input.managed ?? []) {
1624
1853
  const name = requireName(role.name, "managed role");
@@ -1626,6 +1855,10 @@ function contributeRoles(input, built) {
1626
1855
  }
1627
1856
  const objects = [];
1628
1857
  const names = new Set([migration, app, ...managed.keys()]);
1858
+ if (reporting !== undefined)
1859
+ names.add(reporting);
1860
+ for (const grant of input.columns ?? [])
1861
+ names.add(requireName(grant.role, "column grant role"));
1629
1862
  for (const name of [...names].sort()) {
1630
1863
  const declaration = managed.get(name);
1631
1864
  objects.push(roleObject({
@@ -1640,8 +1873,10 @@ function contributeRoles(input, built) {
1640
1873
  }
1641
1874
  const roleIdentity = (name) => ({ kind: "role", name });
1642
1875
  const namespaces = new Set;
1876
+ const exposed = indexExposure(exposure);
1877
+ const columnGrants = new Map;
1643
1878
  for (const object of built) {
1644
- if (object.owner === "ignored")
1879
+ if (object.owner === "ignored" || object.owner === "external")
1645
1880
  continue;
1646
1881
  const ref = grantRef(object);
1647
1882
  if (ref === undefined)
@@ -1659,6 +1894,7 @@ function contributeRoles(input, built) {
1659
1894
  }));
1660
1895
  }
1661
1896
  }
1897
+ const relations = built.filter((object) => object.owner !== "ignored" && object.owner !== "external" && (object.kind === "table" || object.kind === "view" || object.kind === "materializedView"));
1662
1898
  const schemas = namespaces.size === 0 ? ["public"] : [...namespaces].sort();
1663
1899
  for (const name of schemas) {
1664
1900
  const namespace = staticNamespace2(name);
@@ -1679,6 +1915,45 @@ function contributeRoles(input, built) {
1679
1915
  }));
1680
1916
  }
1681
1917
  }
1918
+ if (reporting !== undefined) {
1919
+ objects.push(grantObject({
1920
+ role: reporting,
1921
+ object: { kind: "namespace", namespace, name },
1922
+ privilege: "USAGE",
1923
+ provenance: PROVENANCE,
1924
+ dependencies: [roleIdentity(reporting)]
1925
+ }));
1926
+ objects.push(defaultPrivilegeObject({
1927
+ forRole: migration,
1928
+ namespace,
1929
+ objectKind: "table",
1930
+ grantee: reporting,
1931
+ privilege: "SELECT",
1932
+ provenance: PROVENANCE,
1933
+ dependencies: [roleIdentity(migration), roleIdentity(reporting)]
1934
+ }));
1935
+ }
1936
+ }
1937
+ for (const grant of input.columns ?? []) {
1938
+ noteColumnGrant(columnGrants, grant, built, exposed, reporting);
1939
+ }
1940
+ if (reporting !== undefined) {
1941
+ for (const object of relations) {
1942
+ const ref = grantRef(object);
1943
+ if (ref === undefined)
1944
+ continue;
1945
+ noteReportingSelect(columnGrants, objects, reporting, object, ref, exposed, built);
1946
+ }
1947
+ }
1948
+ for (const pending of [...columnGrants.values()].sort((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0)) {
1949
+ const target = pending.target;
1950
+ objects.push(grantObject({
1951
+ role: pending.role,
1952
+ object: pending.object,
1953
+ privilege: columnPrivilege(pending.privilege, [...pending.columns].sort()),
1954
+ provenance: PROVENANCE,
1955
+ dependencies: [roleIdentity(pending.role), target.identity]
1956
+ }));
1682
1957
  }
1683
1958
  return objects;
1684
1959
  }
@@ -1712,7 +1987,7 @@ function privilegesFor(kind) {
1712
1987
  if (kind === "sequence")
1713
1988
  return SEQUENCE;
1714
1989
  if (kind === "materializedView")
1715
- return READ2;
1990
+ return READ;
1716
1991
  if (kind === "namespace")
1717
1992
  return [];
1718
1993
  return RELATION;
@@ -1720,6 +1995,137 @@ function privilegesFor(kind) {
1720
1995
  function objectIdentity(object) {
1721
1996
  return object.identity;
1722
1997
  }
1998
+ function indexExposure(exposure) {
1999
+ const indexed = new Map;
2000
+ for (const table of exposure ?? [])
2001
+ indexed.set(table.table, table);
2002
+ return indexed;
2003
+ }
2004
+ function noteReportingSelect(pending, objects, role, target, ref, exposed, built) {
2005
+ const key = columnKey(role, ref, "SELECT");
2006
+ const declared = pending.get(key);
2007
+ if (ref.kind === "view" || ref.kind === "materializedView") {
2008
+ if (declared !== undefined)
2009
+ return;
2010
+ objects.push(selectGrant(role, ref, target));
2011
+ return;
2012
+ }
2013
+ const visible = visibleColumns(ref.name, exposed, built);
2014
+ const hidden = hiddenColumns(ref.name, exposed);
2015
+ if (hidden.size === 0) {
2016
+ pending.delete(key);
2017
+ objects.push(selectGrant(role, ref, target));
2018
+ return;
2019
+ }
2020
+ const columns = new Set(visible);
2021
+ for (const name of declared?.columns ?? [])
2022
+ columns.add(name);
2023
+ if (columns.size === 0) {
2024
+ pending.delete(key);
2025
+ return;
2026
+ }
2027
+ const next = declared ?? openColumnGrant(role, ref, "SELECT", target);
2028
+ next.columns.clear();
2029
+ for (const name of columns)
2030
+ next.columns.add(name);
2031
+ pending.set(key, next);
2032
+ }
2033
+ function selectGrant(role, ref, target) {
2034
+ return grantObject({
2035
+ role,
2036
+ object: ref,
2037
+ privilege: "SELECT",
2038
+ provenance: PROVENANCE,
2039
+ dependencies: [{ kind: "role", name: role }, target.identity]
2040
+ });
2041
+ }
2042
+ function noteColumnGrant(pending, grant, built, exposed, reporting) {
2043
+ const role = requireName(grant.role, "column grant role");
2044
+ const target = findRelation(built, grant.on);
2045
+ if (target.owner === "ignored") {
2046
+ throw new OkmError2("invalid", `Column grant on ${grant.on} targets an ignored table.`, {
2047
+ fix: { summary: "Remove the grant, or mark the table external." }
2048
+ });
2049
+ }
2050
+ const ref = grantRef(target);
2051
+ if (ref === undefined) {
2052
+ throw new OkmError2("OKM1020", `Column grant on ${grant.on} is not a table or a view.`, {
2053
+ fix: { summary: "Name a table, view, or materialized view in the schema." }
2054
+ });
2055
+ }
2056
+ for (const privilege of ["select", "update"]) {
2057
+ const names = grant[privilege];
2058
+ if (names === undefined || names.length === 0)
2059
+ continue;
2060
+ if (role === reporting && privilege === "update") {
2061
+ throw new RoleDeclarationError("reporting_write", `Role ${role} is the reporting role and cannot be granted update.`);
2062
+ }
2063
+ const upper = privilege === "select" ? "SELECT" : "UPDATE";
2064
+ const resolved = names.map((name) => resolveColumn(target, built, ref.name, name, exposed));
2065
+ const current = pending.get(columnKey(role, ref, upper)) ?? openColumnGrant(role, ref, upper, target);
2066
+ for (const name of resolved)
2067
+ current.columns.add(name);
2068
+ pending.set(current.key, current);
2069
+ }
2070
+ }
2071
+ function openColumnGrant(role, object, privilege, target) {
2072
+ return {
2073
+ key: columnKey(role, object, privilege),
2074
+ role,
2075
+ privilege,
2076
+ object,
2077
+ target,
2078
+ columns: new Set
2079
+ };
2080
+ }
2081
+ function columnKey(role, object, privilege) {
2082
+ const namespace = object.namespace.form === "static" ? object.namespace.name : object.namespace.pattern;
2083
+ return `${role}\x00${object.kind}\x00${namespace}\x00${object.name}\x00${privilege}`;
2084
+ }
2085
+ function findRelation(built, name) {
2086
+ const found = built.filter((object) => (object.kind === "table" || object.kind === "view" || object.kind === "materializedView") && object.identity.name === name);
2087
+ const target = found[0];
2088
+ if (target === undefined) {
2089
+ throw new OkmError2("OKM1020", `Column grant names ${name}, which is not in the schema.`, {
2090
+ fix: { summary: "Use the SQL name of a table, view, or materialized view." }
2091
+ });
2092
+ }
2093
+ if (found.length > 1) {
2094
+ throw new OkmError2("OKM1020", `Column grant on ${name} matches more than one object.`, {
2095
+ fix: { summary: "Rename one of the objects so the grant names one." }
2096
+ });
2097
+ }
2098
+ return target;
2099
+ }
2100
+ function resolveColumn(target, built, table, name, exposed) {
2101
+ const known = exposed.get(table);
2102
+ const fromField = known?.columns.find((column) => column.field === name || column.sql === name);
2103
+ if (fromField !== undefined)
2104
+ return fromField.sql;
2105
+ const sqlNames = columnNames(target, built, table);
2106
+ if (sqlNames.includes(name))
2107
+ return name;
2108
+ throw new OkmError2("OKM1020", `Column ${table}.${name} is not in the schema.`, {
2109
+ fix: { summary: "Name a column of that table or view." }
2110
+ });
2111
+ }
2112
+ function visibleColumns(table, exposed, built) {
2113
+ const known = exposed.get(table);
2114
+ if (known !== undefined) {
2115
+ return known.columns.filter((column) => !column.hidden).map((column) => column.sql);
2116
+ }
2117
+ return built.flatMap((object) => object.kind === "column" && object.identity.parent.name === table ? [object.identity.name] : []);
2118
+ }
2119
+ function hiddenColumns(table, exposed) {
2120
+ const known = exposed.get(table);
2121
+ return new Set((known?.columns ?? []).filter((column) => column.hidden).map((column) => column.sql));
2122
+ }
2123
+ function columnNames(target, built, table) {
2124
+ if (target.kind === "view" || target.kind === "materializedView") {
2125
+ return target.definition.columns.map((column) => column.name);
2126
+ }
2127
+ return built.flatMap((object) => object.kind === "column" && object.identity.parent.name === table ? [object.identity.name] : []);
2128
+ }
1723
2129
 
1724
2130
  // src/tooling/migrate/estimate.ts
1725
2131
  var LARGE_TABLE_ROWS = 1e6;
@@ -1759,31 +2165,49 @@ async function readRowEstimates(url, schema, tables) {
1759
2165
  return map;
1760
2166
  } catch (error) {
1761
2167
  if (began)
1762
- await connection.execute("rollback").catch(() => {
1763
- return;
1764
- });
1765
- throw error;
2168
+ await finishQuiet(() => connection.execute("rollback"));
2169
+ throw sessionFailure(error) ?? error;
1766
2170
  } finally {
1767
- await connection.release();
2171
+ await finishQuiet(() => connection.release());
1768
2172
  }
1769
2173
  } finally {
1770
- await pool.close();
2174
+ await finishQuiet(() => pool.close());
1771
2175
  }
1772
2176
  }
1773
- function annotateLock(step, estimates) {
2177
+ function annotateLock(step, estimates, aliases = new Map) {
1774
2178
  const pieces = lockPieces(step);
1775
2179
  const text = withBatches(step, pieces.map((piece) => {
1776
2180
  if (piece.table === undefined)
1777
2181
  return piece.mode;
1778
- return `${piece.mode} on ${piece.table}, ${estimatePhrase(estimates.get(piece.table))}`;
1779
- }).join("; "), estimates);
2182
+ return `${piece.mode} on ${piece.table}, ${estimatePhrase(lookupEstimate(piece.table, estimates, aliases))}`;
2183
+ }).join("; "), estimates, aliases);
1780
2184
  if (step.safeRewrite === true)
1781
2185
  return `${text}; safe rewrite applied`;
1782
- if (exclusiveOverLarge(pieces, estimates)) {
2186
+ if (exclusiveOverLarge(pieces, estimates, aliases)) {
1783
2187
  return `${text}; note: more than ${String(LARGE_TABLE_ROWS)} estimated rows`;
1784
2188
  }
1785
2189
  return text;
1786
2190
  }
2191
+ function renameAliases(steps) {
2192
+ const map = new Map;
2193
+ for (const step of steps) {
2194
+ if (step.kind !== "rename-table")
2195
+ continue;
2196
+ const match = /^alter table "(?:(?:[^"]|"")*)"\."((?:[^"]|"")*)" rename to "((?:[^"]|"")*)"/.exec(step.sql);
2197
+ const from = match?.[1]?.replaceAll('""', '"');
2198
+ const to = match?.[2]?.replaceAll('""', '"');
2199
+ if (from !== undefined && to !== undefined)
2200
+ map.set(to, from);
2201
+ }
2202
+ return map;
2203
+ }
2204
+ function lookupEstimate(table, estimates, aliases) {
2205
+ const direct = estimates.get(table);
2206
+ if (direct !== undefined)
2207
+ return direct;
2208
+ const previous = aliases.get(table);
2209
+ return previous === undefined ? undefined : estimates.get(previous);
2210
+ }
1787
2211
  function aboutRows(count) {
1788
2212
  const rounded = Math.max(0, Math.round(count));
1789
2213
  if (rounded < 1000)
@@ -1803,12 +2227,12 @@ function aboutRows(count) {
1803
2227
  }
1804
2228
  return `about ${String(rounded)} rows`;
1805
2229
  }
1806
- function withBatches(step, text, estimates) {
2230
+ function withBatches(step, text, estimates, aliases = new Map) {
1807
2231
  const batch = step.backfill?.batch;
1808
2232
  if (batch === undefined)
1809
2233
  return text;
1810
2234
  const name = step.tables?.[0];
1811
- const estimate = name === undefined ? undefined : estimates.get(name);
2235
+ const estimate = name === undefined ? undefined : lookupEstimate(name, estimates, aliases);
1812
2236
  if (estimate === undefined || estimate.kind !== "rows")
1813
2237
  return text;
1814
2238
  return `${text}, about ${String(aboutBatchCount(estimate.reltuples, batch))} batches`;
@@ -1870,11 +2294,11 @@ function lockPieces(step) {
1870
2294
  }
1871
2295
  return pieces;
1872
2296
  }
1873
- function exclusiveOverLarge(pieces, estimates) {
2297
+ function exclusiveOverLarge(pieces, estimates, aliases = new Map) {
1874
2298
  for (const piece of pieces) {
1875
2299
  if (piece.mode !== "ACCESS EXCLUSIVE" || piece.table === undefined)
1876
2300
  continue;
1877
- const estimate = estimates.get(piece.table);
2301
+ const estimate = lookupEstimate(piece.table, estimates, aliases);
1878
2302
  if (estimate === undefined || estimate.kind !== "rows")
1879
2303
  continue;
1880
2304
  if (estimate.reltuples > LARGE_TABLE_ROWS)
@@ -1904,15 +2328,17 @@ async function buildProject(cwd) {
1904
2328
  }
1905
2329
  async function generateProject(cwd, name, flags) {
1906
2330
  const prepared = await prepare(cwd, name, flags);
2331
+ if (name === "init")
2332
+ writeGitAttributes(cwd);
1907
2333
  writeArtifact(cwd, prepared.config, prepared.built);
1908
2334
  if (prepared.plan.steps.length === 0)
1909
2335
  return;
1910
- const directory = join2(cwd, prepared.config.migrations ?? "migrations");
2336
+ const directory = projectPath(cwd, prepared.config.migrations ?? "migrations");
1911
2337
  mkdirSync(directory, { recursive: true });
1912
2338
  const base = `${nextNumber(directory)}_${safeName(name)}`;
1913
- const sqlPath = join2(directory, `${base}.sql`);
2339
+ const sqlPath = join3(directory, `${base}.sql`);
1914
2340
  writeFileSync(sqlPath, formatPlan(prepared.plan));
1915
- writeFileSync(join2(directory, `${base}.catalog.json`), serializeCatalog2(prepared.built.catalog));
2341
+ writeFileSync(join3(directory, `${base}.catalog.json`), serializeCatalog2(prepared.built.catalog));
1916
2342
  return { path: sqlPath, findings: lintPrepared(prepared) };
1917
2343
  }
1918
2344
  async function planProject(cwd, name, flags, invoke) {
@@ -1935,9 +2361,9 @@ async function checkProject(cwd, flags) {
1935
2361
  const targets = listTargets(opened.config);
1936
2362
  assertTargetAlias(targets);
1937
2363
  if (opened.config.tables !== undefined) {
1938
- const directory = join2(cwd, opened.config.tables);
1939
- const files = existsSync2(directory) ? readdirSync2(directory) : [];
1940
- const source = readFileSync2(join2(cwd, opened.config.schema), "utf8");
2364
+ const directory = projectPath(cwd, opened.config.tables);
2365
+ const files = existsSync3(directory) ? readdirSync3(directory) : [];
2366
+ const source = readFileSync3(projectPath(cwd, opened.config.schema), "utf8");
1941
2367
  const missing = unlistedTableFiles(source, files);
1942
2368
  if (missing.length > 0) {
1943
2369
  throw new OkmError2("OKM1024", `Table file not in schema: ${missing.join(", ")}.`, {
@@ -1956,8 +2382,9 @@ async function checkProject(cwd, flags) {
1956
2382
  const target = selectTarget(opened.config, named);
1957
2383
  assertTargetPolicy(target, "check", flags?.allowProtected ?? false);
1958
2384
  assertTargetPolicy(target, "drift", flags?.allowProtected ?? false);
1959
- const { assertAuthorDrift } = await import("./ykv7wh5b.js");
1960
- await assertAuthorDrift(target.url, opened.built.catalog, opened.config.roles);
2385
+ const { assertAuthorDrift } = await import("./1f0xa3cb.js");
2386
+ const allowPooler = flags?.allowPooler === true || opened.config.allowPooler === true;
2387
+ await assertAuthorDrift(target.url, opened.built.catalog, opened.config.roles, allowPooler);
1961
2388
  return findings;
1962
2389
  }
1963
2390
  function unlistedTableFiles(schemaSource, fileNames) {
@@ -2016,7 +2443,8 @@ async function displayPlan(config, plan, invoke) {
2016
2443
  } catch {
2017
2444
  return offline;
2018
2445
  }
2019
- return formatPlan(plan, (step) => annotateLock(step, estimates));
2446
+ const aliases = renameAliases(plan.steps);
2447
+ return formatPlan(plan, (step) => annotateLock(step, estimates, aliases));
2020
2448
  }
2021
2449
  function lintOpened(opened) {
2022
2450
  const plan = planMigration({
@@ -2035,7 +2463,7 @@ async function openProject(cwd) {
2035
2463
  const config = await loadConfig(cwd);
2036
2464
  const built = withRoles(await loadSchema(cwd, config.schema), config);
2037
2465
  const declarations = schemaDeclarations(built);
2038
- const previous = readPrevious(join2(cwd, config.migrations ?? "migrations"));
2466
+ const previous = readPrevious(projectPath(cwd, config.migrations ?? "migrations"));
2039
2467
  const stale = staleRenames(previous?.catalog, declarations.renames);
2040
2468
  if (stale.length > 0) {
2041
2469
  throw new OkmError2("OKM1020", stale.join(" "), {
@@ -2050,25 +2478,108 @@ async function openProject(cwd) {
2050
2478
  };
2051
2479
  }
2052
2480
  function withRoles(built, config) {
2053
- if (config.roles === undefined)
2481
+ const marks = ownershipMarks(built);
2482
+ const stamped = marks.size === 0 ? built.catalog : stampOwnership(built.catalog, marks);
2483
+ const roles = config.roles;
2484
+ const catalog = roles === undefined ? stamped : attachRoles(stamped, roles, exposureFor(built, roles));
2485
+ if (catalog === built.catalog)
2054
2486
  return built;
2055
- return { ...built, catalog: attachRoles(built.catalog, config.roles) };
2487
+ return { ...built, catalog };
2488
+ }
2489
+ function ownershipMarks(built) {
2490
+ const marks = new Map;
2491
+ const models = built.model;
2492
+ for (const table of built.tables) {
2493
+ const owner = tableOwner(table);
2494
+ if (owner === undefined)
2495
+ continue;
2496
+ const model = models[table.name];
2497
+ if (model === undefined)
2498
+ continue;
2499
+ marks.set(model.sql, owner);
2500
+ }
2501
+ return marks;
2502
+ }
2503
+ function exposureFor(built, roles) {
2504
+ if (roles.reporting === undefined && (roles.columns === undefined || roles.columns.length === 0)) {
2505
+ return;
2506
+ }
2507
+ return Object.values(built.model).map((model) => ({
2508
+ table: model.sql,
2509
+ columns: model.columns.map((column) => ({
2510
+ field: column.field,
2511
+ sql: column.sql,
2512
+ hidden: column.hidden === true || column.hidden === "filter"
2513
+ }))
2514
+ }));
2515
+ }
2516
+ var GIT_ATTRIBUTES = [
2517
+ "migrations/*.sql text eol=lf",
2518
+ "*.catalog.json text eol=lf",
2519
+ ".okm/** text eol=lf",
2520
+ ""
2521
+ ].join(`
2522
+ `);
2523
+ function writeGitAttributes(cwd) {
2524
+ const path = projectPath(cwd, ".gitattributes");
2525
+ if (existsSync3(path))
2526
+ return;
2527
+ writeFileSync(path, GIT_ATTRIBUTES);
2528
+ }
2529
+ function typesText(cwd, spec, built) {
2530
+ return `${emitRowTypes(built, jsonColumnTypes(readSchemaSource(cwd, spec)))}${referenceAugmentation(built)}`;
2531
+ }
2532
+ function readSchemaSource(cwd, spec) {
2533
+ const parts = [];
2534
+ const seen = new Set;
2535
+ const visit = (file) => {
2536
+ if (seen.has(file) || !existsSync3(file))
2537
+ return;
2538
+ let stat;
2539
+ try {
2540
+ stat = statSync(file);
2541
+ } catch {
2542
+ return;
2543
+ }
2544
+ if (!stat.isFile())
2545
+ return;
2546
+ seen.add(file);
2547
+ const text = readFileSync3(file, "utf8");
2548
+ parts.push(text);
2549
+ for (const match of text.matchAll(/from\s+["'](\.[^"']+)["']/g)) {
2550
+ const rel = match[1];
2551
+ if (rel === undefined)
2552
+ continue;
2553
+ const base = join3(dirname(file), rel);
2554
+ const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}.mts`, join3(base, "index.ts")];
2555
+ for (const candidate of candidates) {
2556
+ if (existsSync3(candidate)) {
2557
+ visit(candidate);
2558
+ break;
2559
+ }
2560
+ }
2561
+ }
2562
+ };
2563
+ visit(projectPath(cwd, spec));
2564
+ return parts.join(`
2565
+ `);
2056
2566
  }
2057
2567
  function writeArtifact(cwd, config, built) {
2058
- const directory = join2(cwd, config.out ?? ".okm");
2568
+ const directory = projectPath(cwd, config.out ?? ".okm");
2059
2569
  mkdirSync(directory, { recursive: true });
2060
2570
  const startup = startupCatalog2(built.catalog);
2061
- writeFileSync(join2(directory, "catalog.json"), serializeCatalog2(startup));
2062
- writeFileSync(join2(directory, "catalog.hash"), `${catalogHash2(startup)}
2571
+ const text = catalogFileText2(serializeCatalog2(startup));
2572
+ writeFileSync(join3(directory, "catalog.json"), text);
2573
+ writeFileSync(join3(directory, "catalog.hash"), `${sha2562(text)}
2063
2574
  `);
2064
- writeFileSync(join2(directory, "types.d.ts"), `${emitRowTypes(built)}${referenceAugmentation(built)}`);
2575
+ writeFileSync(join3(directory, "types.d.ts"), typesText(cwd, config.schema, built));
2065
2576
  return directory;
2066
2577
  }
2067
2578
  async function loadBuiltSchema(cwd, config) {
2068
2579
  return withRoles(await loadSchema(cwd, config.schema), config);
2069
2580
  }
2070
2581
  async function loadConfig(cwd) {
2071
- const path = join2(cwd, "okmodel.config.ts");
2582
+ const path = projectPath(cwd, "okmodel.config.ts");
2072
2583
  const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
2073
2584
  const record = isRecord(imported) ? imported : {};
2074
2585
  const config = record.default ?? record.config;
@@ -2078,7 +2589,7 @@ async function loadConfig(cwd) {
2078
2589
  return config;
2079
2590
  }
2080
2591
  async function loadSchema(cwd, spec) {
2081
- const path = join2(cwd, spec);
2592
+ const path = projectPath(cwd, spec);
2082
2593
  const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
2083
2594
  const record = isRecord(imported) ? imported : {};
2084
2595
  for (const value of Object.values(record)) {
@@ -2090,13 +2601,13 @@ async function loadSchema(cwd, spec) {
2090
2601
  throw new OkmError2("invalid", `${spec} must export a schema().`);
2091
2602
  }
2092
2603
  function readPrevious(directory) {
2093
- if (!existsSync2(directory))
2604
+ if (!existsSync3(directory))
2094
2605
  return;
2095
- const catalogs = readdirSync2(directory).filter((file) => file.endsWith(".catalog.json")).sort();
2606
+ const catalogs = readdirSync3(directory).filter((file) => file.endsWith(".catalog.json")).sort();
2096
2607
  const latest = catalogs.at(-1);
2097
2608
  if (latest === undefined)
2098
2609
  return;
2099
- const text = readFileSync2(join2(directory, latest), "utf8");
2610
+ const text = readFileSync3(join3(directory, latest), "utf8");
2100
2611
  return { catalog: parseCatalog2(text) };
2101
2612
  }
2102
2613
  function referenceAugmentation(source) {
@@ -2121,8 +2632,8 @@ function referenceAugmentation(source) {
2121
2632
  }
2122
2633
  function nextNumber(directory) {
2123
2634
  let max = 0;
2124
- if (existsSync2(directory)) {
2125
- for (const file of readdirSync2(directory)) {
2635
+ if (existsSync3(directory)) {
2636
+ for (const file of readdirSync3(directory)) {
2126
2637
  const match = /^(\d+)_/.exec(file);
2127
2638
  const value = match?.[1];
2128
2639
  if (value !== undefined)
@@ -2163,14 +2674,14 @@ function projectImportsValidate(cwd) {
2163
2674
  function directoryImportsValidate(dir) {
2164
2675
  let entries;
2165
2676
  try {
2166
- entries = readdirSync2(dir, { withFileTypes: true });
2677
+ entries = readdirSync3(dir, { withFileTypes: true });
2167
2678
  } catch {
2168
2679
  return false;
2169
2680
  }
2170
2681
  for (const entry of entries) {
2171
2682
  if (entry.name.startsWith(".") || SKIP_SCAN.has(entry.name))
2172
2683
  continue;
2173
- const path = join2(dir, entry.name);
2684
+ const path = join3(dir, entry.name);
2174
2685
  if (entry.isDirectory()) {
2175
2686
  if (directoryImportsValidate(path))
2176
2687
  return true;
@@ -2178,7 +2689,7 @@ function directoryImportsValidate(dir) {
2178
2689
  }
2179
2690
  if (!/\.(?:ts|tsx|mts|cts)$/.test(entry.name))
2180
2691
  continue;
2181
- if (runtimeImportsValidate(readFileSync2(path, "utf8")))
2692
+ if (runtimeImportsValidate(readFileSync3(path, "utf8")))
2182
2693
  return true;
2183
2694
  }
2184
2695
  return false;
@@ -2200,4 +2711,4 @@ function runtimeImportsValidate(source) {
2200
2711
  return false;
2201
2712
  }
2202
2713
 
2203
- export { errorDoc, lintMigrationDirectory, formatFindings, hasError, lintRefusal, assertTargetPolicy, assertDirectConnection, listTargets, selectTarget, buildProject, generateProject, planProject, checkProject, projectHead, openProject, loadBuiltSchema, loadConfig, loadSchema };
2714
+ export { projectPath, errorDoc, loadMigrations, readMigrationPlan, loadHeadSnapshot, lintMigrationDirectory, formatFindings, hasError, lintRefusal, buildProject, generateProject, planProject, checkProject, projectHead, openProject, loadBuiltSchema, loadConfig, loadSchema };