okmodel 0.5.1 → 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 (155) hide show
  1. package/README.md +29 -14
  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 +5 -4
  19. package/dist/dialects/pg/fn/index.js +2 -2
  20. package/dist/dialects/pg/index.d.ts +2 -1
  21. package/dist/dialects/pg/index.js +23 -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/owner.d.ts +42 -0
  25. package/dist/dialects/pg/path-endpoint.d.ts +20 -0
  26. package/dist/dialects/pg/role/check.d.ts +3 -0
  27. package/dist/dialects/pg/role/index.d.ts +71 -6
  28. package/dist/dialects/pg/role/sql.d.ts +11 -0
  29. package/dist/dialects/pg/schema.d.ts +2 -0
  30. package/dist/dialects/pg/table.d.ts +25 -3
  31. package/dist/dialects/pg/tenancy.d.ts +45 -3
  32. package/dist/dialects/pg/view/client.d.ts +19 -1
  33. package/dist/dialects/pg/view/index.d.ts +4 -0
  34. package/dist/dialects/pg/view/index.js +21 -178
  35. package/dist/dialects/pg/view/refresh.d.ts +24 -0
  36. package/dist/runtime/batch.d.ts +1 -1
  37. package/dist/runtime/conflict.d.ts +2 -2
  38. package/dist/runtime/drift.d.ts +9 -0
  39. package/dist/runtime/inflight.d.ts +14 -0
  40. package/dist/runtime/pg/bun.js +4 -3
  41. package/dist/runtime/pg/pg.js +3 -3
  42. package/dist/runtime/pg/pglite.js +4 -3
  43. package/dist/runtime/pg/postgresjs.js +3 -3
  44. package/dist/runtime/plan.d.ts +18 -10
  45. package/dist/runtime/predicate.d.ts +19 -0
  46. package/dist/runtime/safety/index.js +1 -1
  47. package/dist/runtime/tenancy/composite.d.ts +26 -0
  48. package/dist/runtime/tenancy/context.d.ts +16 -0
  49. package/dist/runtime/tenancy/index.d.ts +9 -5
  50. package/dist/runtime/tenancy/index.js +1508 -37
  51. package/dist/runtime/tenancy/path.d.ts +87 -0
  52. package/dist/runtime/tenancy/rls.d.ts +67 -0
  53. package/dist/runtime/tenancy/via.d.ts +22 -0
  54. package/dist/runtime/traits/index.js +17 -11
  55. package/dist/runtime/types.d.ts +13 -1
  56. package/dist/runtime/validate/index.js +4 -4
  57. package/dist/runtime/write.d.ts +1 -1
  58. package/dist/shared/09p049xa.js +702 -0
  59. package/dist/shared/{6qd27mx0.js → 1f0xa3cb.js} +5 -5
  60. package/dist/shared/1nw1f3vs.js +23 -0
  61. package/dist/shared/{96qcj21c.js → 1z04fa85.js} +19 -9
  62. package/dist/shared/{qm7cdtvr.js → 2cg3wnxc.js} +296 -60
  63. package/dist/shared/{dyj86cmj.js → 2jd1sn0e.js} +2 -2
  64. package/dist/shared/{gbtcc2d7.js → 2z7wv5tj.js} +10 -35
  65. package/dist/shared/{k8v5v5wj.js → 32cnm7kn.js} +6 -1
  66. package/dist/shared/{t4tbsy9h.js → 358zsv4r.js} +1 -1
  67. package/dist/shared/{2pqx26h8.js → 3fqwmr09.js} +31 -22
  68. package/dist/shared/{zqrnhwsq.js → 3qnhc7w5.js} +16 -9
  69. package/dist/shared/{r2sp8hrc.js → 5d8t84hg.js} +12 -3
  70. package/dist/shared/{87jvvhnt.js → 5j36z8dn.js} +16 -5
  71. package/dist/shared/{1g0drakv.js → 5yv4k6a6.js} +1 -1
  72. package/dist/shared/8dqhth8c.js +12 -0
  73. package/dist/shared/8etn7d8w.js +62 -0
  74. package/dist/shared/{v36c5g6p.js → 8x72s86k.js} +400 -13
  75. package/dist/shared/{nxdp2ze3.js → 929h19xp.js} +107 -5
  76. package/dist/shared/{gjthds9j.js → 9628a9xt.js} +1 -1
  77. package/dist/shared/{8xsbagad.js → 9s0ypp87.js} +88 -7
  78. package/dist/shared/{efmy2gcg.js → ag4c7516.js} +1 -1
  79. package/dist/shared/b3awxn7h.js +45 -0
  80. package/dist/shared/b4v57v3x.js +9 -0
  81. package/dist/shared/bdk4sty6.js +58 -0
  82. package/dist/shared/{51mvgj8w.js → c58secgj.js} +100 -7
  83. package/dist/shared/d5azex6s.js +6 -0
  84. package/dist/shared/{5fqx9yh4.js → dk8v79p3.js} +3 -1
  85. package/dist/shared/{4m9fn52z.js → ed0b8k4s.js} +1 -1
  86. package/dist/shared/{z2rnngj4.js → ek2wt2dz.js} +8 -5
  87. package/dist/shared/{sycnx8gt.js → f0f1n281.js} +18 -8
  88. package/dist/shared/{f5cw5q5r.js → g90m9b0g.js} +2 -1
  89. package/dist/shared/{pf0d8f77.js → gwn6v667.js} +1 -1
  90. package/dist/shared/{8rsy3hxz.js → h15jx9bc.js} +1 -1
  91. package/dist/shared/{5enne4ma.js → hazqqecq.js} +17 -93
  92. package/dist/shared/{jwtxdj06.js → j3rkk835.js} +32 -9
  93. package/dist/shared/{hf22e0wt.js → j60w0dvr.js} +19 -18
  94. package/dist/shared/{ycatkxmq.js → jgqj11w4.js} +1 -1
  95. package/dist/shared/k68eedmy.js +85 -0
  96. package/dist/shared/{szm0cksa.js → knmes2kh.js} +34 -6
  97. package/dist/shared/{0t22wwh1.js → mb2ycs1p.js} +4 -2
  98. package/dist/shared/{cr74e96e.js → n076ydsc.js} +1 -1
  99. package/dist/shared/{cctw17hj.js → nx9hh281.js} +31 -28
  100. package/dist/shared/{gde73es6.js → p248q79y.js} +56 -55
  101. package/dist/shared/{janx09m5.js → p6bjzq9q.js} +11 -7
  102. package/dist/shared/{6rk2gen5.js → pjr6xrm5.js} +2 -2
  103. package/dist/shared/{wq9n6pwr.js → pz6rj8ay.js} +82 -9
  104. package/dist/shared/{caw8b9d8.js → q3j1tq7a.js} +35 -22
  105. package/dist/shared/{8bwbwy8h.js → qq3899ht.js} +128 -18
  106. package/dist/shared/{tm5ab8we.js → r11n8grd.js} +2 -2
  107. package/dist/shared/r1fdd0f6.js +19 -0
  108. package/dist/shared/{8ktwd3rh.js → r3j8s5y0.js} +1 -1
  109. package/dist/shared/rhs3xe4k.js +7 -0
  110. package/dist/shared/{ntneex2a.js → rpycarcr.js} +6 -6
  111. package/dist/shared/{aj2qxyhj.js → sbdme3vy.js} +14 -2
  112. package/dist/shared/{qv2ydck9.js → t3g0k88x.js} +638 -67
  113. package/dist/shared/{ga3sfy93.js → ttmg09vd.js} +535 -200
  114. package/dist/shared/{kfjp0gck.js → venqrjna.js} +8 -4
  115. package/dist/shared/{fancm5pt.js → vjpnzwjz.js} +138 -15
  116. package/dist/shared/{32zqc1dw.js → vmfkhszw.js} +31 -7
  117. package/dist/shared/{krxx77g0.js → wkme6tep.js} +10 -8
  118. package/dist/shared/{ps7a6h14.js → wt5rzv1h.js} +12 -8
  119. package/dist/shared/wxr8cwns.js +6 -0
  120. package/dist/shared/{6mnp99hx.js → wynzeggs.js} +16 -12
  121. package/dist/shared/x3jc53kg.js +220 -0
  122. package/dist/shared/{sdc7r2ww.js → xq0mvhts.js} +8 -8
  123. package/dist/shared/xqve9598.js +53 -0
  124. package/dist/shared/{47x8x9ze.js → y2t3jktd.js} +2 -2
  125. package/dist/shared/{9x25fxhc.js → y34a8714.js} +1 -1
  126. package/dist/shared/{k7dwkhvq.js → y6jdds7g.js} +4 -1
  127. package/dist/shared/{nkaxvt9q.js → ys0xzyaj.js} +1 -1
  128. package/dist/shared/{c9z5g0fm.js → ywr8hmey.js} +5 -2
  129. package/dist/shared/{g7ez901n.js → ywspezzx.js} +1 -1
  130. package/dist/shared/{9m80mkac.js → ze4hxsmy.js} +1 -1
  131. package/dist/shared/{r8x5qhd7.js → zm0mafk4.js} +10 -4
  132. package/dist/shared/{y005b761.js → zy8r7qvz.js} +6 -2
  133. package/dist/tooling/cli.js +41 -15
  134. package/dist/tooling/migrate/apply.d.ts +14 -0
  135. package/dist/tooling/migrate/check.d.ts +3 -4
  136. package/dist/tooling/migrate/classify.d.ts +4 -0
  137. package/dist/tooling/migrate/config.d.ts +3 -2
  138. package/dist/tooling/migrate/files.d.ts +11 -1
  139. package/dist/tooling/migrate/index.js +14 -14
  140. package/dist/tooling/migrate/lint.d.ts +1 -1
  141. package/dist/tooling/migrate/lost.d.ts +28 -0
  142. package/dist/tooling/migrate/paths.d.ts +14 -0
  143. package/dist/tooling/migrate/plan.d.ts +6 -3
  144. package/dist/tooling/migrate/runner.d.ts +25 -0
  145. package/dist/tooling/migrate/sandbox.d.ts +57 -0
  146. package/dist/tooling/migrate/sql-lex.d.ts +20 -3
  147. package/dist/tooling/migrate/text.d.ts +42 -0
  148. package/dist/tooling/node-version.d.ts +21 -0
  149. package/dist/tooling/testing/facts.d.ts +33 -5
  150. package/dist/tooling/testing/index.d.ts +1 -1
  151. package/dist/tooling/testing/index.js +3 -3
  152. package/dist/tooling/testing/isolation.d.ts +11 -5
  153. package/package.json +5 -2
  154. package/dist/shared/7zwwyh0v.js +0 -448
  155. package/dist/shared/q58y17hy.js +0 -21
