okmodel 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +95 -12
  2. package/dist/contracts/catalog/document.d.ts +20 -0
  3. package/dist/contracts/driver.d.ts +7 -0
  4. package/dist/dialects/pg/filter-parse.d.ts +8 -2
  5. package/dist/dialects/pg/fn/index.js +8 -0
  6. package/dist/dialects/pg/index.js +8 -2
  7. package/dist/dialects/pg/introspect.d.ts +6 -0
  8. package/dist/dialects/pg/ops/or.d.ts +1 -1
  9. package/dist/dialects/pg/role/sql.d.ts +10 -0
  10. package/dist/dialects/pg/schema.d.ts +15 -6
  11. package/dist/dialects/pg/temporal.d.ts +5 -76
  12. package/dist/dialects/pg/view/index.d.ts +20 -6
  13. package/dist/dialects/pg/view/index.js +1 -1
  14. package/dist/dialects/pg/view/scratch.d.ts +2 -2
  15. package/dist/runtime/client.d.ts +6 -1
  16. package/dist/runtime/conflict.d.ts +2 -1
  17. package/dist/runtime/pg/bun.d.ts +12 -7
  18. package/dist/runtime/pg/bun.js +7 -4
  19. package/dist/runtime/pg/pg.d.ts +12 -6
  20. package/dist/runtime/pg/pg.js +7 -4
  21. package/dist/runtime/pg/pglite.d.ts +11 -3
  22. package/dist/runtime/pg/pglite.js +7 -4
  23. package/dist/runtime/pg/postgresjs.d.ts +12 -6
  24. package/dist/runtime/pg/postgresjs.js +7 -4
  25. package/dist/runtime/plan.d.ts +16 -1
  26. package/dist/runtime/tenancy/index.js +1 -1
  27. package/dist/runtime/topology.d.ts +191 -0
  28. package/dist/runtime/traits/index.js +7 -7
  29. package/dist/runtime/types.d.ts +57 -2
  30. package/dist/runtime/validate/index.js +3 -3
  31. package/dist/shared/{radd3z4g.js → 0t22wwh1.js} +5 -9
  32. package/dist/shared/{ady1ms74.js → 2pqx26h8.js} +25 -10
  33. package/dist/shared/{kyxb62mc.js → 31vm914h.js} +9 -1
  34. package/dist/shared/{jns2wx4t.js → 32zqc1dw.js} +18 -7
  35. package/dist/shared/{nxb42q7h.js → 47x8x9ze.js} +2 -2
  36. package/dist/shared/{zvbjc949.js → 4az561fn.js} +1 -1
  37. package/dist/shared/{s6gqfn5j.js → 51mvgj8w.js} +27 -3
  38. package/dist/shared/{k3ted12y.js → 54tqkxja.js} +20 -2
  39. package/dist/shared/{3p4b1fg0.js → 5enne4ma.js} +1 -1
  40. package/dist/shared/{6693j9rs.js → 5fqx9yh4.js} +6 -2
  41. package/dist/shared/{c5g81ngb.js → 6mnp99hx.js} +11 -8
  42. package/dist/shared/{hqjsxnz7.js → 6qd27mx0.js} +5 -4
  43. package/dist/shared/{v7tpnhnz.js → 6rk2gen5.js} +5 -3
  44. package/dist/shared/{2321avmn.js → 7zwwyh0v.js} +46 -16
  45. package/dist/shared/{fa9vt245.js → 87jvvhnt.js} +2 -2
  46. package/dist/shared/{8jb4ymh2.js → 8bwbwy8h.js} +12 -1
  47. package/dist/shared/{c8aghh05.js → 8rsy3hxz.js} +7 -1
  48. package/dist/shared/{41kv2xdm.js → 8xsbagad.js} +1 -1
  49. package/dist/shared/{6p5k20sm.js → 96qcj21c.js} +3 -3
  50. package/dist/shared/{jgxzjzcz.js → aj2qxyhj.js} +2 -2
  51. package/dist/shared/{5vdm4sgp.js → caw8b9d8.js} +4 -4
  52. package/dist/shared/{s4mb6a4b.js → cctw17hj.js} +74 -15
  53. package/dist/shared/{rrstp3kb.js → cgbamx3e.js} +2 -2
  54. package/dist/shared/{axhk06q1.js → cr74e96e.js} +1 -1
  55. package/dist/shared/{b066g80h.js → efmy2gcg.js} +2 -2
  56. package/dist/shared/{gdh9mrtp.js → f5cw5q5r.js} +2 -2
  57. package/dist/shared/{xghkt7a9.js → fancm5pt.js} +6 -233
  58. package/dist/shared/{n5y9zyj1.js → ga3sfy93.js} +369 -14
  59. package/dist/shared/{xrfnq5k8.js → gbtcc2d7.js} +55 -5
  60. package/dist/shared/{7nk68fj8.js → gde73es6.js} +20 -15
  61. package/dist/shared/{kkzwp3d2.js → hf22e0wt.js} +55 -19
  62. package/dist/shared/{tny2t3sm.js → janx09m5.js} +8 -7
  63. package/dist/shared/{4ef7mmqf.js → jwtxdj06.js} +5 -5
  64. package/dist/shared/{sf8m6tfy.js → k8v5v5wj.js} +3 -3
  65. package/dist/shared/{qn0srwwq.js → kfjp0gck.js} +5 -3
  66. package/dist/shared/{wqem4jed.js → krxx77g0.js} +7 -7
  67. package/dist/shared/{edwqqryd.js → nkaxvt9q.js} +1 -1
  68. package/dist/shared/{jsd07prz.js → ntneex2a.js} +1 -1
  69. package/dist/shared/nxdp2ze3.js +1197 -0
  70. package/dist/shared/{tbszx8mz.js → pf0d8f77.js} +1 -1
  71. package/dist/shared/{84ptn35q.js → ps7a6h14.js} +10 -9
  72. package/dist/shared/{kj680nxv.js → q58y17hy.js} +9 -7
  73. package/dist/shared/{awrsqrn7.js → qm7cdtvr.js} +22 -9
  74. package/dist/shared/{q6zsyh31.js → qv2ydck9.js} +156 -205
  75. package/dist/shared/{kafaa5zv.js → r2sp8hrc.js} +28 -8
  76. package/dist/shared/{fb00y8fp.js → r8x5qhd7.js} +5 -5
  77. package/dist/shared/{yjw02enp.js → sdc7r2ww.js} +8 -7
  78. package/dist/shared/sycnx8gt.js +289 -0
  79. package/dist/shared/{hcmdtwgh.js → szm0cksa.js} +4 -1
  80. package/dist/shared/{zg24x0hm.js → tm5ab8we.js} +10 -2
  81. package/dist/shared/{ygem90rn.js → v36c5g6p.js} +85 -21
  82. package/dist/shared/wq9n6pwr.js +139 -0
  83. package/dist/shared/{7p2wbz7f.js → x72jb7ad.js} +1 -1
  84. package/dist/shared/{q795afwa.js → z2rnngj4.js} +3 -3
  85. package/dist/shared/{sr5ms7b3.js → zqrnhwsq.js} +3 -3
  86. package/dist/tooling/cli.js +70 -23
  87. package/dist/tooling/migrate/apply.d.ts +2 -0
  88. package/dist/tooling/migrate/commands.d.ts +1 -2
  89. package/dist/tooling/migrate/drift.d.ts +1 -1
  90. package/dist/tooling/migrate/estimate.d.ts +14 -1
  91. package/dist/tooling/migrate/files.d.ts +7 -0
  92. package/dist/tooling/migrate/index.js +12 -9
  93. package/dist/tooling/migrate/lint.d.ts +3 -3
  94. package/dist/tooling/migrate/plan.d.ts +7 -3
  95. package/dist/tooling/migrate/policy.d.ts +11 -0
  96. package/dist/tooling/migrate/sql-lex.d.ts +22 -0
  97. package/dist/tooling/testing/factories.d.ts +10 -0
  98. package/dist/tooling/testing/facts.d.ts +3 -0
  99. package/dist/tooling/testing/index.js +3 -3
  100. package/dist/tooling/testing/isolation.d.ts +4 -3
  101. package/package.json +6 -5
  102. package/dist/shared/0v1g424p.js +0 -26
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  isConnectionLost
3
- } from "./tbszx8mz.js";
3
+ } from "./pf0d8f77.js";
4
4
  import {
5
5
  openClient,
6
6
  withHttp,
7
7
  settleCall
8
- } from "./kkzwp3d2.js";
8
+ } from "./hf22e0wt.js";
9
9
  import {
10
10
  rejectKeys,
11
11
  isRecord,
12
12
  fail
13
- } from "./xrfnq5k8.js";
13
+ } from "./gbtcc2d7.js";
14
14
  import {
15
15
  OkmError2
16
16
  } from "./52szzakm.js";
