okmodel 0.8.0 → 0.9.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.
- package/README.md +14 -0
- package/dist/dialects/pg/ext/btree-gin.d.ts +26 -0
- package/dist/dialects/pg/ext/btree-gin.js +20 -0
- package/dist/dialects/pg/ext/btree-gist.d.ts +26 -0
- package/dist/dialects/pg/ext/btree-gist.js +20 -0
- package/dist/dialects/pg/ext/call.d.ts +53 -0
- package/dist/dialects/pg/ext/citext.js +1 -1
- package/dist/dialects/pg/ext/feature.d.ts +26 -0
- package/dist/dialects/pg/ext/fuzzystrmatch.d.ts +34 -0
- package/dist/dialects/pg/ext/fuzzystrmatch.js +33 -0
- package/dist/dialects/pg/ext/hstore.d.ts +41 -0
- package/dist/dialects/pg/ext/hstore.js +29 -0
- package/dist/dialects/pg/ext/index.d.ts +56 -0
- package/dist/dialects/pg/ext/index.js +1 -1
- package/dist/dialects/pg/ext/ltree.d.ts +41 -0
- package/dist/dialects/pg/ext/ltree.js +29 -0
- package/dist/dialects/pg/ext/pg-trgm.js +4 -13
- package/dist/dialects/pg/ext/pgcrypto.d.ts +33 -0
- package/dist/dialects/pg/ext/pgcrypto.js +33 -0
- package/dist/dialects/pg/ext/unaccent.d.ts +26 -0
- package/dist/dialects/pg/ext/unaccent.js +21 -0
- package/dist/dialects/pg/ext/using.d.ts +23 -0
- package/dist/dialects/pg/ext/versions.d.ts +36 -0
- package/dist/dialects/pg/external.d.ts +19 -0
- package/dist/dialects/pg/fn/index.js +4 -4
- package/dist/dialects/pg/index.d.ts +1 -0
- package/dist/dialects/pg/index.js +16 -11
- package/dist/dialects/pg/view/index.js +5 -5
- package/dist/dialects/pg/view/scratch.d.ts +4 -1
- package/dist/runtime/artifact-file.d.ts +22 -0
- package/dist/runtime/client.d.ts +10 -0
- package/dist/runtime/drift-detail.d.ts +2 -1
- package/dist/runtime/drift.d.ts +3 -10
- package/dist/runtime/pg/bun.js +4 -4
- package/dist/runtime/pg/neon.js +4 -4
- package/dist/runtime/pg/pg.js +4 -4
- package/dist/runtime/pg/pglite.js +4 -4
- package/dist/runtime/pg/postgresjs.js +4 -4
- package/dist/runtime/server-check.d.ts +36 -6
- package/dist/runtime/traits/index.js +5 -5
- package/dist/runtime/validate/index.js +1 -1
- package/dist/shared/{4m6hskg8.js → 0j6qe91y.js} +4 -4
- package/dist/shared/{14x60fbf.js → 1mxm04fy.js} +5 -5
- package/dist/shared/{b6d6p0zc.js → 2szgv6yj.js} +5 -5
- package/dist/shared/{f4jy1j7x.js → 2zdxkt7b.js} +909 -100
- package/dist/shared/{mnvjvqyp.js → 3456242y.js} +17 -17
- package/dist/shared/3t6kw83m.js +1 -1
- package/dist/shared/{6m78c8ss.js → 5kyb3tqx.js} +1 -1
- package/dist/shared/61bmrmhr.js +35 -0
- package/dist/shared/{zbqbmmrp.js → 62b603gr.js} +8 -8
- package/dist/shared/{jgqj11w4.js → 6f0z3hzh.js} +4 -0
- package/dist/shared/{dpc2r7rb.js → 6nae0spm.js} +150 -89
- package/dist/shared/{0wrea3jq.js → 71n0jmhc.js} +1 -1
- package/dist/shared/7ba5h6pb.js +17 -0
- package/dist/shared/{k86n8vq3.js → 8jesqsrk.js} +1 -1
- package/dist/shared/{qd9hhd8f.js → 8n688yjg.js} +1 -1
- package/dist/shared/{zgex00fd.js → 8qzkd9f3.js} +5 -5
- package/dist/shared/{mkrn359n.js → 90sg0w5a.js} +159 -31
- package/dist/shared/{7ghwamrf.js → 99qp9vtg.js} +1 -1
- package/dist/shared/{bxas03mq.js → 9dtgn1z2.js} +1 -1
- package/dist/shared/{p5nh3r8q.js → aksb89mn.js} +2 -2
- package/dist/shared/{wcrheftc.js → bphrgwz9.js} +2 -2
- package/dist/shared/{wz2a78mp.js → c3mvnq7a.js} +1 -1
- package/dist/shared/{pan0370z.js → ccsmhv9r.js} +24 -6
- package/dist/shared/{n0jmpgj1.js → cexb7z6b.js} +1 -1
- package/dist/shared/{wvdjznah.js → cf8zdz5p.js} +5 -5
- package/dist/shared/{3449rcae.js → ckz05z15.js} +4 -4
- package/dist/shared/{35q0n9tz.js → dd5pmnky.js} +7 -7
- package/dist/shared/{e8zyaqvj.js → ds229x4r.js} +1 -1
- package/dist/{tooling/shared/yw30vnq2.js → shared/e275q7sd.js} +1 -1
- package/dist/shared/{mfedzr74.js → ebhz8vh6.js} +4 -4
- package/dist/shared/{z581d7ny.js → fdsz3w80.js} +2 -2
- package/dist/shared/{sjk86yrx.js → fk9vef30.js} +84 -73
- package/dist/shared/{needfe47.js → g89fmz5s.js} +1 -1
- package/dist/shared/{m4xff2a6.js → ghqzdp94.js} +2 -2
- package/dist/shared/h9811jjm.js +17 -0
- package/dist/shared/{kj9d8maq.js → jpybhf98.js} +1 -1
- package/dist/shared/kzgrnckd.js +29 -0
- package/dist/shared/{agsfb86s.js → mazpbcdj.js} +4 -4
- package/dist/shared/{195pzzzz.js → mgaq2sd3.js} +10 -7
- package/dist/shared/{5mtf6cj3.js → mvmm3xpb.js} +5 -2
- package/dist/shared/{p3xbdcvt.js → myk8fswx.js} +55 -6
- package/dist/shared/{njga14v4.js → n306gqkd.js} +8 -8
- package/dist/shared/n5whk3mt.js +665 -0
- package/dist/shared/{bah9nbg3.js → npdftanb.js} +12 -20
- package/dist/shared/{16kvjebh.js → pg4ezgyf.js} +7 -26
- package/dist/{tooling/shared/08kemxjd.js → shared/pgsak1yw.js} +1 -1
- package/dist/shared/{jp36k7dp.js → qvwwp1mp.js} +61 -40
- package/dist/shared/rbxt795q.js +1766 -0
- package/dist/shared/{g5yc06ka.js → s7he5e5d.js} +1 -1
- package/dist/shared/{hfqmpasr.js → ss2kv2rs.js} +5 -5
- package/dist/shared/stfa9xcw.js +450 -0
- package/dist/{tooling/shared/rs1jb051.js → shared/tr64w5p0.js} +1 -1
- package/dist/shared/{jebgrkeq.js → tw1nvvfj.js} +2 -2
- package/dist/shared/{v1wad97q.js → wvtjg5m3.js} +1 -1
- package/dist/shared/xqacq37x.js +61 -0
- package/dist/shared/{n05z9nwz.js → xtsydxjg.js} +1 -1
- package/dist/shared/{jx2m62cs.js → xv8qj97g.js} +5 -5
- package/dist/shared/{63f4ksbz.js → xwhdcrm4.js} +1 -1
- package/dist/shared/{hm5f8jby.js → yvabyg9k.js} +6 -6
- package/dist/shared/yybhre6n.js +136 -0
- package/dist/shared/zdqf8k5d.js +8 -0
- package/dist/tooling/cli.js +94 -22
- package/dist/tooling/driver-run.js +15 -332
- package/dist/tooling/github/comment.d.ts +50 -0
- package/dist/tooling/github/post-comment.d.ts +17 -0
- package/dist/tooling/migrate/commands.d.ts +2 -1
- package/dist/tooling/migrate/config.d.ts +13 -0
- package/dist/tooling/migrate/export.d.ts +165 -0
- package/dist/tooling/migrate/ext-surface.d.ts +80 -0
- package/dist/tooling/migrate/ext.d.ts +31 -4
- package/dist/tooling/migrate/import-drizzle.d.ts +40 -0
- package/dist/tooling/migrate/index.d.ts +3 -0
- package/dist/tooling/migrate/index.js +55 -10
- package/dist/tooling/migrate/lint-rules.d.ts +18 -1
- package/dist/tooling/migrate/lint.d.ts +14 -1
- package/dist/tooling/migrate/place.d.ts +18 -0
- package/dist/tooling/migrate/plan-json.d.ts +65 -0
- package/dist/tooling/migrate/project.d.ts +10 -1
- package/dist/tooling/migrate/pull-emit.d.ts +98 -0
- package/dist/tooling/migrate/pull-routines.d.ts +126 -0
- package/dist/tooling/migrate/pull-verify.d.ts +26 -0
- package/dist/tooling/migrate/pull.d.ts +62 -0
- package/dist/tooling/migrate/reprint.d.ts +32 -0
- package/dist/tooling/migrate/sandbox.d.ts +8 -2
- package/dist/tooling/migrate/schema-api.d.ts +43 -0
- package/dist/tooling/migrate/sql-lex.d.ts +18 -0
- package/dist/tooling/shared/{3m1hkg7z.js → 04svnxxj.js} +4 -4
- package/dist/tooling/shared/{ppnnnvj8.js → 0j0jtse1.js} +1 -1
- package/dist/tooling/shared/{z8d7ptht.js → 0r05zhyq.js} +5 -5
- package/dist/tooling/shared/{a6pp9d12.js → 1800eymw.js} +3 -3
- package/dist/tooling/shared/{cmptmb4f.js → 35zemrmf.js} +1 -1
- package/dist/tooling/shared/{w7qe8sj1.js → 3rmaz3pq.js} +1 -1
- package/dist/{shared/q0cqt2sp.js → tooling/shared/3yn79q5m.js} +1 -1
- package/dist/tooling/shared/{2revefqt.js → 57btdkgm.js} +2 -2
- package/dist/tooling/shared/{ma2340rx.js → 5bmksndh.js} +2 -2
- package/dist/tooling/shared/{9xzm7hjg.js → 5p8km5jb.js} +3 -3
- package/dist/tooling/shared/{xhrnjch9.js → 5x3pf400.js} +3 -3
- package/dist/tooling/shared/{wwnekq5n.js → 5zrzffnk.js} +1 -1
- package/dist/tooling/shared/{p7f08ktn.js → 62fxpc1p.js} +7 -7
- package/dist/tooling/shared/{7ae7rjmn.js → 68tf18v4.js} +2 -2
- package/dist/tooling/shared/{rf83gr2d.js → 6vk28rjp.js} +7 -3
- package/dist/tooling/shared/76pv6qff.js +9 -0
- package/dist/tooling/shared/{bz2dqhab.js → 7aews1zg.js} +3 -3
- package/dist/tooling/shared/{9kazs5p6.js → 7b3xp9gc.js} +3 -3
- package/dist/tooling/shared/{4d2w25vw.js → 7pxncnzs.js} +2 -2
- package/dist/tooling/shared/{zvnhh1n0.js → 829fjxyw.js} +2 -2
- package/dist/tooling/shared/{dzx85q70.js → 8m5znpdt.js} +1 -1
- package/dist/tooling/shared/{g9ad4n80.js → 8yr7ges1.js} +7 -7
- package/dist/tooling/shared/{tn8gay14.js → 9qr9rkbh.js} +6 -6
- package/dist/tooling/shared/{8sqrsw3s.js → 9sfbtwsk.js} +3 -3
- package/dist/{shared/bnby0esn.js → tooling/shared/9wsnsvbf.js} +1 -1
- package/dist/tooling/shared/{aftb2mwr.js → a2xe04bm.js} +1 -1
- package/dist/tooling/shared/{nf7jmhfa.js → a4pzghha.js} +2 -2
- package/dist/tooling/shared/{y5656f0e.js → am04v8na.js} +5 -5
- package/dist/tooling/shared/{417zdsk7.js → asgr07mp.js} +5 -5
- package/dist/tooling/shared/{pqy21k24.js → c0xevn1h.js} +4 -4
- package/dist/tooling/shared/dhatassp.js +134 -0
- package/dist/tooling/shared/{spk1jg3r.js → ezqn88nj.js} +8 -8
- package/dist/tooling/shared/{6nyfyw70.js → f77nnfqy.js} +2 -2
- package/dist/tooling/shared/{wt7j7nf6.js → fh07z1dt.js} +1 -1
- package/dist/tooling/shared/{3rrw5w8f.js → gqwevek4.js} +1 -1
- package/dist/tooling/shared/{f9mphxej.js → hfq3n3bd.js} +346 -19
- package/dist/tooling/shared/{vs0t6qt8.js → j664qjrt.js} +2 -2
- package/dist/tooling/shared/{733a1tq3.js → jpgmjsx3.js} +3 -3
- package/dist/tooling/shared/{mtrqn2hn.js → jr2nmr81.js} +22 -30
- package/dist/{shared/mtw6jz76.js → tooling/shared/jsjjz875.js} +1 -1
- package/dist/tooling/shared/{2wtvc7rw.js → kbqnrs1x.js} +2 -2
- package/dist/tooling/shared/{gfvtj6d0.js → kz715295.js} +7 -7
- package/dist/tooling/shared/{a7q7t4n3.js → m3xt8b1h.js} +2 -2
- package/dist/tooling/shared/{dw08f3m8.js → m4hhb8aa.js} +2 -2
- package/dist/tooling/shared/{y031jct9.js → mw5npye0.js} +2 -2
- package/dist/tooling/shared/{vraj6d74.js → nxq95eb6.js} +3 -3
- package/dist/tooling/shared/{fb9k2qwd.js → qartgazh.js} +11 -30
- package/dist/tooling/shared/{xjs6zqtc.js → rcywj2w8.js} +9 -9
- package/dist/tooling/shared/{dy672fz6.js → s6qha57m.js} +2 -2
- package/dist/tooling/shared/{vx86qjv8.js → shjs0379.js} +3 -3
- package/dist/tooling/shared/{345tbc2k.js → sjwkt1pg.js} +1 -1
- package/dist/tooling/shared/{rq5kk8j6.js → stebqyyd.js} +3 -3
- package/dist/tooling/shared/{yk9mp7vb.js → t4xveemq.js} +4 -4
- package/dist/tooling/shared/{djydjpqt.js → t6zjeake.js} +5 -5
- package/dist/tooling/shared/{zhwnfyb2.js → tcwpjvxe.js} +1 -1
- package/dist/tooling/shared/{72fyg4zv.js → tpssp7ac.js} +1 -1
- package/dist/tooling/shared/{jne7b93p.js → v6j1ep6w.js} +3 -3
- package/dist/tooling/shared/{ec71zqgt.js → vwr2y833.js} +2 -2
- package/dist/tooling/shared/{j9t5s12a.js → w17sj05d.js} +2 -2
- package/dist/tooling/shared/{3tq8pnb8.js → w5d89rb4.js} +1 -1
- package/dist/tooling/shared/{smn6kz1p.js → wbwq1jsa.js} +2 -2
- package/dist/tooling/shared/{pwsytxm5.js → wezxf3h8.js} +1 -1
- package/dist/tooling/shared/{5bvw2nev.js → yb4kd19s.js} +1 -1
- package/dist/tooling/shared/{gmcf9cxf.js → ybdew6j7.js} +2 -2
- package/dist/tooling/shared/{ep8zh3bk.js → yfedr1zb.js} +1 -1
- package/dist/tooling/shared/{ddjtfgnx.js → yhp829bx.js} +3 -3
- package/dist/tooling/shared/{yfr8hzhm.js → ytk1tjs0.js} +1 -1
- package/dist/tooling/shared/{03k6res6.js → zs9aymcp.js} +1 -1
- package/dist/tooling/shared/zx3v9phv.js +8 -0
- package/dist/tooling/testing/index.js +2 -2
- package/package.json +38 -9
- package/dist/shared/fjpnhqan.js +0 -71
- package/dist/shared/n41w4fbz.js +0 -113
- package/dist/tooling/shared/pw8fgahx.js +0 -69
- /package/dist/shared/{ae4j5f42.js → mfvx6kjx.js} +0 -0
|
@@ -0,0 +1,1766 @@
|
|
|
1
|
+
import {
|
|
2
|
+
functionObject,
|
|
3
|
+
viewObject,
|
|
4
|
+
materializedViewObject
|
|
5
|
+
} from "./12z7aznj.js";
|
|
6
|
+
import {
|
|
7
|
+
quoteIdent2,
|
|
8
|
+
renderCatalog2,
|
|
9
|
+
createObjectSql2,
|
|
10
|
+
identitySequence2,
|
|
11
|
+
createTableSql2
|
|
12
|
+
} from "./fk9vef30.js";
|
|
13
|
+
import {
|
|
14
|
+
cellText,
|
|
15
|
+
introspectSchema
|
|
16
|
+
} from "./myk8fswx.js";
|
|
17
|
+
import {
|
|
18
|
+
projectPath,
|
|
19
|
+
loadConfig
|
|
20
|
+
} from "./2zdxkt7b.js";
|
|
21
|
+
import {
|
|
22
|
+
sqlIdentifiers,
|
|
23
|
+
sessionFailure,
|
|
24
|
+
finishQuiet,
|
|
25
|
+
assertTargetPolicy,
|
|
26
|
+
selectTarget
|
|
27
|
+
} from "./6nae0spm.js";
|
|
28
|
+
import {
|
|
29
|
+
dependencyOrder,
|
|
30
|
+
catalog,
|
|
31
|
+
isDomain
|
|
32
|
+
} from "./ywspezzx.js";
|
|
33
|
+
import {
|
|
34
|
+
staticNamespace2,
|
|
35
|
+
identityKey2
|
|
36
|
+
} from "./358zsv4r.js";
|
|
37
|
+
import {
|
|
38
|
+
OkmError2
|
|
39
|
+
} from "./52szzakm.js";
|
|
40
|
+
|
|
41
|
+
// src/tooling/migrate/pull.ts
|
|
42
|
+
import { spawnSync } from "node:child_process";
|
|
43
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
44
|
+
import { dirname as dirname2, join } from "node:path";
|
|
45
|
+
import postgres from "postgres";
|
|
46
|
+
|
|
47
|
+
// src/tooling/migrate/pull-emit.ts
|
|
48
|
+
import { relative, dirname } from "node:path";
|
|
49
|
+
|
|
50
|
+
// src/tooling/migrate/pull-routines.ts
|
|
51
|
+
var BINDING_SEED = ["schema", "extension", "fn", "trigger", "view", "materializedView"];
|
|
52
|
+
function planRoutines(source, tableExports) {
|
|
53
|
+
const namespace = staticNamespace2("public");
|
|
54
|
+
const used = new Set([...BINDING_SEED, ...tableExports.values()]);
|
|
55
|
+
const tables = new Map;
|
|
56
|
+
for (const name of tableExports.keys()) {
|
|
57
|
+
tables.set(name, { kind: "table", namespace, name });
|
|
58
|
+
}
|
|
59
|
+
const functions = source.objects.filter((object) => object.kind === "function");
|
|
60
|
+
const functionByName = new Map;
|
|
61
|
+
for (const object of functions) {
|
|
62
|
+
const list = functionByName.get(object.identity.name) ?? [];
|
|
63
|
+
list.push(object);
|
|
64
|
+
functionByName.set(object.identity.name, list);
|
|
65
|
+
}
|
|
66
|
+
const views = source.objects.filter((object) => object.kind === "view" || object.kind === "materializedView");
|
|
67
|
+
const viewByName = new Map;
|
|
68
|
+
for (const object of views)
|
|
69
|
+
viewByName.set(object.identity.name, object);
|
|
70
|
+
const skipped = [];
|
|
71
|
+
const plannedFunctions = [];
|
|
72
|
+
const pulledFunctions = new Map;
|
|
73
|
+
const orderEdges = new Map;
|
|
74
|
+
const sortedFunctions = [...functions].sort((left, right) => compare(`${left.identity.name}\x00${left.identity.argTypes.join(",")}`, `${right.identity.name}\x00${right.identity.argTypes.join(",")}`));
|
|
75
|
+
for (const object of sortedFunctions) {
|
|
76
|
+
const label = functionLabel(object);
|
|
77
|
+
if (object.definition.security === "definer" && (object.definition.searchPath === undefined || object.definition.searchPath.length === 0)) {
|
|
78
|
+
skipped.push(skip(object, label, "security definer without search_path is OKM1823", object.definition.body));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const binding = freshBinding(object.identity.name, used);
|
|
82
|
+
const atomic = object.definition.atomic === true;
|
|
83
|
+
const dependsOn = [];
|
|
84
|
+
const edges = [];
|
|
85
|
+
if (atomic) {
|
|
86
|
+
for (const edge of object.dependencies) {
|
|
87
|
+
const fragment = dependFragment(edge.target, tableExports);
|
|
88
|
+
if (fragment === undefined)
|
|
89
|
+
continue;
|
|
90
|
+
dependsOn.push(fragment);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
const matched = matchNames(object.definition.body, tables, viewByName, functionByName);
|
|
94
|
+
for (const name of matched.tables) {
|
|
95
|
+
const exportName = tableExports.get(name);
|
|
96
|
+
const identity = tables.get(name);
|
|
97
|
+
if (exportName === undefined || identity === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
dependsOn.push(exportName);
|
|
100
|
+
edges.push(identity);
|
|
101
|
+
}
|
|
102
|
+
for (const identity of matched.views)
|
|
103
|
+
edges.push(identity);
|
|
104
|
+
for (const identity of matched.functions) {
|
|
105
|
+
if (identityKey2(identity) === identityKey2(object.identity))
|
|
106
|
+
continue;
|
|
107
|
+
edges.push(identity);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (edges.length > 0)
|
|
111
|
+
orderEdges.set(identityKey2(object.identity), edges);
|
|
112
|
+
const planned = { object, binding, dependsOn };
|
|
113
|
+
plannedFunctions.push(planned);
|
|
114
|
+
pulledFunctions.set(identityKey2(object.identity), planned);
|
|
115
|
+
}
|
|
116
|
+
const plannedTriggers = [];
|
|
117
|
+
const triggers = source.objects.filter((object) => object.kind === "trigger");
|
|
118
|
+
for (const object of [...triggers].sort((left, right) => compare(`${left.identity.parent.name}.${left.identity.name}`, `${right.identity.parent.name}.${right.identity.name}`))) {
|
|
119
|
+
const label = `${object.identity.parent.name}.${object.identity.name}`;
|
|
120
|
+
const tableBinding = tableExports.get(object.identity.parent.name);
|
|
121
|
+
if (tableBinding === undefined) {
|
|
122
|
+
skipped.push(skip(object, label, "the table was not pulled", ""));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const callKey = identityKey2({
|
|
126
|
+
kind: "function",
|
|
127
|
+
namespace: object.definition.calls.namespace,
|
|
128
|
+
name: object.definition.calls.name,
|
|
129
|
+
argTypes: object.definition.calls.argTypes
|
|
130
|
+
});
|
|
131
|
+
const call = pulledFunctions.get(callKey);
|
|
132
|
+
if (call === undefined) {
|
|
133
|
+
skipped.push(skip(object, label, "the function was not pulled", ""));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const binding = freshBinding(object.identity.name, used);
|
|
137
|
+
plannedTriggers.push({ object, binding, on: tableBinding, calls: call.binding });
|
|
138
|
+
}
|
|
139
|
+
const indexes = source.objects.filter((object) => object.kind === "index");
|
|
140
|
+
const plannedViews = [];
|
|
141
|
+
for (const object of [...views].sort((left, right) => compare(left.identity.name, right.identity.name))) {
|
|
142
|
+
if (object.kind === "view" && object.definition.securityInvoker === true) {
|
|
143
|
+
skipped.push(skip(object, object.identity.name, "view() sets security_invoker from tenancy, and pull does not declare tenancy", object.definition.query));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const owned = indexes.filter((index) => index.identity.parent.name === object.identity.name);
|
|
147
|
+
const kept = [];
|
|
148
|
+
if (object.kind === "materializedView") {
|
|
149
|
+
for (const index of [...owned].sort((left, right) => compare(left.identity.name, right.identity.name))) {
|
|
150
|
+
if (index.definition.columns.length === 0 || index.definition.expression !== undefined || index.definition.predicate !== undefined) {
|
|
151
|
+
skipped.push(skip(index, `${object.identity.name}.${index.identity.name}`, "materializedView() stores a column index, not an expression or a predicate", ""));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
kept.push(index);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const matched = matchNames(object.definition.query, tables, viewByName, functionByName);
|
|
158
|
+
const self = identityKey2(object.identity);
|
|
159
|
+
const edges = [];
|
|
160
|
+
for (const name of matched.tables) {
|
|
161
|
+
const identity = tables.get(name);
|
|
162
|
+
if (identity !== undefined)
|
|
163
|
+
edges.push(identity);
|
|
164
|
+
}
|
|
165
|
+
for (const identity of matched.views) {
|
|
166
|
+
if (identityKey2(identity) !== self)
|
|
167
|
+
edges.push(identity);
|
|
168
|
+
}
|
|
169
|
+
for (const identity of matched.functions) {
|
|
170
|
+
if (identityKey2(identity) !== self)
|
|
171
|
+
edges.push(identity);
|
|
172
|
+
}
|
|
173
|
+
if (edges.length > 0)
|
|
174
|
+
orderEdges.set(self, edges);
|
|
175
|
+
plannedViews.push({
|
|
176
|
+
object,
|
|
177
|
+
binding: freshBinding(object.identity.name, used),
|
|
178
|
+
indexes: kept
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
functions: plannedFunctions,
|
|
183
|
+
triggers: plannedTriggers,
|
|
184
|
+
views: plannedViews,
|
|
185
|
+
skipped,
|
|
186
|
+
orderEdges
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function renderRoutines(plan, verdicts) {
|
|
190
|
+
const imports = [];
|
|
191
|
+
if (plan.functions.length > 0 || plan.triggers.length > 0) {
|
|
192
|
+
const names = [];
|
|
193
|
+
if (plan.functions.length > 0)
|
|
194
|
+
names.push("fn");
|
|
195
|
+
if (plan.triggers.length > 0)
|
|
196
|
+
names.push("trigger");
|
|
197
|
+
imports.push(`import { ${names.join(", ")} } from "okmodel/fn";`);
|
|
198
|
+
}
|
|
199
|
+
if (plan.views.length > 0) {
|
|
200
|
+
const hasView = plan.views.some((item) => item.object.kind === "view");
|
|
201
|
+
const hasMat = plan.views.some((item) => item.object.kind === "materializedView");
|
|
202
|
+
const names = [];
|
|
203
|
+
if (hasMat)
|
|
204
|
+
names.push("materializedView");
|
|
205
|
+
if (hasView)
|
|
206
|
+
names.push("view");
|
|
207
|
+
imports.push(`import { ${names.join(", ")} } from "okmodel/view";`);
|
|
208
|
+
}
|
|
209
|
+
const lines = [];
|
|
210
|
+
for (const item of plan.functions) {
|
|
211
|
+
lines.push(commentFor(verdicts.get(identityKey2(item.object.identity)), "function"));
|
|
212
|
+
lines.push(functionSource(item));
|
|
213
|
+
lines.push("");
|
|
214
|
+
}
|
|
215
|
+
for (const item of plan.triggers) {
|
|
216
|
+
lines.push(commentFor(verdicts.get(identityKey2(item.object.identity)), "trigger"));
|
|
217
|
+
lines.push(triggerSource(item));
|
|
218
|
+
lines.push("");
|
|
219
|
+
}
|
|
220
|
+
for (const item of plan.views) {
|
|
221
|
+
lines.push(commentFor(verdicts.get(identityKey2(item.object.identity)), item.object.kind === "materializedView" ? "materializedView" : "view"));
|
|
222
|
+
lines.push(viewSource(item));
|
|
223
|
+
lines.push("");
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
imports,
|
|
227
|
+
body: lines.join(`
|
|
228
|
+
`),
|
|
229
|
+
functions: plan.functions.map((item) => item.binding),
|
|
230
|
+
triggers: plan.triggers.map((item) => item.binding),
|
|
231
|
+
views: plan.views.map((item) => item.binding)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function verdictsWithoutScratch(plan, detail) {
|
|
235
|
+
const verdicts = [];
|
|
236
|
+
for (const item of plan.functions) {
|
|
237
|
+
const plpgsql = item.object.definition.language === "plpgsql";
|
|
238
|
+
verdicts.push({
|
|
239
|
+
key: identityKey2(item.object.identity),
|
|
240
|
+
kind: "function",
|
|
241
|
+
label: functionLabel(item.object),
|
|
242
|
+
status: plpgsql ? "candidate-only" : "unverified",
|
|
243
|
+
detail
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
for (const item of plan.triggers) {
|
|
247
|
+
verdicts.push({
|
|
248
|
+
key: identityKey2(item.object.identity),
|
|
249
|
+
kind: "trigger",
|
|
250
|
+
label: `${item.object.identity.parent.name}.${item.object.identity.name}`,
|
|
251
|
+
status: "unverified",
|
|
252
|
+
detail
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
for (const item of plan.views) {
|
|
256
|
+
verdicts.push({
|
|
257
|
+
key: identityKey2(item.object.identity),
|
|
258
|
+
kind: item.object.kind,
|
|
259
|
+
label: item.object.identity.name,
|
|
260
|
+
status: "unverified",
|
|
261
|
+
detail
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return verdicts;
|
|
265
|
+
}
|
|
266
|
+
function matchNames(body, tables, views, functions) {
|
|
267
|
+
const tableHits = new Set;
|
|
268
|
+
const viewHits = new Map;
|
|
269
|
+
const functionHits = new Map;
|
|
270
|
+
for (const name of sqlIdentifiers(body)) {
|
|
271
|
+
if (name.quoted) {
|
|
272
|
+
const table = tables.get(name.text);
|
|
273
|
+
if (table !== undefined)
|
|
274
|
+
tableHits.add(name.text);
|
|
275
|
+
const view = views.get(name.text);
|
|
276
|
+
if (view !== undefined)
|
|
277
|
+
viewHits.set(identityKey2(view.identity), view.identity);
|
|
278
|
+
for (const fn of functions.get(name.text) ?? []) {
|
|
279
|
+
functionHits.set(identityKey2(fn.identity), fn.identity);
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const folded = name.text.toLowerCase();
|
|
284
|
+
for (const [table, identity] of tables) {
|
|
285
|
+
if (table.toLowerCase() === folded) {
|
|
286
|
+
tableHits.add(table);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const [viewName, view] of views) {
|
|
290
|
+
if (viewName.toLowerCase() === folded)
|
|
291
|
+
viewHits.set(identityKey2(view.identity), view.identity);
|
|
292
|
+
}
|
|
293
|
+
for (const [fnName, list] of functions) {
|
|
294
|
+
if (fnName.toLowerCase() !== folded)
|
|
295
|
+
continue;
|
|
296
|
+
for (const fn of list)
|
|
297
|
+
functionHits.set(identityKey2(fn.identity), fn.identity);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
tables: [...tableHits].sort(compare),
|
|
302
|
+
views: [...viewHits.values()],
|
|
303
|
+
functions: [...functionHits.values()]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function dependFragment(target, tableExports) {
|
|
307
|
+
if (target.kind === "table") {
|
|
308
|
+
const binding = tableExports.get(target.name);
|
|
309
|
+
return binding;
|
|
310
|
+
}
|
|
311
|
+
if (target.kind === "column") {
|
|
312
|
+
const binding = tableExports.get(target.parent.name);
|
|
313
|
+
if (binding === undefined)
|
|
314
|
+
return;
|
|
315
|
+
return `{ table: ${binding}, column: ${JSON.stringify(target.name)} }`;
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
function functionSource(item) {
|
|
320
|
+
const definition = item.object.definition;
|
|
321
|
+
const fields = [
|
|
322
|
+
definition.arguments.length > 0 ? `arguments: [${definition.arguments.map((argument) => `{ name: ${JSON.stringify(argument.name)}, type: ${JSON.stringify(argument.type)} }`).join(", ")}]` : "",
|
|
323
|
+
`returns: ${JSON.stringify(definition.returns)}`,
|
|
324
|
+
`language: ${JSON.stringify(definition.language)}`,
|
|
325
|
+
definition.volatility !== "volatile" ? `volatility: ${JSON.stringify(definition.volatility)}` : "",
|
|
326
|
+
definition.security === "definer" ? `security: "definer"` : "",
|
|
327
|
+
definition.searchPath !== undefined ? `searchPath: ${JSON.stringify(definition.searchPath)}` : "",
|
|
328
|
+
`body: ${JSON.stringify(definition.body)}`,
|
|
329
|
+
item.dependsOn.length > 0 || definition.language === "plpgsql" ? `dependsOn: [${item.dependsOn.join(", ")}]` : ""
|
|
330
|
+
].filter((field) => field.length > 0);
|
|
331
|
+
return `const ${item.binding} = fn(${JSON.stringify(item.object.identity.name)}, {
|
|
332
|
+
${fields.join(`,
|
|
333
|
+
`)},
|
|
334
|
+
});`;
|
|
335
|
+
}
|
|
336
|
+
function triggerSource(item) {
|
|
337
|
+
const definition = item.object.definition;
|
|
338
|
+
const fields = [
|
|
339
|
+
`on: ${item.on}`,
|
|
340
|
+
`timing: ${JSON.stringify(definition.timing)}`,
|
|
341
|
+
`events: [${definition.events.map((event) => JSON.stringify(event)).join(", ")}]`,
|
|
342
|
+
`level: ${JSON.stringify(definition.level)}`,
|
|
343
|
+
definition.updateOf !== undefined ? `updateOf: [${definition.updateOf.map((column) => JSON.stringify(column)).join(", ")}]` : "",
|
|
344
|
+
definition.when !== undefined ? `when: ${JSON.stringify(definition.when)}` : "",
|
|
345
|
+
`calls: ${item.calls}`
|
|
346
|
+
].filter((field) => field.length > 0);
|
|
347
|
+
return `const ${item.binding} = trigger(${JSON.stringify(item.object.identity.name)}, {
|
|
348
|
+
${fields.join(`,
|
|
349
|
+
`)},
|
|
350
|
+
});`;
|
|
351
|
+
}
|
|
352
|
+
function viewSource(item) {
|
|
353
|
+
const definition = item.object.definition;
|
|
354
|
+
const columns = definition.columns.map((column) => `{ name: ${JSON.stringify(column.name)}, type: ${JSON.stringify(column.dataType)} }`).join(", ");
|
|
355
|
+
const fields = [`columns: [${columns}]`, `query: ${JSON.stringify(definition.query)}`];
|
|
356
|
+
if (item.object.kind === "materializedView" && item.indexes.length > 0) {
|
|
357
|
+
const indexes = item.indexes.map((index) => {
|
|
358
|
+
const parts = [
|
|
359
|
+
`name: ${JSON.stringify(index.identity.name)}`,
|
|
360
|
+
`columns: [${index.definition.columns.map((column) => JSON.stringify(column)).join(", ")}]`
|
|
361
|
+
];
|
|
362
|
+
if (index.definition.unique)
|
|
363
|
+
parts.push("unique: true");
|
|
364
|
+
return `{ ${parts.join(", ")} }`;
|
|
365
|
+
}).join(", ");
|
|
366
|
+
fields.push(`indexes: [${indexes}]`);
|
|
367
|
+
}
|
|
368
|
+
const call = item.object.kind === "materializedView" ? "materializedView" : "view";
|
|
369
|
+
return `const ${item.binding} = ${call}(${JSON.stringify(item.object.identity.name)}, {
|
|
370
|
+
${fields.join(`,
|
|
371
|
+
`)},
|
|
372
|
+
});`;
|
|
373
|
+
}
|
|
374
|
+
function commentFor(verdict, kind) {
|
|
375
|
+
const status = verdict?.status ?? (kind === "function" ? "candidate-only" : "unverified");
|
|
376
|
+
if (status === "verified" && kind === "function") {
|
|
377
|
+
return "// verified: scratch replay created this function with check_function_bodies on.";
|
|
378
|
+
}
|
|
379
|
+
if (status === "verified" && kind === "trigger") {
|
|
380
|
+
return "// verified: scratch replay created this trigger.";
|
|
381
|
+
}
|
|
382
|
+
if (status === "verified") {
|
|
383
|
+
return "// verified: scratch replay created this view.";
|
|
384
|
+
}
|
|
385
|
+
if (status === "candidate-only") {
|
|
386
|
+
return "// candidate-only: a plpgsql body is not proved by creation. dependsOn is a lexical match.";
|
|
387
|
+
}
|
|
388
|
+
if (kind === "function")
|
|
389
|
+
return "// unverified: scratch replay did not create this function.";
|
|
390
|
+
if (kind === "trigger")
|
|
391
|
+
return "// unverified: scratch replay did not create this trigger.";
|
|
392
|
+
return "// unverified: scratch replay did not create this view.";
|
|
393
|
+
}
|
|
394
|
+
function functionLabel(object) {
|
|
395
|
+
return `${object.identity.name}(${object.identity.argTypes.join(", ")})`;
|
|
396
|
+
}
|
|
397
|
+
function skip(object, label, reason, sql) {
|
|
398
|
+
return { key: identityKey2(object.identity), kind: object.kind, label, reason, sql };
|
|
399
|
+
}
|
|
400
|
+
function freshBinding(sql, used) {
|
|
401
|
+
let name = bindingIdent(sql);
|
|
402
|
+
if (name === "table" || name === "columns")
|
|
403
|
+
name = `${name}Name`;
|
|
404
|
+
let salt = 2;
|
|
405
|
+
while (used.has(name)) {
|
|
406
|
+
name = `${bindingIdent(sql)}_${String(salt)}`;
|
|
407
|
+
salt += 1;
|
|
408
|
+
}
|
|
409
|
+
used.add(name);
|
|
410
|
+
return name;
|
|
411
|
+
}
|
|
412
|
+
function bindingIdent(sql) {
|
|
413
|
+
let out = "";
|
|
414
|
+
for (const char of sql) {
|
|
415
|
+
const ok = char >= "A" && char <= "Z" || char >= "a" && char <= "z" || char >= "0" && char <= "9" || char === "_";
|
|
416
|
+
out += ok ? char : "_";
|
|
417
|
+
}
|
|
418
|
+
if (out.length === 0)
|
|
419
|
+
out = "routine";
|
|
420
|
+
const first = out[0] ?? "";
|
|
421
|
+
if (first >= "0" && first <= "9")
|
|
422
|
+
out = `r_${out}`;
|
|
423
|
+
if (BINDING_RESERVED.has(out))
|
|
424
|
+
out = `${out}Name`;
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
var BINDING_RESERVED = new Set([
|
|
428
|
+
"await",
|
|
429
|
+
"break",
|
|
430
|
+
"case",
|
|
431
|
+
"catch",
|
|
432
|
+
"class",
|
|
433
|
+
"const",
|
|
434
|
+
"continue",
|
|
435
|
+
"debugger",
|
|
436
|
+
"default",
|
|
437
|
+
"delete",
|
|
438
|
+
"do",
|
|
439
|
+
"else",
|
|
440
|
+
"enum",
|
|
441
|
+
"export",
|
|
442
|
+
"extends",
|
|
443
|
+
"false",
|
|
444
|
+
"finally",
|
|
445
|
+
"for",
|
|
446
|
+
"function",
|
|
447
|
+
"if",
|
|
448
|
+
"implements",
|
|
449
|
+
"import",
|
|
450
|
+
"in",
|
|
451
|
+
"instanceof",
|
|
452
|
+
"interface",
|
|
453
|
+
"let",
|
|
454
|
+
"new",
|
|
455
|
+
"null",
|
|
456
|
+
"package",
|
|
457
|
+
"private",
|
|
458
|
+
"protected",
|
|
459
|
+
"public",
|
|
460
|
+
"return",
|
|
461
|
+
"static",
|
|
462
|
+
"super",
|
|
463
|
+
"switch",
|
|
464
|
+
"this",
|
|
465
|
+
"throw",
|
|
466
|
+
"true",
|
|
467
|
+
"try",
|
|
468
|
+
"typeof",
|
|
469
|
+
"var",
|
|
470
|
+
"void",
|
|
471
|
+
"while",
|
|
472
|
+
"with",
|
|
473
|
+
"yield"
|
|
474
|
+
]);
|
|
475
|
+
function compare(left, right) {
|
|
476
|
+
if (left < right)
|
|
477
|
+
return -1;
|
|
478
|
+
if (left > right)
|
|
479
|
+
return 1;
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/tooling/migrate/pull-emit.ts
|
|
484
|
+
var DEFERRED = [
|
|
485
|
+
{ kind: "policy", reason: "a later step writes policies" },
|
|
486
|
+
{ kind: "role", reason: "a later step writes roles and grants" },
|
|
487
|
+
{ kind: "grant", reason: "a later step writes roles and grants" },
|
|
488
|
+
{ kind: "defaultPrivilege", reason: "a later step writes roles and grants" }
|
|
489
|
+
];
|
|
490
|
+
var RESERVED = new Set([
|
|
491
|
+
"await",
|
|
492
|
+
"break",
|
|
493
|
+
"case",
|
|
494
|
+
"catch",
|
|
495
|
+
"class",
|
|
496
|
+
"const",
|
|
497
|
+
"continue",
|
|
498
|
+
"debugger",
|
|
499
|
+
"default",
|
|
500
|
+
"delete",
|
|
501
|
+
"do",
|
|
502
|
+
"else",
|
|
503
|
+
"enum",
|
|
504
|
+
"export",
|
|
505
|
+
"extends",
|
|
506
|
+
"false",
|
|
507
|
+
"finally",
|
|
508
|
+
"for",
|
|
509
|
+
"function",
|
|
510
|
+
"if",
|
|
511
|
+
"implements",
|
|
512
|
+
"import",
|
|
513
|
+
"in",
|
|
514
|
+
"instanceof",
|
|
515
|
+
"interface",
|
|
516
|
+
"let",
|
|
517
|
+
"new",
|
|
518
|
+
"null",
|
|
519
|
+
"package",
|
|
520
|
+
"private",
|
|
521
|
+
"protected",
|
|
522
|
+
"public",
|
|
523
|
+
"return",
|
|
524
|
+
"static",
|
|
525
|
+
"super",
|
|
526
|
+
"switch",
|
|
527
|
+
"this",
|
|
528
|
+
"throw",
|
|
529
|
+
"true",
|
|
530
|
+
"try",
|
|
531
|
+
"typeof",
|
|
532
|
+
"var",
|
|
533
|
+
"void",
|
|
534
|
+
"while",
|
|
535
|
+
"with",
|
|
536
|
+
"yield"
|
|
537
|
+
]);
|
|
538
|
+
function emitPull(source, layout, review) {
|
|
539
|
+
const statements = rendered(source);
|
|
540
|
+
const skipped = [];
|
|
541
|
+
const enums = enumLabels(source);
|
|
542
|
+
const domains = domainDefs(source);
|
|
543
|
+
const byParent = groupByParent(source);
|
|
544
|
+
const tables = source.objects.filter((object) => object.kind === "table").sort((left, right) => compare2(left.identity.name, right.identity.name));
|
|
545
|
+
const files = [];
|
|
546
|
+
const imports = [];
|
|
547
|
+
const usedFiles = new Set;
|
|
548
|
+
const identitySequences = new Set;
|
|
549
|
+
const tableExports = new Map;
|
|
550
|
+
for (const table of tables) {
|
|
551
|
+
if (table.definition.partition !== undefined) {
|
|
552
|
+
skipped.push(skip2(table, "table", table.identity.name, "table() has no partition option", statementFor(statements, table.identity.name)));
|
|
553
|
+
skipChildren(table.identity.name, byParent, statements, skipped);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const planned = planTable(table, byParent, enums, domains, statements, skipped);
|
|
557
|
+
if (planned === undefined)
|
|
558
|
+
continue;
|
|
559
|
+
if (planned.identitySequence !== undefined)
|
|
560
|
+
identitySequences.add(planned.identitySequence);
|
|
561
|
+
tableExports.set(table.identity.name, planned.exportName);
|
|
562
|
+
const fileName = uniqueFile(planned.exportName, usedFiles);
|
|
563
|
+
const path = `${layout.tablesDir}/${fileName}.ts`;
|
|
564
|
+
files.push({ path, text: planned.text });
|
|
565
|
+
imports.push({ name: planned.exportName, file: path });
|
|
566
|
+
}
|
|
567
|
+
const skippedKeys = new Set(skipped.map((item) => item.key));
|
|
568
|
+
for (const object of source.objects) {
|
|
569
|
+
if (object.kind !== "sequence" || skippedKeys.has(identityKey2(object.identity)))
|
|
570
|
+
continue;
|
|
571
|
+
if (identitySequences.has(object.identity.name))
|
|
572
|
+
continue;
|
|
573
|
+
const parent = object.dependencies.find((edge) => edge.target.kind === "table");
|
|
574
|
+
const parentName = parent !== undefined && parent.target.kind === "table" ? parent.target.name : "";
|
|
575
|
+
const kept = parentName.length > 0 && tables.some((table) => table.identity.name === parentName && table.definition.partition === undefined);
|
|
576
|
+
if (kept)
|
|
577
|
+
continue;
|
|
578
|
+
skipped.push(skip2(object, "sequence", object.identity.name, "table() has no standalone sequence. An identity column writes its own", statementFor(statements, object.identity.name)));
|
|
579
|
+
}
|
|
580
|
+
const plan = review?.plan ?? planRoutines(source, tableExports);
|
|
581
|
+
for (const item of plan.skipped)
|
|
582
|
+
skipped.push(item);
|
|
583
|
+
for (const object of source.objects) {
|
|
584
|
+
if (!isDeferred(object.kind))
|
|
585
|
+
continue;
|
|
586
|
+
skipped.push(skip2(object, object.kind, objectLabel(object), deferredReason(object.kind), statementFor(statements, objectName(object))));
|
|
587
|
+
}
|
|
588
|
+
for (const object of source.objects) {
|
|
589
|
+
if (object.kind !== "type")
|
|
590
|
+
continue;
|
|
591
|
+
if (typeIsUsed(object.identity.name, source, skipped))
|
|
592
|
+
continue;
|
|
593
|
+
const label = object.identity.name;
|
|
594
|
+
skipped.push(skip2(object, "type", label, "no pulled column uses this type", statementFor(statements, label)));
|
|
595
|
+
}
|
|
596
|
+
const verdictList = review?.verdicts ?? [];
|
|
597
|
+
const routines = renderRoutines(plan, new Map(verdictList.map((item) => [item.key, item])));
|
|
598
|
+
files.push({
|
|
599
|
+
path: layout.schemaFile,
|
|
600
|
+
text: schemaText(layout.schemaFile, imports, extensionCalls(source), routines)
|
|
601
|
+
});
|
|
602
|
+
files.sort((left, right) => compare2(left.path, right.path));
|
|
603
|
+
skipped.sort((left, right) => compare2(`${left.kind}\x00${left.label}`, `${right.kind}\x00${right.label}`));
|
|
604
|
+
const viewCount = plan.views.filter((item) => item.object.kind === "view").length;
|
|
605
|
+
return {
|
|
606
|
+
files,
|
|
607
|
+
skipped,
|
|
608
|
+
verdicts: verdictList,
|
|
609
|
+
counts: {
|
|
610
|
+
tables: tables.length - skippedTables(source, skipped),
|
|
611
|
+
functions: plan.functions.length,
|
|
612
|
+
triggers: plan.triggers.length,
|
|
613
|
+
views: viewCount,
|
|
614
|
+
materializedViews: plan.views.length - viewCount
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function skippedTables(source, skipped) {
|
|
619
|
+
let count = 0;
|
|
620
|
+
for (const object of source.objects) {
|
|
621
|
+
if (object.kind !== "table")
|
|
622
|
+
continue;
|
|
623
|
+
if (skipped.some((item) => item.key === identityKey2(object.identity)))
|
|
624
|
+
count += 1;
|
|
625
|
+
}
|
|
626
|
+
return count;
|
|
627
|
+
}
|
|
628
|
+
function formatPullReport(counts, skipped, verdicts = [], unexpressed = [], verb = "pulled") {
|
|
629
|
+
const lines = [
|
|
630
|
+
verb === "imported" ? `imported ${counted(counts.tables, "table", "tables")}` : `pulled ${counted(counts.tables, "table", "tables")}, ${counted(counts.functions, "function", "functions")}, ${counted(counts.triggers, "trigger", "triggers")}, ${counted(counts.views, "view", "views")}, ${counted(counts.materializedViews, "materialized view", "materialized views")}`
|
|
631
|
+
];
|
|
632
|
+
const marked = [...verdicts].sort((left, right) => compare2(left.label, right.label));
|
|
633
|
+
for (const item of marked) {
|
|
634
|
+
const detail = item.detail.length > 0 ? ` — ${item.detail}` : "";
|
|
635
|
+
lines.push(` ${item.label}: ${item.status}${detail}`);
|
|
636
|
+
}
|
|
637
|
+
lines.push(verb === "imported" ? "not imported" : "not pulled yet");
|
|
638
|
+
const deferredKinds = ["policy", "role", "grant", "defaultPrivilege"];
|
|
639
|
+
for (const kind of deferredKinds) {
|
|
640
|
+
const rows = skipped.filter((item) => item.kind === kind);
|
|
641
|
+
const reason = deferredReason(kind);
|
|
642
|
+
lines.push(` ${kindLabel(kind)}: ${String(rows.length)} — ${reason}`);
|
|
643
|
+
for (const row of rows)
|
|
644
|
+
lines.push(` ${row.label}`);
|
|
645
|
+
}
|
|
646
|
+
for (const item of unexpressed) {
|
|
647
|
+
lines.push(` ${kindLabel(item.kind)}: ${String(item.count)} — ${item.reason}`);
|
|
648
|
+
}
|
|
649
|
+
const rest = skipped.filter((item) => !deferredKinds.includes(item.kind));
|
|
650
|
+
const groups = new Map;
|
|
651
|
+
for (const item of rest) {
|
|
652
|
+
const group = groups.get(item.kind) ?? [];
|
|
653
|
+
group.push(item);
|
|
654
|
+
groups.set(item.kind, group);
|
|
655
|
+
}
|
|
656
|
+
for (const kind of [...groups.keys()].sort()) {
|
|
657
|
+
const rows = groups.get(kind) ?? [];
|
|
658
|
+
lines.push(` ${kindLabel(kind)}: ${String(rows.length)} — ${rows[0]?.reason ?? ""}`);
|
|
659
|
+
for (const row of rows) {
|
|
660
|
+
lines.push(` ${row.label}`);
|
|
661
|
+
if (row.sql.length > 0)
|
|
662
|
+
lines.push(` ${row.sql}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return `${lines.join(`
|
|
666
|
+
`)}
|
|
667
|
+
`;
|
|
668
|
+
}
|
|
669
|
+
function planTable(table, byParent, enums, domains, statements, skipped) {
|
|
670
|
+
const bag = byParent.get(table.identity.name) ?? {
|
|
671
|
+
columns: [],
|
|
672
|
+
constraints: [],
|
|
673
|
+
indexes: [],
|
|
674
|
+
sequences: []
|
|
675
|
+
};
|
|
676
|
+
const names = new Set;
|
|
677
|
+
const fields = new Map;
|
|
678
|
+
const columnLines = [];
|
|
679
|
+
const emitted = new Set;
|
|
680
|
+
const primary = bag.constraints.find((item) => item.definition.constraintKind === "primaryKey");
|
|
681
|
+
const primaryColumns = primary?.definition.columns ?? [];
|
|
682
|
+
const identityColumn = bag.columns.find((column) => column.definition.identity?.always === true && !column.definition.nullable && primaryColumns.length === 1 && primaryColumns[0] === column.identity.name && column.definition.dataType === "bigint");
|
|
683
|
+
for (const column of [...bag.columns].sort((left, right) => compare2(left.identity.name, right.identity.name))) {
|
|
684
|
+
const field = freshIdent(declaredIdent(column.provenance.source, column.identity.name), names);
|
|
685
|
+
fields.set(column.identity.name, field);
|
|
686
|
+
if (column === identityColumn) {
|
|
687
|
+
const renamed = column.provenance.source !== undefined && field !== column.identity.name ? `.sqlName(${JSON.stringify(column.identity.name)})` : "";
|
|
688
|
+
columnLines.push(` ${field}: t.identity()${renamed},`);
|
|
689
|
+
emitted.add(column.identity.name);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const built = columnSource(column, field, enums, domains);
|
|
693
|
+
if (built === undefined) {
|
|
694
|
+
skipped.push(skip2(column, "column", `${table.identity.name}.${column.identity.name}`, columnReason(column), ""));
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
columnLines.push(` ${field}: ${built},`);
|
|
698
|
+
emitted.add(column.identity.name);
|
|
699
|
+
}
|
|
700
|
+
const options = [];
|
|
701
|
+
const exportName = table.provenance.source === undefined ? exportNameOf(table.identity.name) : freshIdent(declaredIdent(table.provenance.source, table.identity.name), new Set);
|
|
702
|
+
if (exportName !== table.identity.name) {
|
|
703
|
+
options.push(` sqlName: ${JSON.stringify(table.identity.name)},`);
|
|
704
|
+
}
|
|
705
|
+
if (primary !== undefined && identityColumn === undefined && primaryColumns.length > 0) {
|
|
706
|
+
if (primaryColumns.every((name) => emitted.has(name)) && plainConstraint(primary)) {
|
|
707
|
+
const list = primaryColumns.map((name) => JSON.stringify(fields.get(name) ?? name)).join(", ");
|
|
708
|
+
options.push(` primaryKey: [${list}],`);
|
|
709
|
+
} else {
|
|
710
|
+
skipped.push(skip2(primary, "constraint", `${table.identity.name}.${primary.identity.name}`, "primary key uses a column this step did not pull", statementFor(statements, primary.identity.name)));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const uniques = [];
|
|
714
|
+
const checks = [];
|
|
715
|
+
const foreign = new Map;
|
|
716
|
+
for (const constraint of [...bag.constraints].sort((left, right) => compare2(left.identity.name, right.identity.name))) {
|
|
717
|
+
const kind = constraint.definition.constraintKind;
|
|
718
|
+
if (kind === "primaryKey")
|
|
719
|
+
continue;
|
|
720
|
+
const label = `${table.identity.name}.${constraint.identity.name}`;
|
|
721
|
+
if (!constraint.definition.columns.every((name) => emitted.has(name))) {
|
|
722
|
+
skipped.push(skip2(constraint, "constraint", label, "a column of this constraint was not pulled", statementFor(statements, constraint.identity.name)));
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (!plainConstraint(constraint)) {
|
|
726
|
+
skipped.push(skip2(constraint, "constraint", label, "deferrable, initially deferred, and nulls not distinct are not table() options", statementFor(statements, constraint.identity.name)));
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (kind === "unique") {
|
|
730
|
+
const cols = constraint.definition.columns.map((name) => JSON.stringify(fields.get(name) ?? name)).join(", ");
|
|
731
|
+
uniques.push(` ${jsKey(constraint.definition.nameKey)}: [${cols}],`);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (kind === "check" && constraint.definition.expression !== undefined) {
|
|
735
|
+
const touches = constraint.definition.columns.map((name) => `void columns.${fields.get(name) ?? name}`).join("; ");
|
|
736
|
+
const touch = touches.length > 0 ? `${touches}; ` : "";
|
|
737
|
+
checks.push(` ${jsKey(constraint.definition.nameKey)}: (columns) => { ${touch}return ${sqlTemplate(constraint.definition.expression)}; },`);
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (kind === "foreignKey") {
|
|
741
|
+
const local = constraint.definition.columns;
|
|
742
|
+
const references = constraint.definition.references;
|
|
743
|
+
const field = local[0] === undefined ? undefined : fields.get(local[0]);
|
|
744
|
+
if (field !== undefined && foreign.has(field)) {
|
|
745
|
+
skipped.push(skip2(constraint, "constraint", label, "references() is one foreign key on a column", statementFor(statements, constraint.identity.name)));
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (local.length !== 1 || local[0] === undefined || references === undefined) {
|
|
749
|
+
skipped.push(skip2(constraint, "constraint", label, "references() takes one local column", statementFor(statements, constraint.identity.name)));
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (field === undefined)
|
|
753
|
+
continue;
|
|
754
|
+
foreign.set(field, referenceCall(references));
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
skipped.push(skip2(constraint, "constraint", label, "table() cannot store this constraint", statementFor(statements, constraint.identity.name)));
|
|
758
|
+
}
|
|
759
|
+
if (uniques.length > 0) {
|
|
760
|
+
options.push(" unique: {");
|
|
761
|
+
options.push(...uniques);
|
|
762
|
+
options.push(" },");
|
|
763
|
+
}
|
|
764
|
+
if (checks.length > 0) {
|
|
765
|
+
options.push(" checks: {");
|
|
766
|
+
options.push(...checks);
|
|
767
|
+
options.push(" },");
|
|
768
|
+
}
|
|
769
|
+
const indexLines = [];
|
|
770
|
+
let needsIndex = false;
|
|
771
|
+
let needsSql = checks.length > 0;
|
|
772
|
+
for (const index of [...bag.indexes].sort((left, right) => compare2(left.identity.name, right.identity.name))) {
|
|
773
|
+
const label = `${table.identity.name}.${index.identity.name}`;
|
|
774
|
+
if (!index.definition.columns.every((name) => emitted.has(name))) {
|
|
775
|
+
skipped.push(skip2(index, "index", label, "a column of this index was not pulled", statementFor(statements, index.identity.name)));
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (index.definition.columns.length === 0) {
|
|
779
|
+
skipped.push(skip2(index, "index", label, "index() needs a column. This index is an expression", statementFor(statements, index.identity.name)));
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
indexLines.push(` ${indexCall(index, fields)},`);
|
|
783
|
+
needsIndex = true;
|
|
784
|
+
if (index.definition.predicate !== undefined || index.definition.expression !== undefined) {
|
|
785
|
+
needsSql = true;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (indexLines.length > 0) {
|
|
789
|
+
options.push(" indexes: (columns) => [");
|
|
790
|
+
options.push(...indexLines);
|
|
791
|
+
options.push(" ],");
|
|
792
|
+
}
|
|
793
|
+
for (const sequence of bag.sequences) {
|
|
794
|
+
const owned = identityColumn !== undefined && sequence.identity.name === identitySequence(table.identity.name, identityColumn.identity.name) && sequence.definition.dataType === "bigint" && sequence.definition.start === "1" && sequence.definition.increment === "1" && sequence.definition.cycle === false;
|
|
795
|
+
if (owned)
|
|
796
|
+
continue;
|
|
797
|
+
skipped.push(skip2(sequence, "sequence", sequence.identity.name, "table() has no standalone sequence. An identity column writes its own", statementFor(statements, sequence.identity.name)));
|
|
798
|
+
}
|
|
799
|
+
const columnText = rewriteForeign(columnLines, foreign);
|
|
800
|
+
const needsTable = true;
|
|
801
|
+
const imports = importLine(needsTable, needsIndex, needsSql);
|
|
802
|
+
const optionBlock = options.length === 0 ? "" : `,
|
|
803
|
+
{
|
|
804
|
+
${options.join(`
|
|
805
|
+
`)}
|
|
806
|
+
}`;
|
|
807
|
+
const text = `${imports}
|
|
808
|
+
|
|
809
|
+
export const ${exportName} = table(
|
|
810
|
+
${JSON.stringify(exportName === table.identity.name ? table.identity.name : exportName)},
|
|
811
|
+
{
|
|
812
|
+
${columnText.join(`
|
|
813
|
+
`)}
|
|
814
|
+
}${optionBlock},
|
|
815
|
+
);
|
|
816
|
+
`;
|
|
817
|
+
return {
|
|
818
|
+
exportName,
|
|
819
|
+
text,
|
|
820
|
+
...identityColumn === undefined ? {} : { identitySequence: identitySequence(table.identity.name, identityColumn.identity.name) }
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function columnSource(column, field, enums, domains) {
|
|
824
|
+
if (column.definition.identity !== undefined)
|
|
825
|
+
return;
|
|
826
|
+
if (column.definition.generated !== undefined && column.definition.generated.stored !== true) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const labels = enums.get(column.definition.dataType);
|
|
830
|
+
const domain = domains.get(column.definition.dataType);
|
|
831
|
+
let call;
|
|
832
|
+
if (labels !== undefined) {
|
|
833
|
+
const list = labels.map((label) => JSON.stringify(label)).join(", ");
|
|
834
|
+
call = `t.enum(${JSON.stringify(column.definition.dataType)}, [${list}])`;
|
|
835
|
+
} else if (domain !== undefined) {
|
|
836
|
+
const base = builderCall(domain.base, 0);
|
|
837
|
+
if (base === undefined)
|
|
838
|
+
return;
|
|
839
|
+
call = `t.domain(${JSON.stringify(column.definition.dataType)}, ${base}, ${JSON.stringify(domain.check)})`;
|
|
840
|
+
} else {
|
|
841
|
+
call = builderFor(column.definition.dataType);
|
|
842
|
+
}
|
|
843
|
+
if (call === undefined)
|
|
844
|
+
return;
|
|
845
|
+
if (field !== column.identity.name)
|
|
846
|
+
call += `.sqlName(${JSON.stringify(column.identity.name)})`;
|
|
847
|
+
if (column.definition.nullable)
|
|
848
|
+
call += ".nullable()";
|
|
849
|
+
if (column.definition.defaultExpression !== undefined) {
|
|
850
|
+
call += `.defaultSql(${JSON.stringify(column.definition.defaultExpression)})`;
|
|
851
|
+
}
|
|
852
|
+
if (column.definition.generated !== undefined) {
|
|
853
|
+
call += `.generated(${JSON.stringify(column.definition.generated.expression)})`;
|
|
854
|
+
}
|
|
855
|
+
if (column.definition.collation !== undefined)
|
|
856
|
+
return;
|
|
857
|
+
return call;
|
|
858
|
+
}
|
|
859
|
+
function builderFor(dataType) {
|
|
860
|
+
const dims = arrayDims(dataType);
|
|
861
|
+
const scalar = dims === 0 ? dataType : dataType.slice(0, dataType.length - 2 * dims);
|
|
862
|
+
const call = builderCall(scalar, dims);
|
|
863
|
+
return call;
|
|
864
|
+
}
|
|
865
|
+
function builderCall(scalar, dims) {
|
|
866
|
+
const simple = {
|
|
867
|
+
integer: "t.integer()",
|
|
868
|
+
bigint: "t.bigint()",
|
|
869
|
+
smallint: "t.smallint()",
|
|
870
|
+
text: "t.text()",
|
|
871
|
+
boolean: "t.boolean()",
|
|
872
|
+
bytea: "t.bytea()",
|
|
873
|
+
uuid: "t.uuid()",
|
|
874
|
+
jsonb: "t.jsonb()",
|
|
875
|
+
json: "t.json()",
|
|
876
|
+
real: "t.real()",
|
|
877
|
+
"double precision": "t.double()",
|
|
878
|
+
numeric: "t.numeric()",
|
|
879
|
+
date: "t.date()",
|
|
880
|
+
inet: "t.inet()",
|
|
881
|
+
cidr: "t.cidr()",
|
|
882
|
+
macaddr: "t.macaddr()",
|
|
883
|
+
macaddr8: "t.macaddr8()",
|
|
884
|
+
point: "t.point()",
|
|
885
|
+
line: "t.line()",
|
|
886
|
+
tsvector: "t.tsvector()",
|
|
887
|
+
ltree: "t.ltree()",
|
|
888
|
+
citext: "t.citext()",
|
|
889
|
+
tstzrange: "t.tstzrange()",
|
|
890
|
+
daterange: "t.daterange()",
|
|
891
|
+
int4range: "t.int4range()",
|
|
892
|
+
int8range: "t.int8range()",
|
|
893
|
+
numrange: "t.numrange()",
|
|
894
|
+
interval: "t.interval()"
|
|
895
|
+
};
|
|
896
|
+
let call = simple[scalar];
|
|
897
|
+
if (call === undefined)
|
|
898
|
+
call = typedCall(scalar);
|
|
899
|
+
if (call === undefined)
|
|
900
|
+
return;
|
|
901
|
+
if (dims === 0)
|
|
902
|
+
return call;
|
|
903
|
+
if (dims === 1)
|
|
904
|
+
return `${call}.array()`;
|
|
905
|
+
return `${call}.array({ dims: ${String(dims)} })`;
|
|
906
|
+
}
|
|
907
|
+
function typedCall(scalar) {
|
|
908
|
+
const varying = /^(?:character varying|varchar)\((\d+)\)$/.exec(scalar);
|
|
909
|
+
if (varying !== null)
|
|
910
|
+
return `t.varchar(${varying[1] ?? ""})`;
|
|
911
|
+
const fixed = /^(?:character|char)\((\d+)\)$/.exec(scalar);
|
|
912
|
+
if (fixed !== null)
|
|
913
|
+
return `t.char(${fixed[1] ?? ""})`;
|
|
914
|
+
const numeric = /^numeric\((\d+)(?:,(\d+))?\)$/.exec(scalar);
|
|
915
|
+
if (numeric !== null) {
|
|
916
|
+
const scale = numeric[2];
|
|
917
|
+
return scale === undefined ? `t.numeric(${numeric[1] ?? ""})` : `t.numeric(${numeric[1] ?? ""}, ${scale})`;
|
|
918
|
+
}
|
|
919
|
+
const zone = /^(timestamp|time)(?:\((\d+)\))? (with|without) time zone$/.exec(scalar);
|
|
920
|
+
if (zone !== null) {
|
|
921
|
+
const precision = zone[2];
|
|
922
|
+
const arg = precision === undefined ? "" : precision;
|
|
923
|
+
const name = zone[1];
|
|
924
|
+
const withZone = zone[3] === "with";
|
|
925
|
+
if (name === "timestamp" && withZone)
|
|
926
|
+
return arg.length === 0 ? "t.timestamptz()" : `t.timestamptz(${arg})`;
|
|
927
|
+
if (name === "timestamp")
|
|
928
|
+
return arg.length === 0 ? "t.timestamp()" : `t.timestamp(${arg})`;
|
|
929
|
+
if (withZone)
|
|
930
|
+
return arg.length === 0 ? "t.timetz()" : `t.timetz(${arg})`;
|
|
931
|
+
return arg.length === 0 ? "t.time()" : `t.time(${arg})`;
|
|
932
|
+
}
|
|
933
|
+
const interval = /^interval (.+)$/.exec(scalar);
|
|
934
|
+
if (interval !== null) {
|
|
935
|
+
const fields = interval[1] ?? "";
|
|
936
|
+
if (!INTERVAL_FIELDS.has(fields))
|
|
937
|
+
return;
|
|
938
|
+
return `t.interval(${JSON.stringify(fields)})`;
|
|
939
|
+
}
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
var INTERVAL_FIELDS = new Set([
|
|
943
|
+
"year",
|
|
944
|
+
"month",
|
|
945
|
+
"day",
|
|
946
|
+
"hour",
|
|
947
|
+
"minute",
|
|
948
|
+
"second",
|
|
949
|
+
"year to month",
|
|
950
|
+
"day to hour",
|
|
951
|
+
"day to minute",
|
|
952
|
+
"day to second",
|
|
953
|
+
"hour to minute",
|
|
954
|
+
"hour to second",
|
|
955
|
+
"minute to second"
|
|
956
|
+
]);
|
|
957
|
+
function arrayDims(dataType) {
|
|
958
|
+
let dims = 0;
|
|
959
|
+
let end = dataType.length;
|
|
960
|
+
while (end >= 2 && dataType.endsWith("[]", end)) {
|
|
961
|
+
dims += 1;
|
|
962
|
+
end -= 2;
|
|
963
|
+
}
|
|
964
|
+
return dims;
|
|
965
|
+
}
|
|
966
|
+
function columnReason(column) {
|
|
967
|
+
if (column.definition.identity !== undefined) {
|
|
968
|
+
return "t.identity() is generated always, bigint, and the only primary key";
|
|
969
|
+
}
|
|
970
|
+
if (column.definition.generated !== undefined && column.definition.generated.stored !== true) {
|
|
971
|
+
return "generated columns are stored";
|
|
972
|
+
}
|
|
973
|
+
if (column.definition.collation !== undefined)
|
|
974
|
+
return "table() has no collation option";
|
|
975
|
+
return `table() has no builder for ${column.definition.dataType}`;
|
|
976
|
+
}
|
|
977
|
+
function rewriteForeign(lines, foreign) {
|
|
978
|
+
return lines.map((line) => {
|
|
979
|
+
const match = /^ ([A-Za-z_$][A-Za-z0-9_$]*): (.+),$/.exec(line);
|
|
980
|
+
if (match === null)
|
|
981
|
+
return line;
|
|
982
|
+
const field = match[1] ?? "";
|
|
983
|
+
const call = foreign.get(field);
|
|
984
|
+
if (call === undefined)
|
|
985
|
+
return line;
|
|
986
|
+
return ` ${field}: ${match[2] ?? ""}${call},`;
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
function referenceCall(references) {
|
|
990
|
+
const options = [];
|
|
991
|
+
if (references.columns.length > 0) {
|
|
992
|
+
options.push(`columns: [${references.columns.map((name) => JSON.stringify(name)).join(", ")}]`);
|
|
993
|
+
}
|
|
994
|
+
if (references.onDelete !== undefined)
|
|
995
|
+
options.push(`onDelete: ${JSON.stringify(references.onDelete)}`);
|
|
996
|
+
if (references.onUpdate !== undefined)
|
|
997
|
+
options.push(`onUpdate: ${JSON.stringify(references.onUpdate)}`);
|
|
998
|
+
const optionText = options.length === 0 ? "" : `, { ${options.join(", ")} }`;
|
|
999
|
+
return `.references(${JSON.stringify(references.parent.name)}${optionText})`;
|
|
1000
|
+
}
|
|
1001
|
+
function indexCall(index, fields) {
|
|
1002
|
+
const columns = index.definition.columns.map((name) => `columns.${fields.get(name) ?? name}`).join(", ");
|
|
1003
|
+
const predicate = index.definition.predicate;
|
|
1004
|
+
const expression = index.definition.expression;
|
|
1005
|
+
if (predicate === undefined && expression === undefined) {
|
|
1006
|
+
const call = `index(${columns})`;
|
|
1007
|
+
return index.definition.unique ? `${call}.unique()` : call;
|
|
1008
|
+
}
|
|
1009
|
+
const fieldsText = [
|
|
1010
|
+
`columns: [${index.definition.columns.map((name) => `columns.${fields.get(name) ?? name}.name`).join(", ")}]`,
|
|
1011
|
+
`isUnique: ${index.definition.unique ? "true" : "false"}`
|
|
1012
|
+
];
|
|
1013
|
+
if (predicate !== undefined)
|
|
1014
|
+
fieldsText.push(`predicate: ${JSON.stringify(predicate)}`);
|
|
1015
|
+
if (expression !== undefined)
|
|
1016
|
+
fieldsText.push(`expression: ${JSON.stringify(expression)}`);
|
|
1017
|
+
return `{ ${fieldsText.join(", ")}, unique() { return this; } }`;
|
|
1018
|
+
}
|
|
1019
|
+
function plainConstraint(constraint) {
|
|
1020
|
+
return !constraint.definition.deferrable && constraint.definition.initially === "immediate" && !constraint.definition.nullsNotDistinct;
|
|
1021
|
+
}
|
|
1022
|
+
function skipChildren(table, byParent, statements, skipped) {
|
|
1023
|
+
const bag = byParent.get(table);
|
|
1024
|
+
if (bag === undefined)
|
|
1025
|
+
return;
|
|
1026
|
+
for (const column of bag.columns) {
|
|
1027
|
+
skipped.push(skip2(column, "column", `${table}.${column.identity.name}`, "the table is partitioned, and table() has no partition option", ""));
|
|
1028
|
+
}
|
|
1029
|
+
for (const constraint of bag.constraints) {
|
|
1030
|
+
skipped.push(skip2(constraint, "constraint", `${table}.${constraint.identity.name}`, "the table is partitioned, and table() has no partition option", statementFor(statements, constraint.identity.name)));
|
|
1031
|
+
}
|
|
1032
|
+
for (const index of bag.indexes) {
|
|
1033
|
+
skipped.push(skip2(index, "index", `${table}.${index.identity.name}`, "the table is partitioned, and table() has no partition option", statementFor(statements, index.identity.name)));
|
|
1034
|
+
}
|
|
1035
|
+
for (const sequence of bag.sequences) {
|
|
1036
|
+
skipped.push(skip2(sequence, "sequence", sequence.identity.name, "the owning table is partitioned, and table() has no partition option", statementFor(statements, sequence.identity.name)));
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function identitySequence(table, column) {
|
|
1040
|
+
return `${table}_${column}_seq`;
|
|
1041
|
+
}
|
|
1042
|
+
function schemaText(schemaFile, imports, extensions, routines) {
|
|
1043
|
+
const lines = ['import { schema } from "okmodel/pg";'];
|
|
1044
|
+
if (extensions.length > 0)
|
|
1045
|
+
lines.push('import { extension } from "okmodel/ext";');
|
|
1046
|
+
for (const line of routines.imports)
|
|
1047
|
+
lines.push(line);
|
|
1048
|
+
const sorted = [...imports].sort((left, right) => compare2(left.name, right.name));
|
|
1049
|
+
for (const item of sorted) {
|
|
1050
|
+
lines.push(`import { ${item.name} } from ${JSON.stringify(importSpecifier(schemaFile, item.file))};`);
|
|
1051
|
+
}
|
|
1052
|
+
const tables = sorted.map((item) => item.name).join(", ");
|
|
1053
|
+
const extensionBlock = extensions.length === 0 ? "" : `,
|
|
1054
|
+
extensions: [
|
|
1055
|
+
${extensions.map((call) => ` ${call},`).join(`
|
|
1056
|
+
`)}
|
|
1057
|
+
]`;
|
|
1058
|
+
const functionBlock = routines.functions.length === 0 ? "" : `,
|
|
1059
|
+
functions: [${routines.functions.join(", ")}]`;
|
|
1060
|
+
const triggerBlock = routines.triggers.length === 0 ? "" : `,
|
|
1061
|
+
triggers: [${routines.triggers.join(", ")}]`;
|
|
1062
|
+
const viewBlock = routines.views.length === 0 ? "" : `,
|
|
1063
|
+
views: [${routines.views.join(", ")}]`;
|
|
1064
|
+
const body = routines.body.length === 0 ? "" : `
|
|
1065
|
+
${routines.body}`;
|
|
1066
|
+
return `${lines.join(`
|
|
1067
|
+
`)}
|
|
1068
|
+
${body}
|
|
1069
|
+
export default schema({
|
|
1070
|
+
tables: [${tables}]${extensionBlock}${functionBlock}${triggerBlock}${viewBlock},
|
|
1071
|
+
});
|
|
1072
|
+
`;
|
|
1073
|
+
}
|
|
1074
|
+
function extensionCalls(source) {
|
|
1075
|
+
const calls = [];
|
|
1076
|
+
for (const object of source.objects) {
|
|
1077
|
+
if (object.kind !== "extension")
|
|
1078
|
+
continue;
|
|
1079
|
+
const options = [`schema: ${JSON.stringify(object.definition.schema)}`];
|
|
1080
|
+
if (object.definition.version !== undefined) {
|
|
1081
|
+
options.push(`version: ${JSON.stringify(object.definition.version)}`);
|
|
1082
|
+
}
|
|
1083
|
+
options.push(`relocatable: ${object.definition.relocatable ? "true" : "false"}`);
|
|
1084
|
+
calls.push(`extension(${JSON.stringify(object.identity.name)}, { ${options.join(", ")} })`);
|
|
1085
|
+
}
|
|
1086
|
+
calls.sort();
|
|
1087
|
+
return calls;
|
|
1088
|
+
}
|
|
1089
|
+
function importLine(needsTable, needsIndex, needsSql) {
|
|
1090
|
+
const names = ["t"];
|
|
1091
|
+
if (needsIndex)
|
|
1092
|
+
names.push("index");
|
|
1093
|
+
if (needsSql)
|
|
1094
|
+
names.push("sql");
|
|
1095
|
+
if (needsTable)
|
|
1096
|
+
names.push("table");
|
|
1097
|
+
names.sort();
|
|
1098
|
+
return `import { ${names.join(", ")} } from "okmodel/pg";`;
|
|
1099
|
+
}
|
|
1100
|
+
function importSpecifier(fromFile, toFile) {
|
|
1101
|
+
let spec = relative(dirname(fromFile), toFile).replaceAll("\\", "/");
|
|
1102
|
+
if (spec.endsWith(".ts"))
|
|
1103
|
+
spec = spec.slice(0, -3);
|
|
1104
|
+
if (!spec.startsWith("."))
|
|
1105
|
+
spec = `./${spec}`;
|
|
1106
|
+
return spec;
|
|
1107
|
+
}
|
|
1108
|
+
function groupByParent(source) {
|
|
1109
|
+
const map = new Map;
|
|
1110
|
+
const ensure = (name) => {
|
|
1111
|
+
const found = map.get(name);
|
|
1112
|
+
if (found !== undefined)
|
|
1113
|
+
return found;
|
|
1114
|
+
const bag = { columns: [], constraints: [], indexes: [], sequences: [] };
|
|
1115
|
+
map.set(name, bag);
|
|
1116
|
+
return bag;
|
|
1117
|
+
};
|
|
1118
|
+
for (const object of source.objects) {
|
|
1119
|
+
if (object.kind === "column" || object.kind === "constraint" || object.kind === "index") {
|
|
1120
|
+
const bag = ensure(object.identity.parent.name);
|
|
1121
|
+
if (object.kind === "column")
|
|
1122
|
+
bag.columns.push(object);
|
|
1123
|
+
else if (object.kind === "constraint")
|
|
1124
|
+
bag.constraints.push(object);
|
|
1125
|
+
else
|
|
1126
|
+
bag.indexes.push(object);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (object.kind === "sequence") {
|
|
1130
|
+
const owned = object.dependencies.find((edge) => edge.target.kind === "table");
|
|
1131
|
+
if (owned === undefined || owned.target.kind !== "table")
|
|
1132
|
+
continue;
|
|
1133
|
+
ensure(owned.target.name).sequences.push(object);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return map;
|
|
1137
|
+
}
|
|
1138
|
+
function enumLabels(source) {
|
|
1139
|
+
const labels = new Map;
|
|
1140
|
+
for (const object of source.objects) {
|
|
1141
|
+
if (object.kind !== "type" || isDomain(object.definition))
|
|
1142
|
+
continue;
|
|
1143
|
+
labels.set(object.identity.name, object.definition.labels);
|
|
1144
|
+
}
|
|
1145
|
+
return labels;
|
|
1146
|
+
}
|
|
1147
|
+
function domainDefs(source) {
|
|
1148
|
+
const domains = new Map;
|
|
1149
|
+
for (const object of source.objects) {
|
|
1150
|
+
if (object.kind !== "type" || !isDomain(object.definition))
|
|
1151
|
+
continue;
|
|
1152
|
+
domains.set(object.identity.name, {
|
|
1153
|
+
base: object.definition.base,
|
|
1154
|
+
check: object.definition.check
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
return domains;
|
|
1158
|
+
}
|
|
1159
|
+
function typeIsUsed(name, source, skipped) {
|
|
1160
|
+
const skippedKeys = new Set(skipped.map((item) => item.key));
|
|
1161
|
+
for (const object of source.objects) {
|
|
1162
|
+
if (object.kind !== "column")
|
|
1163
|
+
continue;
|
|
1164
|
+
if (skippedKeys.has(identityKey2(object.identity)))
|
|
1165
|
+
continue;
|
|
1166
|
+
if (object.definition.dataType === name)
|
|
1167
|
+
return true;
|
|
1168
|
+
if (object.dependencies.some((edge) => edge.target.kind === "type" && edge.target.name === name)) {
|
|
1169
|
+
return true;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
function rendered(source) {
|
|
1175
|
+
try {
|
|
1176
|
+
return renderCatalog2(source, "public");
|
|
1177
|
+
} catch {
|
|
1178
|
+
return [];
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
function statementFor(statements, name) {
|
|
1182
|
+
if (name.length === 0)
|
|
1183
|
+
return "";
|
|
1184
|
+
const quoted = `"${name.replaceAll('"', '""')}"`;
|
|
1185
|
+
return statements.find((statement) => statement.includes(quoted)) ?? "";
|
|
1186
|
+
}
|
|
1187
|
+
function skip2(object, kind, label, reason, sql) {
|
|
1188
|
+
return { key: identityKey2(object.identity), kind, label, reason, sql };
|
|
1189
|
+
}
|
|
1190
|
+
function isDeferred(kind) {
|
|
1191
|
+
return DEFERRED.some((item) => item.kind === kind);
|
|
1192
|
+
}
|
|
1193
|
+
function deferredReason(kind) {
|
|
1194
|
+
return DEFERRED.find((item) => item.kind === kind)?.reason ?? "a later step writes this object";
|
|
1195
|
+
}
|
|
1196
|
+
function kindLabel(kind) {
|
|
1197
|
+
if (kind === "materializedView")
|
|
1198
|
+
return "materialized views";
|
|
1199
|
+
if (kind === "defaultPrivilege")
|
|
1200
|
+
return "default privileges";
|
|
1201
|
+
if (kind === "eventTrigger")
|
|
1202
|
+
return "event triggers";
|
|
1203
|
+
if (kind === "policy")
|
|
1204
|
+
return "policies";
|
|
1205
|
+
if (kind.endsWith("s"))
|
|
1206
|
+
return kind;
|
|
1207
|
+
return `${kind}s`;
|
|
1208
|
+
}
|
|
1209
|
+
function counted(count, one, many) {
|
|
1210
|
+
return `${String(count)} ${count === 1 ? one : many}`;
|
|
1211
|
+
}
|
|
1212
|
+
function objectLabel(object) {
|
|
1213
|
+
if ("name" in object.identity && typeof object.identity.name === "string") {
|
|
1214
|
+
if ("parent" in object.identity)
|
|
1215
|
+
return `${object.identity.parent.name}.${object.identity.name}`;
|
|
1216
|
+
return object.identity.name;
|
|
1217
|
+
}
|
|
1218
|
+
return object.kind;
|
|
1219
|
+
}
|
|
1220
|
+
function objectName(object) {
|
|
1221
|
+
if ("name" in object.identity && typeof object.identity.name === "string")
|
|
1222
|
+
return object.identity.name;
|
|
1223
|
+
return "";
|
|
1224
|
+
}
|
|
1225
|
+
function uniqueFile(name, used) {
|
|
1226
|
+
let file = name;
|
|
1227
|
+
let salt = 2;
|
|
1228
|
+
while (used.has(file)) {
|
|
1229
|
+
file = `${name}_${String(salt)}`;
|
|
1230
|
+
salt += 1;
|
|
1231
|
+
}
|
|
1232
|
+
used.add(file);
|
|
1233
|
+
return file;
|
|
1234
|
+
}
|
|
1235
|
+
function tableExportNames(source) {
|
|
1236
|
+
const names = new Map;
|
|
1237
|
+
for (const object of source.objects) {
|
|
1238
|
+
if (object.kind !== "table" || object.definition.partition !== undefined)
|
|
1239
|
+
continue;
|
|
1240
|
+
names.set(object.identity.name, exportNameOf(object.identity.name));
|
|
1241
|
+
}
|
|
1242
|
+
return names;
|
|
1243
|
+
}
|
|
1244
|
+
function exportNameOf(name) {
|
|
1245
|
+
const sqlName = jsIdent(name);
|
|
1246
|
+
if (sqlName === name)
|
|
1247
|
+
return name;
|
|
1248
|
+
return freshIdent(name, new Set);
|
|
1249
|
+
}
|
|
1250
|
+
function declaredIdent(source, fallback) {
|
|
1251
|
+
if (source === undefined)
|
|
1252
|
+
return fallback;
|
|
1253
|
+
const field = prefixed(source, "field:");
|
|
1254
|
+
if (field !== undefined)
|
|
1255
|
+
return field;
|
|
1256
|
+
const exported = prefixed(source, "export:");
|
|
1257
|
+
if (exported !== undefined)
|
|
1258
|
+
return exported;
|
|
1259
|
+
return fallback;
|
|
1260
|
+
}
|
|
1261
|
+
function prefixed(source, prefix) {
|
|
1262
|
+
if (!source.startsWith(prefix))
|
|
1263
|
+
return;
|
|
1264
|
+
const name = source.slice(prefix.length);
|
|
1265
|
+
return name.length > 0 ? name : undefined;
|
|
1266
|
+
}
|
|
1267
|
+
function freshIdent(sql, used) {
|
|
1268
|
+
let name = jsIdent(sql);
|
|
1269
|
+
if (name === "table" || name === "columns")
|
|
1270
|
+
name = `${name}Name`;
|
|
1271
|
+
let salt = 2;
|
|
1272
|
+
while (used.has(name)) {
|
|
1273
|
+
name = `${jsIdent(sql)}_${String(salt)}`;
|
|
1274
|
+
salt += 1;
|
|
1275
|
+
}
|
|
1276
|
+
used.add(name);
|
|
1277
|
+
return name;
|
|
1278
|
+
}
|
|
1279
|
+
function jsIdent(sql) {
|
|
1280
|
+
let out = "";
|
|
1281
|
+
for (const char of sql) {
|
|
1282
|
+
const ok = char >= "A" && char <= "Z" || char >= "a" && char <= "z" || char >= "0" && char <= "9" || char === "_";
|
|
1283
|
+
out += ok ? char : "_";
|
|
1284
|
+
}
|
|
1285
|
+
if (out.length === 0)
|
|
1286
|
+
out = "table";
|
|
1287
|
+
const first = out[0] ?? "";
|
|
1288
|
+
if (first >= "0" && first <= "9")
|
|
1289
|
+
out = `t_${out}`;
|
|
1290
|
+
if (RESERVED.has(out))
|
|
1291
|
+
out = `${out}Name`;
|
|
1292
|
+
return out;
|
|
1293
|
+
}
|
|
1294
|
+
function jsKey(name) {
|
|
1295
|
+
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED.has(name))
|
|
1296
|
+
return name;
|
|
1297
|
+
return JSON.stringify(name);
|
|
1298
|
+
}
|
|
1299
|
+
function sqlTemplate(expression) {
|
|
1300
|
+
const body = expression.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${");
|
|
1301
|
+
return `sql\`${body}\``;
|
|
1302
|
+
}
|
|
1303
|
+
function compare2(left, right) {
|
|
1304
|
+
if (left < right)
|
|
1305
|
+
return -1;
|
|
1306
|
+
if (left > right)
|
|
1307
|
+
return 1;
|
|
1308
|
+
return 0;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/tooling/migrate/pull-verify.ts
|
|
1312
|
+
var SCRATCH = "okm_pull_verify";
|
|
1313
|
+
async function verifyPulled(runner, source, plan) {
|
|
1314
|
+
if (plan.functions.length === 0 && plan.triggers.length === 0 && plan.views.length === 0) {
|
|
1315
|
+
return [];
|
|
1316
|
+
}
|
|
1317
|
+
await dropScratch(runner);
|
|
1318
|
+
try {
|
|
1319
|
+
await runner.query(`create schema ${quoteIdent2(SCRATCH)}`);
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
return verdictsWithoutScratch(plan, firstLine(error));
|
|
1322
|
+
}
|
|
1323
|
+
const savedPath = await setting(runner, "search_path");
|
|
1324
|
+
const savedBodies = await setting(runner, "check_function_bodies");
|
|
1325
|
+
const path = `${quoteIdent2(SCRATCH)}, public`;
|
|
1326
|
+
try {
|
|
1327
|
+
await runner.query(`select set_config('check_function_bodies', 'on', false)`);
|
|
1328
|
+
await runner.query(`select set_config('search_path', $1, false)`, [path]);
|
|
1329
|
+
await createTypes(runner, source);
|
|
1330
|
+
const routinePath = `${quoteIdent2(SCRATCH)}, pg_catalog`;
|
|
1331
|
+
const failures = new Map;
|
|
1332
|
+
for (const object of ordered(source, plan)) {
|
|
1333
|
+
if (!creates(object, source, plan))
|
|
1334
|
+
continue;
|
|
1335
|
+
const statement = object.kind === "table" ? createTableSql2(object, source, SCRATCH) : createObjectSql2(object, SCRATCH);
|
|
1336
|
+
if (statement === undefined)
|
|
1337
|
+
continue;
|
|
1338
|
+
const routine = object.kind === "function" || object.kind === "view" || object.kind === "materializedView" || object.kind === "trigger";
|
|
1339
|
+
if (routine)
|
|
1340
|
+
await runner.query(`select set_config('search_path', $1, false)`, [routinePath]);
|
|
1341
|
+
else
|
|
1342
|
+
await runner.query(`select set_config('search_path', $1, false)`, [path]);
|
|
1343
|
+
try {
|
|
1344
|
+
await runner.query(statement);
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
failures.set(identityKey2(object.identity), firstLine(error));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return verdicts(plan, failures);
|
|
1350
|
+
} finally {
|
|
1351
|
+
await runner.query(`select set_config('search_path', $1, false)`, [savedPath]).catch(() => {
|
|
1352
|
+
return;
|
|
1353
|
+
});
|
|
1354
|
+
await runner.query(`select set_config('check_function_bodies', $1, false)`, [savedBodies]).catch(() => {
|
|
1355
|
+
return;
|
|
1356
|
+
});
|
|
1357
|
+
await dropScratch(runner);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
function ordered(source, plan) {
|
|
1361
|
+
const augmented = augment(source, plan.orderEdges);
|
|
1362
|
+
try {
|
|
1363
|
+
return dependencyOrder(augmented);
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
if (error instanceof OkmError2 && error.code === "OKM1026")
|
|
1366
|
+
return dependencyOrder(source.objects);
|
|
1367
|
+
throw error;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
function augment(source, edges) {
|
|
1371
|
+
return source.objects.map((object) => {
|
|
1372
|
+
const extra = edges.get(identityKey2(object.identity));
|
|
1373
|
+
if (extra === undefined || extra.length === 0)
|
|
1374
|
+
return object;
|
|
1375
|
+
const dependencies = [...object.dependencies.map((edge) => edge.target), ...extra];
|
|
1376
|
+
if (object.kind === "function")
|
|
1377
|
+
return copyFunction(object, dependencies);
|
|
1378
|
+
if (object.kind === "view")
|
|
1379
|
+
return copyView(object, dependencies);
|
|
1380
|
+
if (object.kind === "materializedView")
|
|
1381
|
+
return copyMatview(object, dependencies);
|
|
1382
|
+
return object;
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
function copyFunction(object, dependencies) {
|
|
1386
|
+
return functionObject({
|
|
1387
|
+
namespace: object.identity.namespace,
|
|
1388
|
+
name: object.identity.name,
|
|
1389
|
+
arguments: object.definition.arguments,
|
|
1390
|
+
returns: object.definition.returns,
|
|
1391
|
+
language: object.definition.language,
|
|
1392
|
+
volatility: object.definition.volatility,
|
|
1393
|
+
security: object.definition.security,
|
|
1394
|
+
body: object.definition.body,
|
|
1395
|
+
...object.definition.searchPath !== undefined ? { searchPath: object.definition.searchPath } : {},
|
|
1396
|
+
...object.definition.atomic === true ? { atomic: true } : {},
|
|
1397
|
+
owner: object.owner,
|
|
1398
|
+
provenance: object.provenance,
|
|
1399
|
+
dependencies
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
function copyView(object, dependencies) {
|
|
1403
|
+
return viewObject({
|
|
1404
|
+
namespace: object.identity.namespace,
|
|
1405
|
+
name: object.identity.name,
|
|
1406
|
+
columns: object.definition.columns,
|
|
1407
|
+
query: object.definition.query,
|
|
1408
|
+
...object.definition.securityInvoker === true ? { securityInvoker: true } : {},
|
|
1409
|
+
owner: object.owner,
|
|
1410
|
+
provenance: object.provenance,
|
|
1411
|
+
dependencies
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
function copyMatview(object, dependencies) {
|
|
1415
|
+
return materializedViewObject({
|
|
1416
|
+
namespace: object.identity.namespace,
|
|
1417
|
+
name: object.identity.name,
|
|
1418
|
+
columns: object.definition.columns,
|
|
1419
|
+
query: object.definition.query,
|
|
1420
|
+
...object.definition.refresh !== undefined ? { refresh: object.definition.refresh } : {},
|
|
1421
|
+
owner: object.owner,
|
|
1422
|
+
provenance: object.provenance,
|
|
1423
|
+
dependencies
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
function creates(object, source, plan) {
|
|
1427
|
+
if (object.kind === "type")
|
|
1428
|
+
return false;
|
|
1429
|
+
if (object.kind === "column")
|
|
1430
|
+
return false;
|
|
1431
|
+
if (object.kind === "constraint" && object.definition.constraintKind === "primaryKey")
|
|
1432
|
+
return false;
|
|
1433
|
+
if (object.kind === "sequence" && identitySequence2(object, source))
|
|
1434
|
+
return false;
|
|
1435
|
+
if (object.kind === "table")
|
|
1436
|
+
return object.definition.partition === undefined;
|
|
1437
|
+
if (object.kind === "extension" || object.kind === "policy" || object.kind === "role")
|
|
1438
|
+
return false;
|
|
1439
|
+
if (object.kind === "grant" || object.kind === "defaultPrivilege")
|
|
1440
|
+
return false;
|
|
1441
|
+
const key = identityKey2(object.identity);
|
|
1442
|
+
if (object.kind === "function")
|
|
1443
|
+
return plan.functions.some((item) => identityKey2(item.object.identity) === key);
|
|
1444
|
+
if (object.kind === "trigger")
|
|
1445
|
+
return plan.triggers.some((item) => identityKey2(item.object.identity) === key);
|
|
1446
|
+
if (object.kind === "view" || object.kind === "materializedView") {
|
|
1447
|
+
return plan.views.some((item) => identityKey2(item.object.identity) === key);
|
|
1448
|
+
}
|
|
1449
|
+
return true;
|
|
1450
|
+
}
|
|
1451
|
+
async function createTypes(runner, source) {
|
|
1452
|
+
const types = source.objects.filter((object) => object.kind === "type");
|
|
1453
|
+
const enums = types.filter((object) => object.kind === "type" && !isDomain(object.definition));
|
|
1454
|
+
const domains = types.filter((object) => object.kind === "type" && isDomain(object.definition));
|
|
1455
|
+
for (const object of [...enums, ...domains]) {
|
|
1456
|
+
const statement = createObjectSql2(object, SCRATCH);
|
|
1457
|
+
if (statement !== undefined)
|
|
1458
|
+
await runner.query(statement);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
function verdicts(plan, failures) {
|
|
1462
|
+
const verdicts = [];
|
|
1463
|
+
for (const item of plan.functions) {
|
|
1464
|
+
const key = identityKey2(item.object.identity);
|
|
1465
|
+
const failed = failures.get(key);
|
|
1466
|
+
const plpgsql = item.object.definition.language === "plpgsql";
|
|
1467
|
+
let status = "verified";
|
|
1468
|
+
if (failed !== undefined)
|
|
1469
|
+
status = "unverified";
|
|
1470
|
+
else if (plpgsql)
|
|
1471
|
+
status = "candidate-only";
|
|
1472
|
+
verdicts.push({
|
|
1473
|
+
key,
|
|
1474
|
+
kind: "function",
|
|
1475
|
+
label: `${item.object.identity.name}(${item.object.identity.argTypes.join(", ")})`,
|
|
1476
|
+
status,
|
|
1477
|
+
detail: failed ?? ""
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
for (const item of plan.triggers) {
|
|
1481
|
+
const key = identityKey2(item.object.identity);
|
|
1482
|
+
const failed = failures.get(key);
|
|
1483
|
+
verdicts.push({
|
|
1484
|
+
key,
|
|
1485
|
+
kind: "trigger",
|
|
1486
|
+
label: `${item.object.identity.parent.name}.${item.object.identity.name}`,
|
|
1487
|
+
status: failed !== undefined ? "unverified" : "verified",
|
|
1488
|
+
detail: failed ?? ""
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
for (const item of plan.views) {
|
|
1492
|
+
const key = identityKey2(item.object.identity);
|
|
1493
|
+
const failed = failures.get(key);
|
|
1494
|
+
verdicts.push({
|
|
1495
|
+
key,
|
|
1496
|
+
kind: item.object.kind,
|
|
1497
|
+
label: item.object.identity.name,
|
|
1498
|
+
status: failed !== undefined ? "unverified" : "verified",
|
|
1499
|
+
detail: failed ?? ""
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
return verdicts;
|
|
1503
|
+
}
|
|
1504
|
+
async function setting(runner, name) {
|
|
1505
|
+
const rows = await runner.query(`select current_setting($1) as value`, [name]);
|
|
1506
|
+
return cellText(rows[0]?.value);
|
|
1507
|
+
}
|
|
1508
|
+
async function dropScratch(runner) {
|
|
1509
|
+
await runner.query(`drop schema if exists ${quoteIdent2(SCRATCH)} cascade`).catch(() => {
|
|
1510
|
+
return;
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
function firstLine(error) {
|
|
1514
|
+
const message = error instanceof Error && error.message.length > 0 ? error.message : "create failed";
|
|
1515
|
+
const line = message.split(`
|
|
1516
|
+
`)[0] ?? message;
|
|
1517
|
+
return line.length > 200 ? `${line.slice(0, 200)}…` : line;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/tooling/migrate/pull.ts
|
|
1521
|
+
async function pullProject2(cwd, argv) {
|
|
1522
|
+
const flags = parsePullArgs(argv);
|
|
1523
|
+
const config = await loadConfig(cwd);
|
|
1524
|
+
const target = selectTarget(config, flags.target);
|
|
1525
|
+
assertTargetPolicy(target, "pull", flags.allowProtected);
|
|
1526
|
+
const layout = layoutOf(cwd, config.schema, config.tables, flags.out);
|
|
1527
|
+
const sql = postgres(target.url, {
|
|
1528
|
+
max: 1,
|
|
1529
|
+
connect_timeout: 5,
|
|
1530
|
+
idle_timeout: 1,
|
|
1531
|
+
onnotice: () => {}
|
|
1532
|
+
});
|
|
1533
|
+
try {
|
|
1534
|
+
const read = await readCatalog(sql, flags.schemas);
|
|
1535
|
+
const query = queryOf(sql);
|
|
1536
|
+
const plan = planRoutines(read.catalog, tableExportNames(read.catalog));
|
|
1537
|
+
const verdicts = await verifyPulled(query, read.catalog, plan);
|
|
1538
|
+
const unexpressed = await countUnexpressed(query, read.schemas);
|
|
1539
|
+
const emitted = emitPull(read.catalog, layout, { plan, verdicts, unexpressed });
|
|
1540
|
+
const tables = emitted.files.filter((file) => file.path !== layout.schemaFile);
|
|
1541
|
+
if (tables.length === 0) {
|
|
1542
|
+
const name = read.schemas.join(", ");
|
|
1543
|
+
throw new OkmError2("invalid", `Namespace ${name} has no tables.`, {
|
|
1544
|
+
fix: { summary: "Pass --schema <name> for a namespace that has tables." }
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
commitPullFiles2(emitted.files, "Pull");
|
|
1548
|
+
return {
|
|
1549
|
+
text: formatPullReport(emitted.counts, emitted.skipped, emitted.verdicts, unexpressed),
|
|
1550
|
+
skipped: emitted.skipped
|
|
1551
|
+
};
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
if (error instanceof OkmError2)
|
|
1554
|
+
throw error;
|
|
1555
|
+
throw sessionFailure(error) ?? unavailable(error);
|
|
1556
|
+
} finally {
|
|
1557
|
+
await finishQuiet(() => sql.end({ timeout: 5 }));
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function parsePullArgs(argv) {
|
|
1561
|
+
let target;
|
|
1562
|
+
let out;
|
|
1563
|
+
let allowProtected = false;
|
|
1564
|
+
const schemas = [];
|
|
1565
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
1566
|
+
const arg = argv[index] ?? "";
|
|
1567
|
+
if (arg === "--allow-protected") {
|
|
1568
|
+
allowProtected = true;
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
if (arg === "--target" || arg === "--schema" || arg === "--out") {
|
|
1572
|
+
const value = argv[index + 1];
|
|
1573
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1574
|
+
throw new OkmError2("invalid", `${arg} needs a value.`, {
|
|
1575
|
+
fix: { summary: `Pass ${arg} <value>.` }
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
if (arg === "--target")
|
|
1579
|
+
target = value;
|
|
1580
|
+
else if (arg === "--out")
|
|
1581
|
+
out = value;
|
|
1582
|
+
else
|
|
1583
|
+
schemas.push(...splitSchemas(value));
|
|
1584
|
+
index += 1;
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
if (arg.startsWith("--target=")) {
|
|
1588
|
+
target = arg.slice("--target=".length);
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
if (arg.startsWith("--out=")) {
|
|
1592
|
+
out = arg.slice("--out=".length);
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
if (arg.startsWith("--schema=")) {
|
|
1596
|
+
schemas.push(...splitSchemas(arg.slice("--schema=".length)));
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
throw new OkmError2("invalid", `Unknown flag ${arg}.`, {
|
|
1600
|
+
fix: { summary: "okm pull accepts --target, --schema, --out, and --allow-protected." }
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
return { target, allowProtected, out, schemas };
|
|
1604
|
+
}
|
|
1605
|
+
async function readCatalog(sql, schemas) {
|
|
1606
|
+
try {
|
|
1607
|
+
const chosen = schemas.length > 0 ? schemas : [await currentSchema(sql)];
|
|
1608
|
+
const objects = [];
|
|
1609
|
+
for (const name of chosen) {
|
|
1610
|
+
const live = await introspectSchema(queryOf(sql), name, "public");
|
|
1611
|
+
objects.push(...live.objects);
|
|
1612
|
+
}
|
|
1613
|
+
return { catalog: catalog(objects), schemas: chosen };
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
if (error instanceof OkmError2)
|
|
1616
|
+
throw error;
|
|
1617
|
+
throw sessionFailure(error) ?? unavailable(error);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
var UNEXPRESSED = `
|
|
1621
|
+
select
|
|
1622
|
+
(select count(*)::int from pg_proc p
|
|
1623
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
1624
|
+
where n.nspname = $1 and p.prokind = 'a') as aggregates,
|
|
1625
|
+
(select count(*)::int from pg_proc p
|
|
1626
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
1627
|
+
where n.nspname = $1 and p.prokind = 'p') as procedures,
|
|
1628
|
+
(select count(*)::int from pg_event_trigger) as event_triggers,
|
|
1629
|
+
(select count(*)::int from pg_rewrite r
|
|
1630
|
+
join pg_class c on c.oid = r.ev_class
|
|
1631
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
1632
|
+
where n.nspname = $1 and r.rulename <> '_RETURN') as rules
|
|
1633
|
+
`;
|
|
1634
|
+
async function countUnexpressed(query, schemas) {
|
|
1635
|
+
let aggregates = 0;
|
|
1636
|
+
let procedures = 0;
|
|
1637
|
+
let eventTriggers = 0;
|
|
1638
|
+
let rules = 0;
|
|
1639
|
+
for (const name of schemas) {
|
|
1640
|
+
const rows = await query.query(UNEXPRESSED, [name]);
|
|
1641
|
+
const row = rows[0];
|
|
1642
|
+
aggregates += countCell(row?.aggregates);
|
|
1643
|
+
procedures += countCell(row?.procedures);
|
|
1644
|
+
eventTriggers = countCell(row?.event_triggers);
|
|
1645
|
+
rules += countCell(row?.rules);
|
|
1646
|
+
}
|
|
1647
|
+
return [
|
|
1648
|
+
{ kind: "aggregate", count: aggregates, reason: "the catalog has no aggregate kind" },
|
|
1649
|
+
{ kind: "procedure", count: procedures, reason: "the catalog has no procedure kind" },
|
|
1650
|
+
{
|
|
1651
|
+
kind: "eventTrigger",
|
|
1652
|
+
count: eventTriggers,
|
|
1653
|
+
reason: "the catalog has no event trigger kind"
|
|
1654
|
+
},
|
|
1655
|
+
{ kind: "rule", count: rules, reason: "the catalog has no rule kind" }
|
|
1656
|
+
];
|
|
1657
|
+
}
|
|
1658
|
+
function countCell(value) {
|
|
1659
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1660
|
+
return value;
|
|
1661
|
+
if (typeof value === "bigint")
|
|
1662
|
+
return Number(value);
|
|
1663
|
+
if (typeof value === "string" && value.length > 0)
|
|
1664
|
+
return Number(value);
|
|
1665
|
+
return 0;
|
|
1666
|
+
}
|
|
1667
|
+
async function currentSchema(sql) {
|
|
1668
|
+
const rows = await sql`select current_schema() as name`;
|
|
1669
|
+
const name = rows[0]?.name;
|
|
1670
|
+
if (name === null || name === undefined || name.length === 0) {
|
|
1671
|
+
throw new OkmError2("invalid", "The target has no current schema.", {
|
|
1672
|
+
fix: { summary: "Pass --schema <name>." }
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
return name;
|
|
1676
|
+
}
|
|
1677
|
+
function queryOf(sql) {
|
|
1678
|
+
return {
|
|
1679
|
+
async query(text, params) {
|
|
1680
|
+
const rows = await sql.unsafe(text, params === undefined ? undefined : [...params]);
|
|
1681
|
+
return rows.map((row) => {
|
|
1682
|
+
const copy = {};
|
|
1683
|
+
for (const [key, value] of Object.entries(row))
|
|
1684
|
+
copy[key] = value;
|
|
1685
|
+
return copy;
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
function pullLayout2(cwd, schema, tables, out) {
|
|
1691
|
+
return layoutOf(cwd, schema, tables, out);
|
|
1692
|
+
}
|
|
1693
|
+
function commitPullFiles2(files, verb = "Pull") {
|
|
1694
|
+
refuseExisting(files, verb);
|
|
1695
|
+
for (const file of files) {
|
|
1696
|
+
mkdirSync(dirname2(file.path), { recursive: true });
|
|
1697
|
+
writeFileSync(file.path, file.text);
|
|
1698
|
+
}
|
|
1699
|
+
formatFiles(files.map((file) => file.path));
|
|
1700
|
+
}
|
|
1701
|
+
function layoutOf(cwd, schema, tables, out) {
|
|
1702
|
+
if (out !== undefined) {
|
|
1703
|
+
const root = projectPath(cwd, out);
|
|
1704
|
+
return { schemaFile: join(root, "schema.ts"), tablesDir: join(root, "tables") };
|
|
1705
|
+
}
|
|
1706
|
+
const schemaFile = projectPath(cwd, schema);
|
|
1707
|
+
const tablesDir = tables !== undefined ? projectPath(cwd, tables) : join(dirname2(schemaFile), "tables");
|
|
1708
|
+
return { schemaFile, tablesDir };
|
|
1709
|
+
}
|
|
1710
|
+
function refuseExisting(files, verb) {
|
|
1711
|
+
for (const file of files) {
|
|
1712
|
+
if (!existsSync(file.path))
|
|
1713
|
+
continue;
|
|
1714
|
+
throw new OkmError2("OKM1855", `${verb} would overwrite ${file.path}.`, {
|
|
1715
|
+
fix: {
|
|
1716
|
+
summary: `Pass --out <directory> to write somewhere else, or move ${file.path}.`
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
function formatFiles(files) {
|
|
1722
|
+
if (files.length === 0)
|
|
1723
|
+
return;
|
|
1724
|
+
const root = formatterRoot(dirname2(files[0] ?? "."));
|
|
1725
|
+
const binary = join(root, "node_modules", ".bin", "oxfmt");
|
|
1726
|
+
const result = spawnSync(binary, ["--write", ...files], {
|
|
1727
|
+
cwd: root,
|
|
1728
|
+
encoding: "utf8"
|
|
1729
|
+
});
|
|
1730
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
1731
|
+
const detail = result.stderr || result.stdout || "oxfmt failed";
|
|
1732
|
+
throw new OkmError2("invalid", `The pulled files were not formatted. ${detail.trim()}`, {
|
|
1733
|
+
fix: { summary: "Run oxfmt on the pulled files, or fix the formatter settings." }
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function formatterRoot(start) {
|
|
1738
|
+
let dir = start;
|
|
1739
|
+
for (;; ) {
|
|
1740
|
+
if (existsSync(join(dir, ".oxfmtrc.json")))
|
|
1741
|
+
return dir;
|
|
1742
|
+
const parent = dirname2(dir);
|
|
1743
|
+
if (parent === dir)
|
|
1744
|
+
return start;
|
|
1745
|
+
dir = parent;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
function splitSchemas(value) {
|
|
1749
|
+
const names = value.split(",").map((name) => name.trim()).filter((name) => name.length > 0);
|
|
1750
|
+
if (names.length === 0) {
|
|
1751
|
+
throw new OkmError2("invalid", "--schema needs a name.", {
|
|
1752
|
+
fix: { summary: "Pass --schema <name>. Repeat the flag to read more than one." }
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
return names;
|
|
1756
|
+
}
|
|
1757
|
+
function unavailable(error) {
|
|
1758
|
+
const message = error instanceof Error && error.message.length > 0 ? error.message : "The database is unavailable.";
|
|
1759
|
+
return new OkmError2("unavailable", message, {
|
|
1760
|
+
kind: "unavailable",
|
|
1761
|
+
cause: error,
|
|
1762
|
+
fix: { summary: "Check the target URL. okm pull only reads." }
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
export { emitPull, formatPullReport, pullProject2, pullLayout2, commitPullFiles2 };
|