@@ -9,51 +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 "./krxx77g0.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
- sqlStatements,
22
27
  parseReplace,
23
28
  planMigration,
24
29
  formatPlan,
25
30
  parsePlan,
26
31
  staleRenames,
32
+ sessionFailure,
33
+ finishQuiet,
27
34
  assertTargetPolicy,
28
35
  assertTargetAlias,
29
36
  listTargets,
30
37
  selectTarget
31
- } from "./ga3sfy93.js";
38
+ } from "./ttmg09vd.js";
32
39
  import {
33
40
  contributedFields
34
41
  } from "./n0dgs0pb.js";
35
42
  import {
36
43
  assertValidation,
37
44
  writeWouldValidate
38
- } from "./9m80mkac.js";
45
+ } from "./ze4hxsmy.js";
46
+ import {
47
+ tableOwner,
48
+ stampOwnership
49
+ } from "./xqve9598.js";
39
50
  import {
40
51
  catalog,
41
52
  isDomain
42
- } from "./g7ez901n.js";
53
+ } from "./ywspezzx.js";
43
54
  import {
44
55
  snakeCase,
45
56
  emittedTypeName
46
- } from "./ntneex2a.js";
57
+ } from "./rpycarcr.js";
47
58
  import {
48
59
  ColumnBuilder2
49
- } from "./c9z5g0fm.js";
60
+ } from "./ywr8hmey.js";
50
61
  import {
51
62
  staticNamespace2
52
- } from "./t4tbsy9h.js";
63
+ } from "./358zsv4r.js";
64
+ import {
65
+ sha2562
66
+ } from "./nfzc5z0w.js";
53
67
  import {
54
68
  OkmError2
55
69
  } from "./52szzakm.js";