@@ -92,6 +92,9 @@ class Frame {
92
92
  }
93
93
  }
94
94
  async function tx(session, args) {
95
+ if (typeof args[0] === "function" && args.length > 1) {
96
+ fail("OKM1120", "options go first: tx(options, fn).");
97
+ }
95
98
  const fn = typeof args[0] === "function" ? args[0] : args[1];
96
99
  const given = typeof args[0] === "function" ? {} : args[0] ?? {};
97
100
  try {
@@ -312,8 +315,16 @@ function held(session, frame, after, depth) {
312
315
  };
313
316
  const pool = depth === 0 ? {
314
317
  capabilities: base.capabilities,
315
- execute: (text, params, options) => guarded(() => frame.conn.execute(text, params, stoppable(frame, options)), false),
316
- batch: (statements, options) => guarded(() => frame.conn.batch(statements, stoppable(frame, options)), true),
318
+ execute: (text, params, options) => {
319
+ if (options?.route === "replica")
320
+ replicaInside();
321
+ return guarded(() => frame.conn.execute(text, params, stoppable(frame, options)), false);
322
+ },
323
+ batch: (statements, options) => {
324
+ if (options?.route === "replica")
325
+ replicaInside();
326
+ return guarded(() => frame.conn.batch(statements, stoppable(frame, options)), true);
327
+ },
317
328
  stats: () => base.stats(),
318
329
  close: () => Promise.resolve()
319
330
  } : base;
@@ -364,13 +375,22 @@ function lockKey(key) {
364
375
  return ["select pg_advisory_xact_lock($1::bigint)", String(key)];
365
376
  }
366
377
  function callOf(options) {
367
- if (options?.signal === undefined && options?.timeout === undefined)
378
+ if (options?.signal === undefined && options?.timeout === undefined && options?.route === undefined) {
368
379
  return;
380
+ }
369
381
  return {
370
- ...options.signal !== undefined ? { signal: options.signal } : {},
371
- ...options.timeout !== undefined ? { timeout: options.timeout } : {}
382
+ ...options?.signal !== undefined ? { signal: options.signal } : {},
383
+ ...options?.timeout !== undefined ? { timeout: options.timeout } : {},
384
+ ...options?.route !== undefined ? { route: options.route } : {}
372
385
  };
373
386
  }
387
+ function replicaInside() {
388
+ throw new OkmError2("OKM1840", "This operation needs the primary.", {
389
+ fix: {
390
+ summary: "Run the operation on the primary. Writes, batch, locks, and tx() never go to a replica."
391
+ }
392
+ });
393
+ }
374
394
  export {
375
395
  tx
376
396
  };
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  send,
3
3
  connectClient
4
- } from "./6p5k20sm.js";
4
+ } from "./96qcj21c.js";
5
5
  import {
6
6
  isConnectionLost
7
- } from "./tbszx8mz.js";
7
+ } from "./pf0d8f77.js";
8
8
  import {
9
9
  WireSession
10
- } from "./zvbjc949.js";
10
+ } from "./4az561fn.js";
11
11
  import {
12
12
  rejectClosed,
13
13
  runCall
14
- } from "./rrstp3kb.js";
14
+ } from "./cgbamx3e.js";
15
15
 
