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.
- package/README.md +95 -12
- package/dist/contracts/catalog/document.d.ts +20 -0
- package/dist/contracts/driver.d.ts +7 -0
- package/dist/dialects/pg/filter-parse.d.ts +8 -2
- package/dist/dialects/pg/fn/index.js +8 -0
- package/dist/dialects/pg/index.js +8 -2
- package/dist/dialects/pg/introspect.d.ts +6 -0
- package/dist/dialects/pg/ops/or.d.ts +1 -1
- package/dist/dialects/pg/role/sql.d.ts +10 -0
- package/dist/dialects/pg/schema.d.ts +15 -6
- package/dist/dialects/pg/temporal.d.ts +5 -76
- package/dist/dialects/pg/view/index.d.ts +20 -6
- package/dist/dialects/pg/view/index.js +1 -1
- package/dist/dialects/pg/view/scratch.d.ts +2 -2
- package/dist/runtime/client.d.ts +6 -1
- package/dist/runtime/conflict.d.ts +2 -1
- package/dist/runtime/pg/bun.d.ts +12 -7
- package/dist/runtime/pg/bun.js +7 -4
- package/dist/runtime/pg/pg.d.ts +12 -6
- package/dist/runtime/pg/pg.js +7 -4
- package/dist/runtime/pg/pglite.d.ts +11 -3
- package/dist/runtime/pg/pglite.js +7 -4
- package/dist/runtime/pg/postgresjs.d.ts +12 -6
- package/dist/runtime/pg/postgresjs.js +7 -4
- package/dist/runtime/plan.d.ts +16 -1
- package/dist/runtime/tenancy/index.js +1 -1
- package/dist/runtime/topology.d.ts +191 -0
- package/dist/runtime/traits/index.js +7 -7
- package/dist/runtime/types.d.ts +57 -2
- package/dist/runtime/validate/index.js +3 -3
- package/dist/shared/{radd3z4g.js → 0t22wwh1.js} +5 -9
- package/dist/shared/{ady1ms74.js → 2pqx26h8.js} +25 -10
- package/dist/shared/{kyxb62mc.js → 31vm914h.js} +9 -1
- package/dist/shared/{jns2wx4t.js → 32zqc1dw.js} +18 -7
- package/dist/shared/{nxb42q7h.js → 47x8x9ze.js} +2 -2
- package/dist/shared/{zvbjc949.js → 4az561fn.js} +1 -1
- package/dist/shared/{s6gqfn5j.js → 51mvgj8w.js} +27 -3
- package/dist/shared/{k3ted12y.js → 54tqkxja.js} +20 -2
- package/dist/shared/{3p4b1fg0.js → 5enne4ma.js} +1 -1
- package/dist/shared/{6693j9rs.js → 5fqx9yh4.js} +6 -2
- package/dist/shared/{c5g81ngb.js → 6mnp99hx.js} +11 -8
- package/dist/shared/{hqjsxnz7.js → 6qd27mx0.js} +5 -4
- package/dist/shared/{v7tpnhnz.js → 6rk2gen5.js} +5 -3
- package/dist/shared/{2321avmn.js → 7zwwyh0v.js} +46 -16
- package/dist/shared/{fa9vt245.js → 87jvvhnt.js} +2 -2
- package/dist/shared/{8jb4ymh2.js → 8bwbwy8h.js} +12 -1
- package/dist/shared/{c8aghh05.js → 8rsy3hxz.js} +7 -1
- package/dist/shared/{41kv2xdm.js → 8xsbagad.js} +1 -1
- package/dist/shared/{6p5k20sm.js → 96qcj21c.js} +3 -3
- package/dist/shared/{jgxzjzcz.js → aj2qxyhj.js} +2 -2
- package/dist/shared/{5vdm4sgp.js → caw8b9d8.js} +4 -4
- package/dist/shared/{s4mb6a4b.js → cctw17hj.js} +74 -15
- package/dist/shared/{rrstp3kb.js → cgbamx3e.js} +2 -2
- package/dist/shared/{axhk06q1.js → cr74e96e.js} +1 -1
- package/dist/shared/{b066g80h.js → efmy2gcg.js} +2 -2
- package/dist/shared/{gdh9mrtp.js → f5cw5q5r.js} +2 -2
- package/dist/shared/{xghkt7a9.js → fancm5pt.js} +6 -233
- package/dist/shared/{n5y9zyj1.js → ga3sfy93.js} +369 -14
- package/dist/shared/{xrfnq5k8.js → gbtcc2d7.js} +55 -5
- package/dist/shared/{7nk68fj8.js → gde73es6.js} +20 -15
- package/dist/shared/{kkzwp3d2.js → hf22e0wt.js} +55 -19
- package/dist/shared/{tny2t3sm.js → janx09m5.js} +8 -7
- package/dist/shared/{4ef7mmqf.js → jwtxdj06.js} +5 -5
- package/dist/shared/{sf8m6tfy.js → k8v5v5wj.js} +3 -3
- package/dist/shared/{qn0srwwq.js → kfjp0gck.js} +5 -3
- package/dist/shared/{wqem4jed.js → krxx77g0.js} +7 -7
- package/dist/shared/{edwqqryd.js → nkaxvt9q.js} +1 -1
- package/dist/shared/{jsd07prz.js → ntneex2a.js} +1 -1
- package/dist/shared/nxdp2ze3.js +1197 -0
- package/dist/shared/{tbszx8mz.js → pf0d8f77.js} +1 -1
- package/dist/shared/{84ptn35q.js → ps7a6h14.js} +10 -9
- package/dist/shared/{kj680nxv.js → q58y17hy.js} +9 -7
- package/dist/shared/{awrsqrn7.js → qm7cdtvr.js} +22 -9
- package/dist/shared/{q6zsyh31.js → qv2ydck9.js} +156 -205
- package/dist/shared/{kafaa5zv.js → r2sp8hrc.js} +28 -8
- package/dist/shared/{fb00y8fp.js → r8x5qhd7.js} +5 -5
- package/dist/shared/{yjw02enp.js → sdc7r2ww.js} +8 -7
- package/dist/shared/sycnx8gt.js +289 -0
- package/dist/shared/{hcmdtwgh.js → szm0cksa.js} +4 -1
- package/dist/shared/{zg24x0hm.js → tm5ab8we.js} +10 -2
- package/dist/shared/{ygem90rn.js → v36c5g6p.js} +85 -21
- package/dist/shared/wq9n6pwr.js +139 -0
- package/dist/shared/{7p2wbz7f.js → x72jb7ad.js} +1 -1
- package/dist/shared/{q795afwa.js → z2rnngj4.js} +3 -3
- package/dist/shared/{sr5ms7b3.js → zqrnhwsq.js} +3 -3
- package/dist/tooling/cli.js +70 -23
- package/dist/tooling/migrate/apply.d.ts +2 -0
- package/dist/tooling/migrate/commands.d.ts +1 -2
- package/dist/tooling/migrate/drift.d.ts +1 -1
- package/dist/tooling/migrate/estimate.d.ts +14 -1
- package/dist/tooling/migrate/files.d.ts +7 -0
- package/dist/tooling/migrate/index.js +12 -9
- package/dist/tooling/migrate/lint.d.ts +3 -3
- package/dist/tooling/migrate/plan.d.ts +7 -3
- package/dist/tooling/migrate/policy.d.ts +11 -0
- package/dist/tooling/migrate/sql-lex.d.ts +22 -0
- package/dist/tooling/testing/factories.d.ts +10 -0
- package/dist/tooling/testing/facts.d.ts +3 -0
- package/dist/tooling/testing/index.js +3 -3
- package/dist/tooling/testing/isolation.d.ts +4 -3
- package/package.json +6 -5
- package/dist/shared/0v1g424p.js +0 -26
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isConnectionLost
|
|
3
|
-
} from "./
|
|
3
|
+
} from "./pf0d8f77.js";
|
|
4
4
|
import {
|
|
5
5
|
openClient,
|
|
6
6
|
withHttp,
|
|
7
7
|
settleCall
|
|
8
|
-
} from "./
|
|
8
|
+
} from "./hf22e0wt.js";
|
|
9
9
|
import {
|
|
10
10
|
rejectKeys,
|
|
11
11
|
isRecord,
|
|
12
12
|
fail
|
|
13
|
-
} from "./
|
|
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) =>
|
|
316
|
-
|
|
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
|
|
371
|
-
...options
|
|
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 "./
|
|
4
|
+
} from "./96qcj21c.js";
|
|
5
5
|
import {
|
|
6
6
|
isConnectionLost
|
|
7
|
-
} from "./
|
|
7
|
+
} from "./pf0d8f77.js";
|
|
8
8
|
import {
|
|
9
9
|
WireSession
|
|
10
|
-
} from "./
|
|
10
|
+
} from "./4az561fn.js";
|
|
11
11
|
import {
|
|
12
12
|
rejectClosed,
|
|
13
13
|
runCall
|
|
14
|
-
} from "./
|
|
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("./
|
|
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"./
|
|
1
|
+
import"./8bwbwy8h.js";
|
|
2
2
|
import"./n442h54c.js";
|
|
3
|
-
import"./
|
|
3
|
+
import"./krxx77g0.js";
|
|
4
4
|
import {
|
|
5
|
-
listTargets,
|
|
6
5
|
loadConfig
|
|
7
|
-
} from "./
|
|
8
|
-
import
|
|
9
|
-
|
|
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("./
|
|
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
|
|
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 "./
|
|
6
|
+
} from "./hf22e0wt.js";
|
|
7
7
|
import {
|
|
8
8
|
fail
|
|
9
|
-
} from "./
|
|
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 "./
|
|
3
|
+
} from "./8xsbagad.js";
|
|
4
4
|
import {
|
|
5
5
|
createClient
|
|
6
|
-
} from "./
|
|
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
|
-
|
|
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,
|
|
542
|
+
const client = openTenant(host, tenantA);
|
|
530
543
|
const handle = tableHandle2(client, facts.name);
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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")
|