56
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
+
57
77
  // src/tooling/errors/registry.ts
58
78
  var ERROR_DOCS = [
59
79
  {
@@ -494,6 +514,18 @@ var ERROR_DOCS = [
494
514
  summary: "The last migration's catalog does not match the schema.",
495
515
  fix: "Run okm generate."
496
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
+ },
497
529
  {
498
530
  code: "OKM1601",
499
531
  title: "Stale typed SQL",
@@ -533,8 +565,14 @@ var ERROR_DOCS = [
533
565
  {
534
566
  code: "OKM1707",
535
567
  title: "RLS role",
536
- summary: "connect() was given a role that owns the tables or is a superuser, under the rls strategy.",
537
- 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."
538
576
  },
539
577
  {
540
578
  code: "OKM1801",
@@ -617,7 +655,7 @@ var ERROR_DOCS = [
617
655
  {
618
656
  code: "OKM1825",
619
657
  title: "Missing privilege",
620
- summary: "The application role lacks a privilege on a managed object.",
658
+ summary: "A role lacks a privilege on a managed object.",
621
659
  fix: "Grant the privilege, or change the role. okm doctor reports the object and the privilege."
622
660
  },
623
661
  {
@@ -712,9 +750,53 @@ function indexDocs() {
712
750
  return index;
713
751
  }
714
752
 
715
- // src/tooling/migrate/lint.ts
753
+ // src/tooling/migrate/files.ts
716
754
  import { existsSync, readdirSync, readFileSync } from "node:fs";
717
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";
718
800
 
719
801
  // src/tooling/migrate/lint-rules.ts
720
802
  var INTEGER_WIDTH = {
@@ -751,7 +833,12 @@ var STEP_RULES = [
751
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")),
752
834
  rule("OKM1537", "locking", "error", "SET NOT NULL locks the table while it scans for nulls.", (input) => hit(unsafeSetNotNull(input), "sets NOT NULL")),
753
835
  rule("OKM1538", "locking", "warning", "A type change that rewrites the table locks it for the rewrite.", (input) => typeChange(input, "rewrite")),
754
- rule("OKM1542", "data-dependent", "error", "A data statement outside backfill(), or DO or CALL, is not a migration step.", (input) => dataStatement(input.step))
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
+ })
755
842
  ];
756
843
  var COLUMN_RULES = [
757
844
  {
@@ -810,6 +897,146 @@ function normalized(sql) {
810
897
  }
811
898
  return out;
812
899
  }
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)
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
+ }
813
1040
  function dataStatement(step) {
814
1041
  const text = normalized(step.sql);
815
1042
  if (/^(?:do|call)\b/.test(text))
@@ -1161,15 +1388,15 @@ function lintCatalog(source) {
1161
1388
  return findings;
1162
1389
  }
1163
1390
  function lintMigrationDirectory(directory, pending) {
1164
- if (!existsSync(directory))
1391
+ if (!existsSync2(directory))
1165
1392
  return [];
1166
- const names = readdirSync(directory).filter((file) => file.endsWith(".sql")).sort();
1393
+ const names = readdirSync2(directory).filter((file) => file.endsWith(".sql")).sort();
1167
1394
  let before = catalog([]);
1168
1395
  const findings = [];
1169
1396
  for (const file of names) {
1170
1397
  const id = file.slice(0, -".sql".length);
1171
- const plan = parsePlan(readFileSync(join(directory, file), "utf8"));
1172
- 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"));
1173
1400
  if (pending === undefined || pending.has(id)) {
1174
1401
  for (const item of lintPlan(plan, before, after)) {
1175
1402
  findings.push({ ...item, place: `${id} ${item.place}` });
@@ -1288,8 +1515,8 @@ function columnPlace(column) {
1288
1515
  }
1289
1516
 
1290
1517
  // src/tooling/migrate/project.ts
1291
- import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
1292
- 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";
1293
1520
  import { pathToFileURL } from "node:url";
1294
1521
 
1295
1522
  // src/dialects/pg/declarations.ts
@@ -1331,7 +1558,7 @@ function sqlIdentifier(name, casing) {
1331
1558
  }
1332
1559
 
1333
1560
  // src/dialects/pg/emit.ts
1334
- function emitRowTypes(source) {
1561
+ function emitRowTypes(source, jsonTypes) {
1335
1562
  const lines = [
1336
1563
  "/**",
1337
1564
  " * Row types emitted from the schema.",
@@ -1349,13 +1576,13 @@ function emitRowTypes(source) {
1349
1576
  inserts.push(` readonly ${key}: ${name}Insert;`);
1350
1577
  updates.push(` readonly ${key}: ${name}Update;`);
1351
1578
  lines.push(`export interface ${name} {`);
1352
- lines.push(fields(item, "row", source));
1579
+ lines.push(fields(item, "row", source, jsonTypes));
1353
1580
  lines.push("}", "");
1354
1581
  lines.push(`export interface ${name}Insert {`);
1355
- lines.push(fields(item, "insert", source));
1582
+ lines.push(fields(item, "insert", source, jsonTypes));
1356
1583
  lines.push("}", "");
1357
1584
  lines.push(`export interface ${name}Update {`);
1358
- lines.push(fields(item, "update", source));
1585
+ lines.push(fields(item, "update", source, jsonTypes));
1359
1586
  lines.push("}", "");
1360
1587
  }
1361
1588
  lines.push("export interface Rows {");
@@ -1388,7 +1615,74 @@ ${body}`;
1388
1615
  function propertyName(name) {
1389
1616
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : JSON.stringify(name);
1390
1617
  }
1391
- 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) {
1392
1686
  const printed = [];
1393
1687
  const options = item.options;
1394
1688
  const entries = Object.entries(item.columns);
@@ -1406,7 +1700,7 @@ function fields(item, kind, source) {
1406
1700
  const state = builder.state;
1407
1701
  if (kind === "row") {
1408
1702
  if (!state.hidden) {
1409
- printed.push(` readonly ${field}: ${valueType(state)};`);
1703
+ printed.push(` readonly ${field}: ${valueType(state, jsonTypes?.get(`${item.name}.${field}`))};`);
1410
1704
  }
1411
1705
  continue;
1412
1706
  }
@@ -1415,7 +1709,7 @@ function fields(item, kind, source) {
1415
1709
  continue;
1416
1710
  }
1417
1711
  const mode = kind === "update" ? "optional" : written;
1418
- const label = valueType(state);
1712
+ const label = valueType(state, jsonTypes?.get(`${item.name}.${field}`));
1419
1713
  printed.push(mode === "optional" ? ` readonly ${field}?: ${label};` : ` readonly ${field}: ${label};`);
1420
1714
  }
1421
1715
  return printed.join(`
@@ -1435,8 +1729,8 @@ function updateMode(state) {
1435
1729
  return "omit";
1436
1730
  return "optional";
1437
1731
  }
1438
- function valueType(state) {
1439
- let label = scalarLabel(state);
1732
+ function valueType(state, jsonArg) {
1733
+ let label = scalarLabel(state, jsonArg);
1440
1734
  if (state.dims > 0) {
1441
1735
  for (let rank = 0;rank < state.dims; rank += 1) {
1442
1736
  label = `readonly (${label})[]`;
@@ -1447,7 +1741,7 @@ function valueType(state) {
1447
1741
  }
1448
1742
  return label;
1449
1743
  }
1450
- function scalarLabel(state) {
1744
+ function scalarLabel(state, jsonArg) {
1451
1745
  if (state.picklist !== undefined) {
1452
1746
  return state.picklist.values.map((value) => JSON.stringify(value)).join(" | ");
1453
1747
  }
@@ -1480,9 +1774,9 @@ function scalarLabel(state) {
1480
1774
  case "bytea":
1481
1775
  return "Uint8Array";
1482
1776
  case "json":
1483
- return "json<unknown>";
1777
+ return `json<${jsonArg ?? "unknown"}>`;
1484
1778
  case "jsonb":
1485
- return "jsonb<unknown>";
1779
+ return `jsonb<${jsonArg ?? "unknown"}>`;
1486
1780
  case "timestamptz":
1487
1781
  return "Temporal.Instant";
1488
1782
  case "timestamp":
@@ -1526,17 +1820,34 @@ function baseName(dataType) {
1526
1820
  }
1527
1821
 
1528
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
+ }
1529
1836
  var PROVENANCE = { origin: "file", name: "roles" };
1530
1837
  var RELATION = ["SELECT", "INSERT", "UPDATE", "DELETE"];
1531
1838
  var READ = ["SELECT"];
1532
1839
  var ROUTINE = ["EXECUTE"];
1533
1840
  var SEQUENCE = ["USAGE", "SELECT"];
1534
- function attachRoles(source, input) {
1535
- return catalog([...source.objects, ...contributeRoles(input, source.objects)]);
1841
+ function attachRoles(source, input, exposure) {
1842
+ return catalog([...source.objects, ...contributeRoles(input, source.objects, exposure)]);
1536
1843
  }
1537
- function contributeRoles(input, built) {
1844
+ function contributeRoles(input, built, exposure) {
1538
1845
  const migration = requireName(input.migration, "migration");
1539
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
+ }
1540
1851
  const managed = new Map;
1541
1852
  for (const role of input.managed ?? []) {
1542
1853
  const name = requireName(role.name, "managed role");
@@ -1544,6 +1855,10 @@ function contributeRoles(input, built) {
1544
1855
  }
1545
1856
  const objects = [];
1546
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"));
1547
1862
  for (const name of [...names].sort()) {
1548
1863
  const declaration = managed.get(name);
1549
1864
  objects.push(roleObject({
@@ -1558,8 +1873,10 @@ function contributeRoles(input, built) {
1558
1873
  }
1559
1874
  const roleIdentity = (name) => ({ kind: "role", name });
1560
1875
  const namespaces = new Set;
1876
+ const exposed = indexExposure(exposure);
1877
+ const columnGrants = new Map;
1561
1878
  for (const object of built) {
1562
- if (object.owner === "ignored")
1879
+ if (object.owner === "ignored" || object.owner === "external")
1563
1880
  continue;
1564
1881
  const ref = grantRef(object);
1565
1882
  if (ref === undefined)
@@ -1577,6 +1894,7 @@ function contributeRoles(input, built) {
1577
1894
  }));
1578
1895
  }
1579
1896
  }
1897
+ const relations = built.filter((object) => object.owner !== "ignored" && object.owner !== "external" && (object.kind === "table" || object.kind === "view" || object.kind === "materializedView"));
1580
1898
  const schemas = namespaces.size === 0 ? ["public"] : [...namespaces].sort();
1581
1899
  for (const name of schemas) {
1582
1900
  const namespace = staticNamespace2(name);
@@ -1597,6 +1915,45 @@ function contributeRoles(input, built) {
1597
1915
  }));
1598
1916
  }
1599
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
+ }));
1600
1957
  }
1601
1958
  return objects;
1602
1959
  }
@@ -1638,6 +1995,137 @@ function privilegesFor(kind) {
1638
1995
  function objectIdentity(object) {
1639
1996
  return object.identity;
1640
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
+ }
1641
2129
 
1642
2130
  // src/tooling/migrate/estimate.ts
1643
2131
  var LARGE_TABLE_ROWS = 1e6;
@@ -1677,15 +2165,13 @@ async function readRowEstimates(url, schema, tables) {
1677
2165
  return map;
1678
2166
  } catch (error) {
1679
2167
  if (began)
1680
- await connection.execute("rollback").catch(() => {
1681
- return;
1682
- });
1683
- throw error;
2168
+ await finishQuiet(() => connection.execute("rollback"));
2169
+ throw sessionFailure(error) ?? error;
1684
2170
  } finally {
1685
- await connection.release();
2171
+ await finishQuiet(() => connection.release());
1686
2172
  }
1687
2173
  } finally {
1688
- await pool.close();
2174
+ await finishQuiet(() => pool.close());
1689
2175
  }
1690
2176
  }
1691
2177
  function annotateLock(step, estimates, aliases = new Map) {
@@ -1842,15 +2328,17 @@ async function buildProject(cwd) {
1842
2328
  }
1843
2329
  async function generateProject(cwd, name, flags) {
1844
2330
  const prepared = await prepare(cwd, name, flags);
2331
+ if (name === "init")
2332
+ writeGitAttributes(cwd);
1845
2333
  writeArtifact(cwd, prepared.config, prepared.built);
1846
2334
  if (prepared.plan.steps.length === 0)
1847
2335
  return;
1848
- const directory = join2(cwd, prepared.config.migrations ?? "migrations");
2336
+ const directory = projectPath(cwd, prepared.config.migrations ?? "migrations");
1849
2337
  mkdirSync(directory, { recursive: true });
1850
2338
  const base = `${nextNumber(directory)}_${safeName(name)}`;
1851
- const sqlPath = join2(directory, `${base}.sql`);
2339
+ const sqlPath = join3(directory, `${base}.sql`);
1852
2340
  writeFileSync(sqlPath, formatPlan(prepared.plan));
1853
- writeFileSync(join2(directory, `${base}.catalog.json`), serializeCatalog2(prepared.built.catalog));
2341
+ writeFileSync(join3(directory, `${base}.catalog.json`), serializeCatalog2(prepared.built.catalog));
1854
2342
  return { path: sqlPath, findings: lintPrepared(prepared) };
1855
2343
  }
1856
2344
  async function planProject(cwd, name, flags, invoke) {
@@ -1873,9 +2361,9 @@ async function checkProject(cwd, flags) {
1873
2361
  const targets = listTargets(opened.config);
1874
2362
  assertTargetAlias(targets);
1875
2363
  if (opened.config.tables !== undefined) {
1876
- const directory = join2(cwd, opened.config.tables);
1877
- const files = existsSync2(directory) ? readdirSync2(directory) : [];
1878
- 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");
1879
2367
  const missing = unlistedTableFiles(source, files);
1880
2368
  if (missing.length > 0) {
1881
2369
  throw new OkmError2("OKM1024", `Table file not in schema: ${missing.join(", ")}.`, {
@@ -1894,7 +2382,7 @@ async function checkProject(cwd, flags) {
1894
2382
  const target = selectTarget(opened.config, named);
1895
2383
  assertTargetPolicy(target, "check", flags?.allowProtected ?? false);
1896
2384
  assertTargetPolicy(target, "drift", flags?.allowProtected ?? false);
1897
- const { assertAuthorDrift } = await import("./6qd27mx0.js");
2385
+ const { assertAuthorDrift } = await import("./1f0xa3cb.js");
1898
2386
  const allowPooler = flags?.allowPooler === true || opened.config.allowPooler === true;
1899
2387
  await assertAuthorDrift(target.url, opened.built.catalog, opened.config.roles, allowPooler);
1900
2388
  return findings;
@@ -1975,7 +2463,7 @@ async function openProject(cwd) {
1975
2463
  const config = await loadConfig(cwd);
1976
2464
  const built = withRoles(await loadSchema(cwd, config.schema), config);
1977
2465
  const declarations = schemaDeclarations(built);
1978
- const previous = readPrevious(join2(cwd, config.migrations ?? "migrations"));
2466
+ const previous = readPrevious(projectPath(cwd, config.migrations ?? "migrations"));
1979
2467
  const stale = staleRenames(previous?.catalog, declarations.renames);
1980
2468
  if (stale.length > 0) {
1981
2469
  throw new OkmError2("OKM1020", stale.join(" "), {
@@ -1990,25 +2478,108 @@ async function openProject(cwd) {
1990
2478
  };
1991
2479
  }
1992
2480
  function withRoles(built, config) {
1993
- 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)
1994
2486
  return built;
1995
- 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
+ `);
1996
2566
  }
1997
2567
  function writeArtifact(cwd, config, built) {
1998
- const directory = join2(cwd, config.out ?? ".okm");
2568
+ const directory = projectPath(cwd, config.out ?? ".okm");
1999
2569
  mkdirSync(directory, { recursive: true });
2000
2570
  const startup = startupCatalog2(built.catalog);
2001
- writeFileSync(join2(directory, "catalog.json"), serializeCatalog2(startup));
2002
- 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)}
2003
2574
  `);
2004
- writeFileSync(join2(directory, "types.d.ts"), `${emitRowTypes(built)}${referenceAugmentation(built)}`);
2575
+ writeFileSync(join3(directory, "types.d.ts"), typesText(cwd, config.schema, built));
2005
2576
  return directory;
2006
2577
  }
2007
2578
  async function loadBuiltSchema(cwd, config) {
2008
2579
  return withRoles(await loadSchema(cwd, config.schema), config);
2009
2580
  }
2010
2581
  async function loadConfig(cwd) {
2011
- const path = join2(cwd, "okmodel.config.ts");
2582
+ const path = projectPath(cwd, "okmodel.config.ts");
2012
2583
  const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
2013
2584
  const record = isRecord(imported) ? imported : {};
2014
2585
  const config = record.default ?? record.config;
@@ -2018,7 +2589,7 @@ async function loadConfig(cwd) {
2018
2589
  return config;
2019
2590
  }
2020
2591
  async function loadSchema(cwd, spec) {
2021
- const path = join2(cwd, spec);
2592
+ const path = projectPath(cwd, spec);
2022
2593
  const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
2023
2594
  const record = isRecord(imported) ? imported : {};
2024
2595
  for (const value of Object.values(record)) {
@@ -2030,13 +2601,13 @@ async function loadSchema(cwd, spec) {
2030
2601
  throw new OkmError2("invalid", `${spec} must export a schema().`);
2031
2602
  }
2032
2603
  function readPrevious(directory) {
2033
- if (!existsSync2(directory))
2604
+ if (!existsSync3(directory))
2034
2605
  return;
2035
- const catalogs = readdirSync2(directory).filter((file) => file.endsWith(".catalog.json")).sort();
2606
+ const catalogs = readdirSync3(directory).filter((file) => file.endsWith(".catalog.json")).sort();
2036
2607
  const latest = catalogs.at(-1);
2037
2608
  if (latest === undefined)
2038
2609
  return;
2039
- const text = readFileSync2(join2(directory, latest), "utf8");
2610
+ const text = readFileSync3(join3(directory, latest), "utf8");
2040
2611
  return { catalog: parseCatalog2(text) };
2041
2612
  }
2042
2613
  function referenceAugmentation(source) {
@@ -2061,8 +2632,8 @@ function referenceAugmentation(source) {
2061
2632
  }
2062
2633
  function nextNumber(directory) {
2063
2634
  let max = 0;
2064
- if (existsSync2(directory)) {
2065
- for (const file of readdirSync2(directory)) {
2635
+ if (existsSync3(directory)) {
2636
+ for (const file of readdirSync3(directory)) {
2066
2637
  const match = /^(\d+)_/.exec(file);
2067
2638
  const value = match?.[1];
2068
2639
  if (value !== undefined)
@@ -2103,14 +2674,14 @@ function projectImportsValidate(cwd) {
2103
2674
  function directoryImportsValidate(dir) {
2104
2675
  let entries;
2105
2676
  try {
2106
- entries = readdirSync2(dir, { withFileTypes: true });
2677
+ entries = readdirSync3(dir, { withFileTypes: true });
2107
2678
  } catch {
2108
2679
  return false;
2109
2680
  }
2110
2681
  for (const entry of entries) {
2111
2682
  if (entry.name.startsWith(".") || SKIP_SCAN.has(entry.name))
2112
2683
  continue;
2113
- const path = join2(dir, entry.name);
2684
+ const path = join3(dir, entry.name);
2114
2685
  if (entry.isDirectory()) {
2115
2686
  if (directoryImportsValidate(path))
2116
2687
  return true;
@@ -2118,7 +2689,7 @@ function directoryImportsValidate(dir) {
2118
2689
  }
2119
2690
  if (!/\.(?:ts|tsx|mts|cts)$/.test(entry.name))
2120
2691
  continue;
2121
- if (runtimeImportsValidate(readFileSync2(path, "utf8")))
2692
+ if (runtimeImportsValidate(readFileSync3(path, "utf8")))
2122
2693
  return true;
2123
2694
  }
2124
2695
  return false;
@@ -2140,4 +2711,4 @@ function runtimeImportsValidate(source) {
2140
2711
  return false;
2141
2712
  }
2142
2713
 
2143
- export { errorDoc, lintMigrationDirectory, formatFindings, hasError, lintRefusal, 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 };