16
16
  // src/adapters/pg/nodepostgres-extra.ts
17
17
  var RESET_ALL = "RESET ALL";
@@ -43,7 +43,7 @@ async function checkout(held) {
43
43
  return {
44
44
  execute: (text, params, options) => guard(() => runCall(isClosed(), options, (watch) => session.query(text, params, watch))),
45
45
  batch: (statements, options) => guard(() => runCall(isClosed(), options, async (watch) => {
46
- const { runAtomicBatch } = await import("./b066g80h.js");
46
+ const { runAtomicBatch } = await import("./efmy2gcg.js");
47
47
  return runAtomicBatch(session, statements, watch);
48
48
  })),
49
49
  cancel() {
@@ -1,12 +1,13 @@
1
- import"./8jb4ymh2.js";
1
+ import"./8bwbwy8h.js";
2
2
  import"./n442h54c.js";
3
- import"./wqem4jed.js";
3
+ import"./krxx77g0.js";
4
4
  import {
5
- listTargets,
6
5
  loadConfig
7
- } from "./q6zsyh31.js";
8
- import"./n5y9zyj1.js";
9
- import"./41kv2xdm.js";
6
+ } from "./qv2ydck9.js";
7
+ import {
8
+ listTargets
9
+ } from "./ga3sfy93.js";
10
+ import"./8xsbagad.js";
10
11
 
11
12
  // src/tooling/migrate/dev.ts
12
13
  import { mkdirSync } from "node:fs";
@@ -19,7 +20,7 @@ async function devProject(cwd) {
19
20
  `;
20
21
  const directory = join(cwd, config.out ?? ".okm", "dev-db");
21
22
  mkdirSync(directory, { recursive: true });
22
- const { open } = await import("./gdh9mrtp.js");
23
+ const { open } = await import("./f5cw5q5r.js");
23
24
  const pool = await open({ dataDir: directory });
24
25
  try {
25
26
  await pool.execute("select 1");
@@ -0,0 +1,289 @@
1
+ import {
2
+ cellText,
3
+ VIEW_DEPENDENCIES,
4
+ viewDependencyEdges,
5
+ PUBLIC,
6
+ introspectSchema
7
+ } from "./fancm5pt.js";
8
+ import {
9
+ planMigration,
10
+ assertDirectConnection,
11
+ assertStableBackend
12
+ } from "./ga3sfy93.js";
13
+ import {
14
+ quoteIdent,
15
+ createObjectSql
16
+ } from "./8xsbagad.js";
17
+ import {
18
+ normaliseViewQuery,
19
+ viewObject,
20
+ materializedViewObject
21
+ } from "./y005b761.js";
22
+ import {
23
+ catalog
24
+ } from "./g7ez901n.js";
25
+ import {
26
+ OkmError2
27
+ } from "./52szzakm.js";
28
+
29
+ // src/tooling/migrate/drift.ts
30
+ import postgres from "postgres";
31
+
32
+ // src/dialects/pg/view/scratch.ts
33
+ var SCRATCH = "okm_view_seal";
34
+ var VIEW_SHAPE = `
35
+ select c.relname as name, c.relkind as kind, pg_get_viewdef(c.oid, true) as query,
36
+ (
37
+ select coalesce(string_agg(
38
+ a.attname || '|' || format_type(a.atttypid, a.atttypmod),
39
+ E'\\n' order by a.attnum
40
+ ), '')
41
+ from pg_attribute a
42
+ where a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped
43
+ ) as columns
44
+ from pg_class c
45
+ join pg_namespace n on n.oid = c.relnamespace
46
+ where n.nspname = $1 and c.relkind in ('v', 'm')
47
+ `;
48
+ async function sealViews(runner, source) {
49
+ const views = source.objects.filter((object) => object.kind === "view" || object.kind === "materializedView");
50
+ if (views.length === 0)
51
+ return source;
52
+ await dropScratch(runner);
53
+ await runner.query(`create schema ${quoteIdent(SCRATCH)}`);
54
+ const previous = await runner.query(`select current_setting('search_path') as path`);
55
+ const saved = cellText(previous[0]?.path);
56
+ const path = await scratchSearchPath(runner, source);
57
+ try {
58
+ await runner.query(`select set_config('search_path', $1, false)`, [path]);
59
+ await createObjects(runner, source, "type");
60
+ for (const object of source.objects) {
61
+ if (object.kind !== "table")
62
+ continue;
63
+ await runner.query(stubTable(object, source));
64
+ }
65
+ await createObjects(runner, source, "function");
66
+ await createViews(runner, views);
67
+ const shapes = await runner.query(VIEW_SHAPE, [SCRATCH]);
68
+ const deps = await runner.query(VIEW_DEPENDENCIES, [SCRATCH]);
69
+ const edges = viewDependencyEdges(deps, PUBLIC, source.objects);
70
+ const byName = new Map(shapes.map((row) => [cellText(row.name), row]));
71
+ const next = source.objects.map((object) => {
72
+ if (object.kind !== "view" && object.kind !== "materializedView")
73
+ return object;
74
+ const row = byName.get(object.identity.name);
75
+ if (row === undefined) {
76
+ throw new OkmError2("OKM1020", `View ${object.identity.name} was not created in the scratch schema.`, {
77
+ fix: { summary: "The query has to run against the tables in this schema." }
78
+ });
79
+ }
80
+ const columns = readColumns(object, cellText(row.columns));
81
+ const query = normaliseViewQuery(cellText(row.query));
82
+ const dependencies = edges.get(object.identity.name) ?? [];
83
+ if (object.kind === "view") {
84
+ return viewObject({
85
+ namespace: object.identity.namespace,
86
+ name: object.identity.name,
87
+ columns,
88
+ query,
89
+ owner: object.owner,
90
+ provenance: object.provenance,
91
+ dependencies
92
+ });
93
+ }
94
+ return materializedViewObject({
95
+ namespace: object.identity.namespace,
96
+ name: object.identity.name,
97
+ columns,
98
+ query,
99
+ owner: object.owner,
100
+ provenance: object.provenance,
101
+ dependencies,
102
+ ...object.definition.refresh !== undefined ? { refresh: object.definition.refresh } : {}
103
+ });
104
+ });
105
+ return catalog(next);
106
+ } finally {
107
+ await runner.query(`select set_config('search_path', $1, false)`, [saved]).catch(() => {
108
+ return;
109
+ });
110
+ await dropScratch(runner);
111
+ }
112
+ }
113
+ async function scratchSearchPath(runner, source) {
114
+ const names = new Set;
115
+ for (const object of source.objects) {
116
+ if (object.kind !== "column")
117
+ continue;
118
+ const bare = object.definition.dataType.split(".").pop()?.replace(/\(.*$/, "").replace(/\[\]$/, "").trim().toLowerCase();
119
+ if (bare !== undefined && bare.length > 0)
120
+ names.add(bare);
121
+ }
122
+ const extra = [];
123
+ if (names.size > 0) {
124
+ const rows = await runner.query(`select distinct n.nspname as schema
125
+ from pg_type t
126
+ join pg_namespace n on n.oid = t.typnamespace
127
+ where t.typname = any(string_to_array($1, E'\\x1f'))
128
+ and n.nspname not in ('pg_catalog', 'information_schema', 'pg_toast')`, [[...names].join("\x1F")]);
129
+ for (const row of rows) {
130
+ const schema = cellText(row.schema);
131
+ if (schema.length > 0 && schema !== "public" && schema !== SCRATCH)
132
+ extra.push(schema);
133
+ }
134
+ }
135
+ extra.sort();
136
+ return [SCRATCH, ...extra, "public"].map((name) => quoteIdent(name)).join(", ");
137
+ }
138
+ async function createObjects(runner, source, kind) {
139
+ for (const object of source.objects) {
140
+ if (object.kind !== kind)
141
+ continue;
142
+ const statement = createObjectSql(object, SCRATCH);
143
+ if (statement !== undefined)
144
+ await runner.query(statement);
145
+ }
146
+ }
147
+ async function createViews(runner, views) {
148
+ const pending = [...views];
149
+ let last = "";
150
+ for (let pass = 0;pass < views.length && pending.length > 0; pass += 1) {
151
+ const failed = [];
152
+ let created = 0;
153
+ for (const object of pending) {
154
+ const sql = object.kind === "materializedView" ? `create materialized view ${quoteIdent(object.identity.name)} as ${object.definition.query} with no data` : `create view ${quoteIdent(object.identity.name)} as ${object.definition.query}`;
155
+ try {
156
+ await runner.query(sql);
157
+ created += 1;
158
+ } catch (error) {
159
+ last = error instanceof Error ? error.message : String(error);
160
+ failed.push(object);
161
+ }
162
+ }
163
+ if (created === 0) {
164
+ throw new OkmError2("OKM1020", `View ${failed[0]?.identity.name ?? ""} could not be created. ${last}`, {
165
+ fix: { summary: "The query has to run against the tables in this schema." }
166
+ });
167
+ }
168
+ pending.length = 0;
169
+ pending.push(...failed);
170
+ }
171
+ }
172
+ function stubTable(object, source) {
173
+ const columns = source.objects.filter((item) => item.kind === "column" && item.identity.parent.name === object.identity.name);
174
+ const lines = columns.map((column) => `${quoteIdent(column.identity.name)} ${column.definition.dataType}`);
175
+ return `create table ${quoteIdent(object.identity.name)} (${lines.join(", ")})`;
176
+ }
177
+ function readColumns(object, printed) {
178
+ const live = printed.length === 0 ? [] : printed.split(`
179
+ `);
180
+ const declared = object.definition.columns;
181
+ if (live.length !== declared.length) {
182
+ throw mismatch(object.identity.name, declared.map((column) => column.name).join(", "), printed);
183
+ }
184
+ return declared.map((column, index) => {
185
+ const line = live[index] ?? "";
186
+ const splitAt = line.indexOf("|");
187
+ const name = splitAt < 0 ? line : line.slice(0, splitAt);
188
+ const type = splitAt < 0 ? "" : line.slice(splitAt + 1);
189
+ if (name !== column.name || type !== column.dataType) {
190
+ throw mismatch(object.identity.name, `${column.name}:${column.dataType}`, live[index] ?? "");
191
+ }
192
+ return column;
193
+ });
194
+ }
195
+ function mismatch(view, declared, live) {
196
+ return new OkmError2("OKM1020", `View ${view} declares ${declared} and the database has ${live}.`, { fix: { summary: "Declare the columns the query returns, in that order, with those types." } });
197
+ }
198
+ async function dropScratch(runner) {
199
+ const present = await runner.query(`select 1 as ok from pg_namespace where nspname = $1`, [
200
+ SCRATCH
201
+ ]);
202
+ if (present.length === 0)
203
+ return;
204
+ const rows = await runner.query(`
205
+ select c.relname as name, c.relkind as kind
206
+ from pg_class c
207
+ join pg_namespace n on n.oid = c.relnamespace
208
+ where n.nspname = $1 and c.relkind in ('v', 'm', 'r')
209
+ `, [SCRATCH]);
210
+ const views = rows.filter((row) => cellText(row.kind) === "v" || cellText(row.kind) === "m");
211
+ for (let pass = 0;pass < Math.max(views.length, 1); pass += 1) {
212
+ for (const row of views) {
213
+ const kind = cellText(row.kind) === "m" ? "materialized view" : "view";
214
+ try {
215
+ await runner.query(`drop ${kind} if exists ${quoteIdent(SCRATCH)}.${quoteIdent(cellText(row.name))}`);
216
+ } catch {}
217
+ }
218
+ }
219
+ for (const row of rows) {
220
+ if (cellText(row.kind) !== "r")
221
+ continue;
222
+ await runner.query(`drop table if exists ${quoteIdent(SCRATCH)}.${quoteIdent(cellText(row.name))}`);
223
+ }
224
+ await runner.query(`drop schema if exists ${quoteIdent(SCRATCH)} cascade`);
225
+ }
226
+
227
+ // src/tooling/migrate/drift.ts
228
+ async function assertAuthorDrift2(url, author, roles, allowPooler = false) {
229
+ assertDirectConnection(url, allowPooler);
230
+ const sql = postgres(url, {
231
+ max: 1,
232
+ connect_timeout: 5,
233
+ idle_timeout: 1,
234
+ onnotice: () => {}
235
+ });
236
+ try {
237
+ await assertStableBackend(async () => {
238
+ const rows = await sql`select pg_backend_pid()::text as pid`;
239
+ return rows[0]?.pid ?? "";
240
+ }, allowPooler);
241
+ const present = await sql`
242
+ select to_regclass('public.okm_meta')::text as reg
243
+ `;
244
+ if (present[0]?.reg == null)
245
+ return;
246
+ const runner = catalogQuery2(sql);
247
+ const authorOnServer = await sealViews(runner, author);
248
+ const live = await introspectSchema(runner, "public", "public", managedRoleOptions2(roles));
249
+ const plan = planMigration({ before: live, after: authorOnServer, name: "check" });
250
+ if (plan.steps.length === 0)
251
+ return;
252
+ throw new OkmError2("OKM1520", `The database differs from the schema.
253
+ ${plan.steps.map((step) => step.sql).join(`
254
+ `)}`, {
255
+ fix: {
256
+ summary: "Change the schema or the database so okm check reports no difference."
257
+ }
258
+ });
259
+ } finally {
260
+ await sql.end({ timeout: 5 });
261
+ }
262
+ }
263
+ function catalogQuery2(sql) {
264
+ return {
265
+ async query(text, params) {
266
+ const rows = await sql.unsafe(text, params === undefined ? undefined : [...params]);
267
+ return rows.map((row) => {
268
+ const copy = {};
269
+ for (const [key, value] of Object.entries(row))
270
+ copy[key] = value;
271
+ return copy;
272
+ });
273
+ }
274
+ };
275
+ }
276
+ function managedRoleOptions2(roles) {
277
+ const names = managedRoleNames(roles);
278
+ return names === undefined ? undefined : { managedRoles: names };
279
+ }
280
+ function managedRoleNames(roles) {
281
+ if (roles === undefined)
282
+ return;
283
+ const names = new Set([roles.migration, roles.app]);
284
+ for (const role of roles.managed ?? [])
285
+ names.add(role.name);
286
+ return [...names];
287
+ }
288
+
289
+ export { sealViews, assertAuthorDrift2, catalogQuery2, managedRoleOptions2 };
@@ -1,3 +1,6 @@
1
+ import {
2
+ FUNCTION_ARG_TYPES
3
+ } from "./fancm5pt.js";
1
4
  import {
2
5
  OkmError2
3
6
  } from "./52szzakm.js";
@@ -92,7 +95,7 @@ async function functionExists(runner, schema, name, args) {
92
95
  const result = await runner.execute(`select 1 from pg_proc p
93
96
  join pg_namespace n on n.oid = p.pronamespace
94
97
  where n.nspname = $1 and p.proname = $2
95
- and replace(pg_get_function_identity_arguments(p.oid), ' ', '') = $3`, [schema, name, args]);
98
+ and ${FUNCTION_ARG_TYPES} = $3`, [schema, name, args]);
96
99
  return result.rows.length > 0;
97
100
  }
98
101
  function truthy(value) {
@@ -3,10 +3,10 @@ import {
3
3
  readHandle,
4
4
  readCall,
5
5
  attachHttp
6
- } from "./kkzwp3d2.js";
6
+ } from "./hf22e0wt.js";
7
7
  import {
8
8
  fail
9
- } from "./xrfnq5k8.js";
9
+ } from "./gbtcc2d7.js";
10
10
  import {
11
11
  OkmError2,
12
12
  throwNamed2
@@ -27,6 +27,14 @@ function build(session, table, options, mods, view) {
27
27
  }
28
28
  function plan(session, table, options, mods, view) {
29
29
  const { lock, wait, ...rest } = options;
30
+ const forced = session.pool.forcedRoute;
31
+ if (rest.route === "replica" || forced === "replica") {
32
+ throw new OkmError2("OKM1840", "A locking read needs the primary.", {
33
+ fix: {
34
+ summary: "Run the operation on the primary. Writes, batch, locks, and tx() never go to a replica."
35
+ }
36
+ });
37
+ }
30
38
  if (session.tx === undefined) {
31
39
  throw new OkmError2("OKM1830", "A row lock needs a transaction. find was called outside tx().", {
32
40
  fix: { summary: "Move the find into tx(): the lock lasts until the transaction ends." }
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  renderCatalog
3
- } from "./41kv2xdm.js";
3
+ } from "./8xsbagad.js";
4
4
  import {
5
5
  createClient
6
- } from "./kkzwp3d2.js";
6
+ } from "./hf22e0wt.js";
7
7
  import {
8
8
  OkmError2
9
9
  } from "./52szzakm.js";
@@ -12,10 +12,15 @@ import {
12
12
  var tenantProbe = "00000000-0000-4000-8000-000000000001";
13
13
  function readFacts(schema) {
14
14
  const foreign = foreignKeys(schema.catalog);
15
+ const tables = new Set;
16
+ for (const object of schema.catalog.objects) {
17
+ if (object.kind === "table")
18
+ tables.add(object.identity.name);
19
+ }
15
20
  const facts = [];
16
21
  for (const name of Object.keys(schema.model)) {
17
22
  const model = schema.model[name];
18
- if (model === undefined)
23
+ if (model === undefined || !tables.has(model.sql))
19
24
  continue;
20
25
  const keys = foreign.get(model.sql) ?? [];
21
26
  facts.push({
@@ -458,6 +463,9 @@ function fieldOf(facts, sql) {
458
463
  function cacheKey(table, tenant) {
459
464
  return `${tenant ?? "*"}:${table}`;
460
465
  }
466
+ function forgetCached(host, table, tenant) {
467
+ host.cache.delete(cacheKey(table, tenant));
468
+ }
461
469
  function refOf(value) {
462
470
  if (typeof value !== "object" || value === null || !(REF in value))
463
471
  return;
@@ -497,14 +505,14 @@ function isRow(value) {
497
505
  }
498
506
 
499
507
  // src/tooling/testing/isolation.ts
500
- var TENANT_A = "00000000-0000-4000-8000-00000000000a";
501
- var TENANT_B = "00000000-0000-4000-8000-00000000000b";
502
508
  async function isolation(host) {
503
509
  const saved = host.cache;
504
510
  host.cache = new Map;
505
511
  try {
506
512
  const checked = [];
507
513
  const skipped = [];
514
+ const tenantA = crypto.randomUUID();
515
+ const tenantB = crypto.randomUUID();
508
516
  for (const facts of host.facts.values()) {
509
517
  if (facts.globalReason !== undefined) {
510
518
  skipped.push({ table: facts.name, reason: facts.globalReason });
@@ -512,7 +520,7 @@ async function isolation(host) {
512
520
  }
513
521
  if (!facts.tenant)
514
522
  continue;
515
- await checkTable(host, facts);
523
+ await checkTable(host, facts, tenantA, tenantB);
516
524
  checked.push(facts.name);
517
525
  }
518
526
  return { checked, skipped };
@@ -520,25 +528,68 @@ async function isolation(host) {
520
528
  host.cache = saved;
521
529
  }
522
530
  }
523
- async function checkTable(host, facts) {
531
+ async function checkTable(host, facts, tenantA, tenantB) {
524
532
  const key = host.schema.tenancy?.key;
525
533
  if (key === undefined)
526
534
  return;
527
- const row = await insertRow(host, facts.name, { [key]: TENANT_B }, undefined, TENANT_B);
535
+ if (host.definitions.size > 0 && !host.definitions.has(facts.name)) {
536
+ throw new OkmError2("invalid", `Factory ${facts.name} is missing.`, {
537
+ fix: { summary: `Add ${facts.name} to factories().` }
538
+ });
539
+ }
540
+ const row = await insertRow(host, facts.name, { [key]: tenantB }, undefined, tenantB);
528
541
  const where = primaryWhere(facts, row, key);
529
- const client = openTenant(host, TENANT_A);
542
+ const client = openTenant(host, tenantA);
530
543
  const handle = tableHandle2(client, facts.name);
531
- const found = await handle.find({ where, limit: 1 });
532
- const rows = asRows(found);
544
+ try {
545
+ await readTenant(facts, handle, where);
546
+ const set = updateSet(host, facts, row);
547
+ if (set !== undefined) {
548
+ const updated = asCount(await handle.update({ where, set }));
549
+ if (updated > 0)
550
+ leak(facts.name, "update", { count: updated });
551
+ if (typeof client.batch === "function") {
552
+ const batched = await client.batch([handle.update({ where, set })]);
553
+ if (Array.isArray(batched) && batched.some((item) => asCount(item) > 0)) {
554
+ leak(facts.name, "batch", batched);
555
+ }
556
+ }
557
+ }
558
+ const removed = asCount(await handle.delete({ where }));
559
+ if (removed > 0)
560
+ leak(facts.name, "delete", { count: removed });
561
+ } finally {
562
+ const owner = tableHandle2(openTenant(host, tenantB), facts.name);
563
+ await owner.delete({ where }).catch(() => {
564
+ return;
565
+ });
566
+ forgetCached(host, facts.name, tenantB);
567
+ }
568
+ }
569
+ async function readTenant(facts, handle, where) {
570
+ const query = handle.find({ where, limit: 1 });
571
+ const rows = asRows(await query);
533
572
  if (rows.length > 0)
534
573
  leak(facts.name, "find", rows[0]);
574
+ const streamed = handle.find({ where, limit: 1 });
575
+ if (typeof streamed.stream === "function") {
576
+ try {
577
+ for await (const item of streamed.stream()) {
578
+ if (isRow2(item))
579
+ leak(facts.name, "stream", item);
580
+ }
581
+ } catch (error) {
582
+ if (!(error instanceof OkmError2) || error.code !== "OKM1111")
583
+ throw error;
584
+ }
585
+ }
535
586
  const one = await handle.one({ where });
536
587
  if (isRow2(one))
537
588
  leak(facts.name, "one", one);
538
- const counted = asCount(await handle.count());
589
+ const counted = asCount(await handle.count({ where }));
539
590
  if (counted > 0)
540
591
  leak(facts.name, "count", { count: counted });
541
- const exists = await handle.exists();
592
+ const exists = await handle.exists({ where });
542
593
  if (exists === true)
543
594
  leak(facts.name, "exists", { exists: true });
544
595
  const relation = facts.relations[0];
@@ -547,15 +598,28 @@ async function checkTable(host, facts) {
547
598
  if (isRow2(included))
548
599
  leak(facts.name, "include", included);
549
600
  }
550
- const set = updateSet(host, facts, row);
551
- if (set !== undefined) {
552
- const updated = asCount(await handle.update({ where, set }));
553
- if (updated > 0)
554
- leak(facts.name, "update", { count: updated });
601
+ if (typeof handle.aggregate === "function") {
602
+ const aggregated = await handle.aggregate({ where, count: true });
603
+ if (Array.isArray(aggregated)) {
604
+ for (const item of aggregated) {
605
+ if (isRow2(item) && asCount(item) > 0)
606
+ leak(facts.name, "aggregate", item);
607
+ }
608
+ }
609
+ }
610
+ if (typeof handle.page === "function") {
611
+ const orderField = Object.keys(where)[0];
612
+ if (orderField !== undefined) {
613
+ const paged = await handle.page({
614
+ where,
615
+ orderBy: { [orderField]: "asc" },
616
+ limit: 5
617
+ });
618
+ if (isRow2(paged) && Array.isArray(paged.items) && paged.items.length > 0) {
619
+ leak(facts.name, "page", paged.items[0]);
620
+ }
621
+ }
555
622
  }
556
- const deleted = asCount(await handle.delete({ where }));
557
- if (deleted > 0)
558
- leak(facts.name, "delete", { count: deleted });
559
623
  }
560
624
  function includeOf(relation) {
561
625
  if (relation.kind === "many")