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
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
VALIDATION_NOT_IMPORTED2,
|
|
3
|
-
VALIDATION_NOT_IMPORTED_FIX2
|
|
4
|
-
} from "./n442h54c.js";
|
|
5
1
|
import {
|
|
6
2
|
roleObject,
|
|
7
3
|
grantObject,
|
|
@@ -9,12 +5,20 @@ import {
|
|
|
9
5
|
serializeCatalog2,
|
|
10
6
|
catalogHash2,
|
|
11
7
|
startupCatalog2,
|
|
8
|
+
startupHash2,
|
|
12
9
|
catalogFileText2,
|
|
13
10
|
parseCatalog2
|
|
14
11
|
} from "./12z7aznj.js";
|
|
12
|
+
import {
|
|
13
|
+
quoteIdent2
|
|
14
|
+
} from "./fk9vef30.js";
|
|
15
|
+
import {
|
|
16
|
+
VALIDATION_NOT_IMPORTED2,
|
|
17
|
+
VALIDATION_NOT_IMPORTED_FIX2
|
|
18
|
+
} from "./n442h54c.js";
|
|
15
19
|
import {
|
|
16
20
|
open2
|
|
17
|
-
} from "./
|
|
21
|
+
} from "./dd5pmnky.js";
|
|
18
22
|
import {
|
|
19
23
|
sqlStatements,
|
|
20
24
|
transactionControl,
|
|
@@ -35,7 +39,7 @@ import {
|
|
|
35
39
|
assertTargetAlias,
|
|
36
40
|
listTargets,
|
|
37
41
|
selectTarget
|
|
38
|
-
} from "./
|
|
42
|
+
} from "./6nae0spm.js";
|
|
39
43
|
import {
|
|
40
44
|
contributedFields
|
|
41
45
|
} from "./n0dgs0pb.js";
|
|
@@ -78,6 +82,374 @@ function projectPath(cwd, ...parts) {
|
|
|
78
82
|
return resolve(cwd, ...parts);
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
// src/tooling/migrate/export.ts
|
|
86
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
87
|
+
import { isAbsolute, join } from "node:path";
|
|
88
|
+
var EXPORT_FORMAT = 1;
|
|
89
|
+
var LOGICAL_FORMAT = 1;
|
|
90
|
+
function snapshotFromCatalog(source, namespace, database) {
|
|
91
|
+
return {
|
|
92
|
+
format: EXPORT_FORMAT,
|
|
93
|
+
logical: { namespace, name: namespace, format: LOGICAL_FORMAT },
|
|
94
|
+
catalogHash: startupHash2(source),
|
|
95
|
+
...database !== undefined ? { database } : {},
|
|
96
|
+
tables: exportTables(source)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function serializeSnapshot(snapshot) {
|
|
100
|
+
return `${JSON.stringify(snapshot, null, 2)}
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
function parseSnapshot(path, text) {
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(text);
|
|
107
|
+
} catch {
|
|
108
|
+
throw snapshotError(path, "stale", `Catalog snapshot ${path} is not JSON.`);
|
|
109
|
+
}
|
|
110
|
+
if (!isSnapshot(parsed)) {
|
|
111
|
+
throw snapshotError(path, "stale", `Catalog snapshot ${path} is stale: the file format is not ${String(EXPORT_FORMAT)}.`);
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
function databaseName(url) {
|
|
116
|
+
try {
|
|
117
|
+
const name = decodeURIComponent(new URL(url).pathname.replace(/^\//, ""));
|
|
118
|
+
return name.length > 0 ? name : undefined;
|
|
119
|
+
} catch {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function catalogNamespace(config) {
|
|
124
|
+
return config.namespace ?? "public";
|
|
125
|
+
}
|
|
126
|
+
function loadSnapshots(cwd, config) {
|
|
127
|
+
const paths = config.external ?? [];
|
|
128
|
+
return paths.map((path) => {
|
|
129
|
+
const file = resolveSnapshot(cwd, path);
|
|
130
|
+
if (!existsSync(file)) {
|
|
131
|
+
throw snapshotError(path, "missing", `Catalog snapshot ${path} is missing.`);
|
|
132
|
+
}
|
|
133
|
+
return { path, snapshot: parseSnapshot(path, readFileSync(file, "utf8")) };
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function externalNamespaceMap(snapshots) {
|
|
137
|
+
const names = new Map;
|
|
138
|
+
for (const loaded of snapshots) {
|
|
139
|
+
for (const table of loaded.snapshot.tables)
|
|
140
|
+
names.set(table.name, table.namespace);
|
|
141
|
+
}
|
|
142
|
+
return names;
|
|
143
|
+
}
|
|
144
|
+
function couplingFindings(source) {
|
|
145
|
+
const findings = [];
|
|
146
|
+
for (const object of source.objects) {
|
|
147
|
+
if (object.kind !== "constraint" || object.definition.constraintKind !== "foreignKey")
|
|
148
|
+
continue;
|
|
149
|
+
const references = object.definition.references;
|
|
150
|
+
if (references === undefined)
|
|
151
|
+
continue;
|
|
152
|
+
const from = object.identity.parent.namespace;
|
|
153
|
+
const to = references.parent.namespace;
|
|
154
|
+
if (from.form !== "static" || to.form !== "static" || from.name === to.name)
|
|
155
|
+
continue;
|
|
156
|
+
findings.push({
|
|
157
|
+
severity: "warning",
|
|
158
|
+
code: "coupling",
|
|
159
|
+
place: `${from.name}.${object.identity.parent.name}.${object.definition.columns.join(",")}`,
|
|
160
|
+
reason: `references ${to.name}.${references.parent.name}.${references.columns.join(",")}`,
|
|
161
|
+
fix: "Cross-catalog foreign keys are allowed inside one database."
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return findings;
|
|
165
|
+
}
|
|
166
|
+
function assertSnapshots(cwd, config, source) {
|
|
167
|
+
if ((config.external ?? []).length === 0)
|
|
168
|
+
return;
|
|
169
|
+
const snapshots = loadSnapshots(cwd, config);
|
|
170
|
+
for (const table of externalTables(source)) {
|
|
171
|
+
const found = findTable(snapshots, table.namespace, table.name);
|
|
172
|
+
if (found === undefined) {
|
|
173
|
+
throw snapshotError(table.name, "missing", `External table ${table.namespace}.${table.name} is missing from the catalog snapshots.`);
|
|
174
|
+
}
|
|
175
|
+
const differ = columnDifference(table.columns, found.table.columns);
|
|
176
|
+
if (differ !== undefined) {
|
|
177
|
+
throw snapshotError(found.path, "stale", `Catalog snapshot ${found.path} is stale: ${table.namespace}.${table.name}.${differ} does not match the export.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function assertLiveExternal(url, source, snapshots) {
|
|
182
|
+
if (snapshots.length === 0 && externalTables(source).length === 0)
|
|
183
|
+
return;
|
|
184
|
+
const postgres = (await import("postgres")).default;
|
|
185
|
+
const sql = postgres(url, { max: 1, connect_timeout: 5, idle_timeout: 1, onnotice: () => {} });
|
|
186
|
+
try {
|
|
187
|
+
const current = await sql`select current_database() as name`;
|
|
188
|
+
const database = current[0]?.name;
|
|
189
|
+
if (database !== undefined)
|
|
190
|
+
assertSameDatabase(source, snapshots, database);
|
|
191
|
+
for (const table of externalTables(source)) {
|
|
192
|
+
const found = findTable(snapshots, table.namespace, table.name);
|
|
193
|
+
if (found === undefined)
|
|
194
|
+
continue;
|
|
195
|
+
await assertLiveTable(sql, found.path, found.snapshot, table);
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
await sql.end({ timeout: 5 });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function renderExport(cwd, config, source, args) {
|
|
202
|
+
const flags = exportFlags(args);
|
|
203
|
+
const database = exportDatabase(config);
|
|
204
|
+
const snapshot = snapshotFromCatalog(source, catalogNamespace(config), database);
|
|
205
|
+
const text = serializeSnapshot(snapshot);
|
|
206
|
+
if (flags.out !== undefined) {
|
|
207
|
+
const path = isAbsolute(flags.out) ? flags.out : join(cwd, flags.out);
|
|
208
|
+
writeFileSync(path, text);
|
|
209
|
+
}
|
|
210
|
+
if (flags.json || flags.out === undefined)
|
|
211
|
+
return text;
|
|
212
|
+
return `${flags.out}
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
function exportTables(source) {
|
|
216
|
+
const tables = [];
|
|
217
|
+
for (const object of source.objects) {
|
|
218
|
+
if (object.kind !== "table" || object.owner === "external" || object.owner === "ignored") {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (object.identity.namespace.form !== "static")
|
|
222
|
+
continue;
|
|
223
|
+
const columns = [];
|
|
224
|
+
for (const column of source.objects) {
|
|
225
|
+
if (!isColumnOf(column, object.identity.namespace.name, object.identity.name))
|
|
226
|
+
continue;
|
|
227
|
+
columns.push({
|
|
228
|
+
name: column.identity.name,
|
|
229
|
+
type: column.definition.dataType,
|
|
230
|
+
nullable: column.definition.nullable
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
tables.push({
|
|
234
|
+
namespace: object.identity.namespace.name,
|
|
235
|
+
name: object.identity.name,
|
|
236
|
+
columns
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return tables;
|
|
240
|
+
}
|
|
241
|
+
function isColumnOf(object, namespace, table) {
|
|
242
|
+
return object.kind === "column" && object.identity.parent.name === table && object.identity.parent.namespace.form === "static" && object.identity.parent.namespace.name === namespace;
|
|
243
|
+
}
|
|
244
|
+
function externalTables(source) {
|
|
245
|
+
const tables = [];
|
|
246
|
+
for (const object of source.objects) {
|
|
247
|
+
if (object.kind !== "table" || object.owner !== "external")
|
|
248
|
+
continue;
|
|
249
|
+
if (object.identity.namespace.form !== "static")
|
|
250
|
+
continue;
|
|
251
|
+
const columns = [];
|
|
252
|
+
for (const column of source.objects) {
|
|
253
|
+
if (!isColumnOf(column, object.identity.namespace.name, object.identity.name))
|
|
254
|
+
continue;
|
|
255
|
+
columns.push({
|
|
256
|
+
name: column.identity.name,
|
|
257
|
+
type: column.definition.dataType,
|
|
258
|
+
nullable: column.definition.nullable
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
tables.push({ namespace: object.identity.namespace.name, name: object.identity.name, columns });
|
|
262
|
+
}
|
|
263
|
+
return tables;
|
|
264
|
+
}
|
|
265
|
+
function findTable(snapshots, namespace, name) {
|
|
266
|
+
for (const loaded of snapshots) {
|
|
267
|
+
for (const table of loaded.snapshot.tables) {
|
|
268
|
+
if (table.name === name && (table.namespace === namespace || namespace === "public")) {
|
|
269
|
+
return { path: loaded.path, snapshot: loaded.snapshot, table };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const loaded of snapshots) {
|
|
274
|
+
for (const table of loaded.snapshot.tables) {
|
|
275
|
+
if (table.name === name)
|
|
276
|
+
return { path: loaded.path, snapshot: loaded.snapshot, table };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
function columnDifference(left, right) {
|
|
282
|
+
const byName = new Map(right.map((column) => [column.name, column]));
|
|
283
|
+
for (const column of left) {
|
|
284
|
+
const other = byName.get(column.name);
|
|
285
|
+
if (other === undefined)
|
|
286
|
+
return column.name;
|
|
287
|
+
if (other.type !== column.type || other.nullable !== column.nullable)
|
|
288
|
+
return column.name;
|
|
289
|
+
}
|
|
290
|
+
for (const column of right) {
|
|
291
|
+
if (!left.some((item) => item.name === column.name))
|
|
292
|
+
return column.name;
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
function assertSameDatabase(source, snapshots, database) {
|
|
297
|
+
if (!hasCrossCatalogForeignKey(source))
|
|
298
|
+
return;
|
|
299
|
+
for (const loaded of snapshots) {
|
|
300
|
+
const name = loaded.snapshot.database;
|
|
301
|
+
if (name !== undefined && name !== database) {
|
|
302
|
+
throw new OkmError2("OKM1842", `A foreign key references ${loaded.snapshot.logical.namespace} in database ${name}. This database is ${database}.`, {
|
|
303
|
+
kind: "invalid",
|
|
304
|
+
fieldReason: "database",
|
|
305
|
+
fix: {
|
|
306
|
+
summary: "Keep the foreign key inside one database. Cross-catalog keys are allowed only there."
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function hasCrossCatalogForeignKey(source) {
|
|
313
|
+
for (const object of source.objects) {
|
|
314
|
+
if (object.kind !== "constraint" || object.definition.constraintKind !== "foreignKey")
|
|
315
|
+
continue;
|
|
316
|
+
const references = object.definition.references;
|
|
317
|
+
if (references === undefined)
|
|
318
|
+
continue;
|
|
319
|
+
const from = object.identity.parent.namespace;
|
|
320
|
+
const to = references.parent.namespace;
|
|
321
|
+
if (from.form === "static" && to.form === "static" && from.name !== to.name)
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
async function assertLiveTable(sql, path, snapshot, table) {
|
|
327
|
+
const hash = await liveHash(sql, table.namespace);
|
|
328
|
+
if (hash !== undefined && hash !== snapshot.catalogHash) {
|
|
329
|
+
throw snapshotError(path, "stale", `Catalog snapshot ${path} is stale: the catalog hash changed.`);
|
|
330
|
+
}
|
|
331
|
+
const live = await liveColumns(sql, table.namespace, table.name);
|
|
332
|
+
if (live === undefined) {
|
|
333
|
+
throw snapshotError(path, "missing", `External table ${table.namespace}.${table.name} is missing on the database.`);
|
|
334
|
+
}
|
|
335
|
+
const differ = columnDifference(snapshotTableColumns(snapshot, table), live);
|
|
336
|
+
if (differ !== undefined) {
|
|
337
|
+
throw snapshotError(path, "stale", `Catalog snapshot ${path} is stale: ${table.namespace}.${table.name}.${differ} is missing on the database.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function snapshotTableColumns(snapshot, table) {
|
|
341
|
+
for (const item of snapshot.tables) {
|
|
342
|
+
if (item.name === table.name)
|
|
343
|
+
return item.columns;
|
|
344
|
+
}
|
|
345
|
+
return table.columns;
|
|
346
|
+
}
|
|
347
|
+
async function liveHash(sql, namespace) {
|
|
348
|
+
const reg = `${quoteIdent2(namespace)}.${quoteIdent2("okm_meta")}`;
|
|
349
|
+
const present = await sql.unsafe(`select to_regclass('${reg}')::text as reg`);
|
|
350
|
+
if (present[0]?.reg == null)
|
|
351
|
+
return;
|
|
352
|
+
const rows = await sql.unsafe(`select catalog_hash from ${reg} where id = 'head'`);
|
|
353
|
+
return rows[0]?.catalog_hash;
|
|
354
|
+
}
|
|
355
|
+
async function liveColumns(sql, namespace, table) {
|
|
356
|
+
const rows = await sql`select a.attname as name, format_type(a.atttypid, a.atttypmod) as type, a.attnotnull as not_null
|
|
357
|
+
from pg_attribute a
|
|
358
|
+
join pg_class c on c.oid = a.attrelid
|
|
359
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
360
|
+
where n.nspname = ${namespace} and c.relname = ${table}
|
|
361
|
+
and a.attnum > 0 and not a.attisdropped
|
|
362
|
+
order by a.attnum`;
|
|
363
|
+
if (rows.length === 0) {
|
|
364
|
+
const exists = await sql`
|
|
365
|
+
select to_regclass(${`${quoteIdent2(namespace)}.${quoteIdent2(table)}`})::text as reg
|
|
366
|
+
`;
|
|
367
|
+
if (exists[0]?.reg == null)
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
return rows.map((row) => ({ name: row.name, type: row.type, nullable: !row.not_null }));
|
|
371
|
+
}
|
|
372
|
+
function resolveSnapshot(cwd, path) {
|
|
373
|
+
return isAbsolute(path) ? path : projectPath(cwd, path);
|
|
374
|
+
}
|
|
375
|
+
function exportDatabase(config) {
|
|
376
|
+
const target = config.database;
|
|
377
|
+
if (typeof target === "string")
|
|
378
|
+
return databaseName(target);
|
|
379
|
+
if (target?.url !== undefined)
|
|
380
|
+
return databaseName(target.url);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
function exportFlags(args) {
|
|
384
|
+
let json = false;
|
|
385
|
+
let out;
|
|
386
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
387
|
+
const arg = args[index] ?? "";
|
|
388
|
+
if (arg === "--json") {
|
|
389
|
+
json = true;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (arg === "--out") {
|
|
393
|
+
const value = args[index + 1];
|
|
394
|
+
if (value === undefined || value.startsWith("--")) {
|
|
395
|
+
throw new OkmError2("invalid", "--out needs a file.", {
|
|
396
|
+
fix: { summary: "Pass --out <file>." }
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
out = value;
|
|
400
|
+
index += 1;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (arg.startsWith("--out=")) {
|
|
404
|
+
out = arg.slice("--out=".length);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
throw new OkmError2("invalid", `Unknown flag ${arg}.`, {
|
|
408
|
+
fix: { summary: "okm catalog export accepts --json and --out <file>." }
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return out === undefined ? { json } : { json, out };
|
|
412
|
+
}
|
|
413
|
+
function snapshotError(path, reason, message) {
|
|
414
|
+
return new OkmError2("OKM1841", message, {
|
|
415
|
+
kind: reason === "missing" ? "not_found" : "schema_drift",
|
|
416
|
+
fieldReason: reason,
|
|
417
|
+
columns: [path],
|
|
418
|
+
fix: {
|
|
419
|
+
summary: reason === "missing" ? "Point external at the file okm catalog export wrote, and apply the owning catalog." : "Run okm catalog export in the owning catalog and update the dependent."
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
function isSnapshot(value) {
|
|
424
|
+
if (!isRecord(value) || value.format !== EXPORT_FORMAT)
|
|
425
|
+
return false;
|
|
426
|
+
if (typeof value.catalogHash !== "string")
|
|
427
|
+
return false;
|
|
428
|
+
if (!isLogical(value.logical))
|
|
429
|
+
return false;
|
|
430
|
+
if (value.database !== undefined && typeof value.database !== "string")
|
|
431
|
+
return false;
|
|
432
|
+
if (!Array.isArray(value.tables))
|
|
433
|
+
return false;
|
|
434
|
+
return value.tables.every(isTable);
|
|
435
|
+
}
|
|
436
|
+
function isLogical(value) {
|
|
437
|
+
if (!isRecord(value))
|
|
438
|
+
return false;
|
|
439
|
+
return typeof value.namespace === "string" && typeof value.name === "string" && value.format === LOGICAL_FORMAT;
|
|
440
|
+
}
|
|
441
|
+
function isTable(value) {
|
|
442
|
+
if (!isRecord(value) || typeof value.namespace !== "string" || typeof value.name !== "string") {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
if (!Array.isArray(value.columns))
|
|
446
|
+
return false;
|
|
447
|
+
return value.columns.every((column) => isRecord(column) && typeof column.name === "string" && typeof column.type === "string" && typeof column.nullable === "boolean");
|
|
448
|
+
}
|
|
449
|
+
function isRecord(value) {
|
|
450
|
+
return typeof value === "object" && value !== null;
|
|
451
|
+
}
|
|
452
|
+
|
|
81
453
|
// src/tooling/errors/registry.ts
|
|
82
454
|
var ERROR_DOCS = [
|
|
83
455
|
{
|
|
@@ -455,7 +827,7 @@ var ERROR_DOCS = [
|
|
|
455
827
|
{
|
|
456
828
|
code: "OKM1538",
|
|
457
829
|
title: "Type rewrite",
|
|
458
|
-
summary: "A statement changes a column type in a way that rewrites the table.",
|
|
830
|
+
summary: "A statement changes a column type in a way that rewrites the table. Binary-coercible changes, varchar widening, varchar to text, and a numeric precision increase at the same scale do not.",
|
|
459
831
|
fix: "There is no safe form for a rewriting type change in this version. A column swap is expand and contract work for a later step. OKM1524 already requires a reason for the type change."
|
|
460
832
|
},
|
|
461
833
|
{
|
|
@@ -536,6 +908,12 @@ var ERROR_DOCS = [
|
|
|
536
908
|
summary: "A step contains COMMIT, ROLLBACK, or other transaction control, including inside a DO body. A function or procedure body is not this.",
|
|
537
909
|
fix: "Remove the transaction control. okm migrate check replays one transaction and rolls it back. Apply accepts the step only with -- okm-allow OKM1551: reason."
|
|
538
910
|
},
|
|
911
|
+
{
|
|
912
|
+
code: "OKM1552",
|
|
913
|
+
title: "Reprinted declaration",
|
|
914
|
+
summary: "A domain check or a LANGUAGE sql function body is stored differently from the text in the migration catalog.",
|
|
915
|
+
fix: "Write the check or the body the way Postgres prints it, or allow OKM1552 with a reason. The warning does not fail apply. A domain whose check differs is still OKM1547."
|
|
916
|
+
},
|
|
539
917
|
{
|
|
540
918
|
code: "OKM1601",
|
|
541
919
|
title: "Stale typed SQL",
|
|
@@ -751,6 +1129,18 @@ var ERROR_DOCS = [
|
|
|
751
1129
|
title: "Pooler session",
|
|
752
1130
|
summary: "Two statements on the same connection saw different backend pids. A transaction-mode pooler is in the way.",
|
|
753
1131
|
fix: "Use a direct connection, or pass --allow-pooler."
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
code: "OKM1855",
|
|
1135
|
+
title: "Pull would overwrite",
|
|
1136
|
+
summary: "okm pull or okm import drizzle found a file it would replace. It writes nothing.",
|
|
1137
|
+
fix: "Pass --out <directory> to write somewhere else, or move the file."
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
code: "OKM1856",
|
|
1141
|
+
title: "Drizzle is not installed",
|
|
1142
|
+
summary: "okm import drizzle could not resolve drizzle-orm from the project.",
|
|
1143
|
+
fix: "Install drizzle-orm in the project that owns the schema."
|
|
754
1144
|
}
|
|
755
1145
|
];
|
|
756
1146
|
var byCode;
|
|
@@ -767,25 +1157,25 @@ function indexDocs() {
|
|
|
767
1157
|
}
|
|
768
1158
|
|
|
769
1159
|
// src/tooling/migrate/files.ts
|
|
770
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
771
|
-
import { join } from "node:path";
|
|
1160
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1161
|
+
import { join as join2 } from "node:path";
|
|
772
1162
|
function loadMigrations(directory) {
|
|
773
|
-
if (!
|
|
1163
|
+
if (!existsSync2(directory))
|
|
774
1164
|
return [];
|
|
775
1165
|
const names = readdirSync(directory).filter((file) => file.endsWith(".sql")).sort();
|
|
776
1166
|
const migrations = [];
|
|
777
1167
|
for (const file of names) {
|
|
778
1168
|
const id = file.slice(0, -".sql".length);
|
|
779
|
-
const catalogPath =
|
|
780
|
-
if (!
|
|
1169
|
+
const catalogPath = join2(directory, `${id}.catalog.json`);
|
|
1170
|
+
if (!existsSync2(catalogPath)) {
|
|
781
1171
|
throw new OkmError2("invalid", `Migration ${id} is missing its catalog snapshot.`, {
|
|
782
1172
|
fix: {
|
|
783
1173
|
summary: "Generate the migration again so the SQL and the catalog are written together."
|
|
784
1174
|
}
|
|
785
1175
|
});
|
|
786
1176
|
}
|
|
787
|
-
const plan = readMigrationPlan(
|
|
788
|
-
const stored = parseCatalog2(
|
|
1177
|
+
const plan = readMigrationPlan(join2(directory, file));
|
|
1178
|
+
const stored = parseCatalog2(readFileSync2(catalogPath, "utf8"));
|
|
789
1179
|
const startup = startupCatalog2(stored);
|
|
790
1180
|
const hash = catalogHash2(startup);
|
|
791
1181
|
migrations.push(startup === stored ? { id, catalogHash: hash, steps: plan.steps } : { id, catalogHash: hash, legacyHash: catalogHash2(stored), steps: plan.steps });
|
|
@@ -793,7 +1183,7 @@ function loadMigrations(directory) {
|
|
|
793
1183
|
return migrations;
|
|
794
1184
|
}
|
|
795
1185
|
function readMigrationPlan(path) {
|
|
796
|
-
const text = prepareMigrationText(
|
|
1186
|
+
const text = prepareMigrationText(readFileSync2(path, "utf8"), path);
|
|
797
1187
|
const plan = parsePlan(text);
|
|
798
1188
|
assertMigrationSteps(text, plan.steps.length, path);
|
|
799
1189
|
return plan;
|
|
@@ -806,13 +1196,13 @@ function loadHeadSnapshot(directory) {
|
|
|
806
1196
|
return {
|
|
807
1197
|
id: last.id,
|
|
808
1198
|
catalogHash: last.catalogHash,
|
|
809
|
-
catalog: parseCatalog2(
|
|
1199
|
+
catalog: parseCatalog2(readFileSync2(join2(directory, `${last.id}.catalog.json`), "utf8"))
|
|
810
1200
|
};
|
|
811
1201
|
}
|
|
812
1202
|
|
|
813
1203
|
// src/tooling/migrate/lint.ts
|
|
814
|
-
import { existsSync as
|
|
815
|
-
import { join as
|
|
1204
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1205
|
+
import { join as join3 } from "node:path";
|
|
816
1206
|
|
|
817
1207
|
// src/tooling/migrate/lint-rules.ts
|
|
818
1208
|
var INTEGER_WIDTH = {
|
|
@@ -899,7 +1289,7 @@ function rule(code, category, severity, doc, check) {
|
|
|
899
1289
|
function hit(matched, reason) {
|
|
900
1290
|
return matched ? [reason] : [];
|
|
901
1291
|
}
|
|
902
|
-
function
|
|
1292
|
+
function normalizedSql(sql) {
|
|
903
1293
|
const collapsed = sql.trim().replace(/\s+/g, " ");
|
|
904
1294
|
let out = "";
|
|
905
1295
|
let quoted = false;
|
|
@@ -914,7 +1304,7 @@ function normalized(sql) {
|
|
|
914
1304
|
return out;
|
|
915
1305
|
}
|
|
916
1306
|
function weakensRls(input) {
|
|
917
|
-
const text =
|
|
1307
|
+
const text = normalizedSql(input.step.sql);
|
|
918
1308
|
if (/\bdisable row level security\b/.test(text))
|
|
919
1309
|
return ["disables row level security"];
|
|
920
1310
|
if (/\bno force row level security\b/.test(text))
|
|
@@ -1031,7 +1421,7 @@ function parenthesized(text, open) {
|
|
|
1031
1421
|
}
|
|
1032
1422
|
function policyExpression(input, table, name) {
|
|
1033
1423
|
const expression = expressionOf(input.after, table, name) ?? expressionOf(input.before, table, name);
|
|
1034
|
-
return expression === undefined ? undefined :
|
|
1424
|
+
return expression === undefined ? undefined : normalizedSql(expression);
|
|
1035
1425
|
}
|
|
1036
1426
|
function expressionOf(source, table, name) {
|
|
1037
1427
|
for (const object of source.objects) {
|
|
@@ -1054,7 +1444,7 @@ function hasPolicy(source, table) {
|
|
|
1054
1444
|
return source.objects.some((object) => object.kind === "policy" && object.identity.parent.name === table);
|
|
1055
1445
|
}
|
|
1056
1446
|
function dataStatement(step) {
|
|
1057
|
-
const text =
|
|
1447
|
+
const text = normalizedSql(step.sql);
|
|
1058
1448
|
if (/^(?:do|call)\b/.test(text))
|
|
1059
1449
|
return ["the body is not analysed"];
|
|
1060
1450
|
if (step.backfill !== undefined)
|
|
@@ -1068,16 +1458,16 @@ function dataStatement(step) {
|
|
|
1068
1458
|
return [];
|
|
1069
1459
|
}
|
|
1070
1460
|
function matches(step, kind, pattern) {
|
|
1071
|
-
return pattern.test(
|
|
1461
|
+
return pattern.test(normalizedSql(step.sql)) || step.kind === kind;
|
|
1072
1462
|
}
|
|
1073
1463
|
function renamesTable(step) {
|
|
1074
|
-
const text =
|
|
1464
|
+
const text = normalizedSql(step.sql);
|
|
1075
1465
|
if (/^alter table\s+\S+\s+rename to\b/.test(text))
|
|
1076
1466
|
return true;
|
|
1077
1467
|
return step.kind === "rename-table" && !/^alter table\s+\S+\s+rename (column|constraint)\b/.test(text);
|
|
1078
1468
|
}
|
|
1079
1469
|
function droppedType(input) {
|
|
1080
|
-
const text =
|
|
1470
|
+
const text = normalizedSql(input.step.sql);
|
|
1081
1471
|
const dropping = /^drop type\b/.test(text);
|
|
1082
1472
|
if (!dropping && input.step.kind !== "drop-enum" && input.step.kind !== "drop-domain") {
|
|
1083
1473
|
return;
|
|
@@ -1093,7 +1483,7 @@ function droppedType(input) {
|
|
|
1093
1483
|
return "enum";
|
|
1094
1484
|
}
|
|
1095
1485
|
function requiredColumn(input) {
|
|
1096
|
-
const text =
|
|
1486
|
+
const text = normalizedSql(input.step.sql);
|
|
1097
1487
|
const adding = /^alter table\s+\S+\s+add column\b/.test(text) || input.step.kind === "add-column-required";
|
|
1098
1488
|
if (!adding)
|
|
1099
1489
|
return false;
|
|
@@ -1109,7 +1499,7 @@ function requiredColumn(input) {
|
|
|
1109
1499
|
function uniqueConstraint(input) {
|
|
1110
1500
|
if (!tableExists(input))
|
|
1111
1501
|
return false;
|
|
1112
|
-
const text =
|
|
1502
|
+
const text = normalizedSql(input.step.sql);
|
|
1113
1503
|
if (/\busing index\b/.test(text))
|
|
1114
1504
|
return false;
|
|
1115
1505
|
if (/^alter table\s+\S+\s+add constraint\b/.test(text) && /\b(unique|primary key)\b/.test(text)) {
|
|
@@ -1120,16 +1510,16 @@ function uniqueConstraint(input) {
|
|
|
1120
1510
|
const name = createdIndexName(text);
|
|
1121
1511
|
if (name === undefined)
|
|
1122
1512
|
return false;
|
|
1123
|
-
return input.later.some((step) => usesIndex(
|
|
1513
|
+
return input.later.some((step) => usesIndex(normalizedSql(step.sql), name));
|
|
1124
1514
|
}
|
|
1125
1515
|
function uniqueIndex(input) {
|
|
1126
|
-
const text =
|
|
1516
|
+
const text = normalizedSql(input.step.sql);
|
|
1127
1517
|
if (!/^create unique index\b/.test(text))
|
|
1128
1518
|
return false;
|
|
1129
1519
|
if (!tableExists(input))
|
|
1130
1520
|
return false;
|
|
1131
1521
|
const name = createdIndexName(text);
|
|
1132
|
-
if (name !== undefined && input.later.some((step) => usesIndex(
|
|
1522
|
+
if (name !== undefined && input.later.some((step) => usesIndex(normalizedSql(step.sql), name))) {
|
|
1133
1523
|
return false;
|
|
1134
1524
|
}
|
|
1135
1525
|
return true;
|
|
@@ -1142,7 +1532,7 @@ function usesIndex(text, name) {
|
|
|
1142
1532
|
return text.includes(`using index "${name}"`) || text.includes(`using index ${name}`);
|
|
1143
1533
|
}
|
|
1144
1534
|
function unsafeSetNotNull(input) {
|
|
1145
|
-
const text =
|
|
1535
|
+
const text = normalizedSql(input.step.sql);
|
|
1146
1536
|
const match = /^alter table\s+(\S+)\s+alter column\s+(\S+)\s+set not null\b/.exec(text);
|
|
1147
1537
|
if (match === null && input.step.kind !== "set-not-null")
|
|
1148
1538
|
return false;
|
|
@@ -1158,21 +1548,21 @@ function unsafeSetNotNull(input) {
|
|
|
1158
1548
|
function validatedNotNullCheck(earlier, table, column) {
|
|
1159
1549
|
const needle = `check (${column} is not null) not valid`;
|
|
1160
1550
|
for (const step of earlier) {
|
|
1161
|
-
const text =
|
|
1551
|
+
const text = normalizedSql(step.sql);
|
|
1162
1552
|
if (!text.startsWith(`alter table ${table} `) || !text.includes(needle))
|
|
1163
1553
|
continue;
|
|
1164
1554
|
const added = /^alter table\s+\S+\s+add constraint\s+(\S+)\s+check\b/.exec(text);
|
|
1165
1555
|
const name = added?.[1];
|
|
1166
1556
|
if (name === undefined)
|
|
1167
1557
|
continue;
|
|
1168
|
-
const validated = earlier.some((item) =>
|
|
1558
|
+
const validated = earlier.some((item) => normalizedSql(item.sql).startsWith(`alter table ${table} validate constraint ${name}`));
|
|
1169
1559
|
if (validated)
|
|
1170
1560
|
return true;
|
|
1171
1561
|
}
|
|
1172
1562
|
return false;
|
|
1173
1563
|
}
|
|
1174
1564
|
function blockingIndex(input) {
|
|
1175
|
-
const text =
|
|
1565
|
+
const text = normalizedSql(input.step.sql);
|
|
1176
1566
|
if (!/^create(?: unique)? index\b/.test(text) && input.step.kind !== "create-index")
|
|
1177
1567
|
return false;
|
|
1178
1568
|
if (/\bconcurrently\b/.test(text))
|
|
@@ -1180,10 +1570,10 @@ function blockingIndex(input) {
|
|
|
1180
1570
|
return tableExists(input);
|
|
1181
1571
|
}
|
|
1182
1572
|
function checkWithoutNotValid(input) {
|
|
1183
|
-
return addedConstraint(input, "check") && !/\bnot valid\b/.test(
|
|
1573
|
+
return addedConstraint(input, "check") && !/\bnot valid\b/.test(normalizedSql(input.step.sql));
|
|
1184
1574
|
}
|
|
1185
1575
|
function foreignKeyWithoutNotValid(input) {
|
|
1186
|
-
return addedConstraint(input, "foreign key") && !/\bnot valid\b/.test(
|
|
1576
|
+
return addedConstraint(input, "foreign key") && !/\bnot valid\b/.test(normalizedSql(input.step.sql));
|
|
1187
1577
|
}
|
|
1188
1578
|
function validatingCheck(input) {
|
|
1189
1579
|
if (widenValidate(input))
|
|
@@ -1193,7 +1583,7 @@ function validatingCheck(input) {
|
|
|
1193
1583
|
return checkWithoutNotValid(input);
|
|
1194
1584
|
}
|
|
1195
1585
|
function widenValidate(input) {
|
|
1196
|
-
const text =
|
|
1586
|
+
const text = normalizedSql(input.step.sql);
|
|
1197
1587
|
if (!/^alter table\s+\S+\s+validate constraint\b/.test(text))
|
|
1198
1588
|
return false;
|
|
1199
1589
|
if (input.step.kind === "widen-check")
|
|
@@ -1204,7 +1594,7 @@ function widenValidate(input) {
|
|
|
1204
1594
|
return input.earlier.some((step) => {
|
|
1205
1595
|
if (step.kind !== "widen-check")
|
|
1206
1596
|
return false;
|
|
1207
|
-
const prior =
|
|
1597
|
+
const prior = normalizedSql(step.sql);
|
|
1208
1598
|
return prior.includes(`add constraint ${name} `);
|
|
1209
1599
|
});
|
|
1210
1600
|
}
|
|
@@ -1216,13 +1606,13 @@ function validatingForeignKey(input) {
|
|
|
1216
1606
|
function addedConstraint(input, kind) {
|
|
1217
1607
|
if (!tableExists(input))
|
|
1218
1608
|
return false;
|
|
1219
|
-
const text =
|
|
1609
|
+
const text = normalizedSql(input.step.sql);
|
|
1220
1610
|
if (!/^alter table\s+\S+\s+add constraint\b/.test(text))
|
|
1221
1611
|
return false;
|
|
1222
1612
|
return new RegExp(`\\b${kind}\\b`).test(text);
|
|
1223
1613
|
}
|
|
1224
1614
|
function validates(input, kind) {
|
|
1225
|
-
const text =
|
|
1615
|
+
const text = normalizedSql(input.step.sql);
|
|
1226
1616
|
const table = /^alter table\s+\S+\s+validate constraint\s+(\S+)/.exec(text);
|
|
1227
1617
|
if (table !== null) {
|
|
1228
1618
|
const token = table[1];
|
|
@@ -1242,7 +1632,7 @@ function validates(input, kind) {
|
|
|
1242
1632
|
}
|
|
1243
1633
|
var TYPE_CHANGE = /^alter table\s+(\S+)\s+alter column\s+(\S+)\s+(?:set data type|type)\b/;
|
|
1244
1634
|
function typeChange(input, mode) {
|
|
1245
|
-
const text =
|
|
1635
|
+
const text = normalizedSql(input.step.sql);
|
|
1246
1636
|
const changing = TYPE_CHANGE.test(text) || input.step.kind === "set-column-type";
|
|
1247
1637
|
if (!changing)
|
|
1248
1638
|
return [];
|
|
@@ -1258,18 +1648,19 @@ function typeChange(input, mode) {
|
|
|
1258
1648
|
const shrink = lengthShrink(before, after);
|
|
1259
1649
|
if (mode === "shrink")
|
|
1260
1650
|
return shrink ? ["shortens a character length"] : [];
|
|
1261
|
-
if (shrink)
|
|
1262
|
-
return [];
|
|
1651
|
+
if (shrink) {
|
|
1652
|
+
return mode === "rewrite" && tableRewrites(before, after, input) ? ["rewrites the table"] : [];
|
|
1653
|
+
}
|
|
1263
1654
|
if (mode === "narrow")
|
|
1264
1655
|
return narrows(before, after) ? ["narrows a type"] : [];
|
|
1265
|
-
if (lengthGrow(before, after))
|
|
1656
|
+
if (lengthGrow(before, after) && mode !== "rewrite")
|
|
1266
1657
|
return [];
|
|
1267
1658
|
if (mode === "incompatible")
|
|
1268
1659
|
return ["changes a column type"];
|
|
1269
|
-
return
|
|
1660
|
+
return tableRewrites(before, after, input) ? ["rewrites the table"] : [];
|
|
1270
1661
|
}
|
|
1271
1662
|
function columnTypes(input) {
|
|
1272
|
-
const text =
|
|
1663
|
+
const text = normalizedSql(input.step.sql);
|
|
1273
1664
|
const match = TYPE_CHANGE.exec(text);
|
|
1274
1665
|
const table = lastName(match?.[1]);
|
|
1275
1666
|
const column = lastName(match?.[2]);
|
|
@@ -1294,13 +1685,115 @@ function lengthGrow(before, after) {
|
|
|
1294
1685
|
const right = charLength(after);
|
|
1295
1686
|
return left !== undefined && right !== undefined && left.family === right.family && right.length > left.length;
|
|
1296
1687
|
}
|
|
1297
|
-
function
|
|
1298
|
-
if (
|
|
1688
|
+
function tableRewrites(before, after, input) {
|
|
1689
|
+
if (usingRewrites(input))
|
|
1690
|
+
return true;
|
|
1691
|
+
if (varyingRelaxation(before, after))
|
|
1692
|
+
return false;
|
|
1693
|
+
if (isVarying(before) && after === "text")
|
|
1299
1694
|
return false;
|
|
1300
|
-
if (
|
|
1695
|
+
if (before === "text" && unconstrainedVarying(after))
|
|
1696
|
+
return false;
|
|
1697
|
+
if ((before === "text" || isVarying(before)) && unconstrainedCharacter(after))
|
|
1698
|
+
return false;
|
|
1699
|
+
if (numericRelaxation(before, after))
|
|
1700
|
+
return false;
|
|
1701
|
+
if (binaryPair(before, after))
|
|
1702
|
+
return false;
|
|
1703
|
+
if (domainSkipsRewrite(before, after, input))
|
|
1301
1704
|
return false;
|
|
1302
1705
|
return true;
|
|
1303
1706
|
}
|
|
1707
|
+
function statementMayReprint(sql) {
|
|
1708
|
+
const text = normalizedSql(sql);
|
|
1709
|
+
if (/^(?:create|alter) domain\b/.test(text))
|
|
1710
|
+
return true;
|
|
1711
|
+
return /^create(?: or replace)? function\b/.test(text) && /\blanguage sql\b/.test(text);
|
|
1712
|
+
}
|
|
1713
|
+
function usingRewrites(input) {
|
|
1714
|
+
const text = normalizedSql(input.step.sql);
|
|
1715
|
+
const at = text.search(/\busing\b/);
|
|
1716
|
+
if (at < 0)
|
|
1717
|
+
return false;
|
|
1718
|
+
const column = lastName(TYPE_CHANGE.exec(text)?.[2]);
|
|
1719
|
+
if (column === undefined)
|
|
1720
|
+
return true;
|
|
1721
|
+
const expression = text.slice(at + "using".length).trim().replace(/;$/, "");
|
|
1722
|
+
return !plainCast(expression, column);
|
|
1723
|
+
}
|
|
1724
|
+
function plainCast(expression, column) {
|
|
1725
|
+
const bare = column.toLowerCase();
|
|
1726
|
+
const quoted = `"${column}"`;
|
|
1727
|
+
return expression.startsWith(`${bare}::`) || expression.startsWith(`${quoted}::`) || expression.startsWith(`cast(${bare} as `) || expression.startsWith(`cast(${quoted} as `);
|
|
1728
|
+
}
|
|
1729
|
+
function isVarying(type) {
|
|
1730
|
+
return type === "character varying" || type === "varchar" || type.startsWith("character varying(") || type.startsWith("varchar(");
|
|
1731
|
+
}
|
|
1732
|
+
function unconstrainedVarying(type) {
|
|
1733
|
+
return type === "character varying" || type === "varchar";
|
|
1734
|
+
}
|
|
1735
|
+
function unconstrainedCharacter(type) {
|
|
1736
|
+
return type === "bpchar" || type === "character";
|
|
1737
|
+
}
|
|
1738
|
+
function varyingRelaxation(before, after) {
|
|
1739
|
+
const left = charLength(before);
|
|
1740
|
+
if (left?.family === "varying" && unconstrainedVarying(after))
|
|
1741
|
+
return true;
|
|
1742
|
+
const right = charLength(after);
|
|
1743
|
+
return left?.family === "varying" && right?.family === "varying" && right.length > left.length;
|
|
1744
|
+
}
|
|
1745
|
+
function numericRelaxation(before, after) {
|
|
1746
|
+
const left = numericBounds(before);
|
|
1747
|
+
const right = numericBounds(after);
|
|
1748
|
+
if (left === undefined || right === undefined)
|
|
1749
|
+
return false;
|
|
1750
|
+
if (right.precision === undefined)
|
|
1751
|
+
return left.precision !== undefined;
|
|
1752
|
+
if (left.precision === undefined || left.scale === undefined || right.scale === undefined) {
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
return right.scale === left.scale && right.precision > left.precision;
|
|
1756
|
+
}
|
|
1757
|
+
function numericBounds(type) {
|
|
1758
|
+
const match = /^numeric(?:\((\d+)(?:,(\d+))?\))?$/.exec(type);
|
|
1759
|
+
if (match === null)
|
|
1760
|
+
return;
|
|
1761
|
+
if (match[1] === undefined)
|
|
1762
|
+
return { precision: undefined, scale: undefined };
|
|
1763
|
+
return {
|
|
1764
|
+
precision: Number(match[1]),
|
|
1765
|
+
scale: match[2] === undefined ? 0 : Number(match[2])
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
function binaryPair(before, after) {
|
|
1769
|
+
if (before === "integer" && after === "oid")
|
|
1770
|
+
return true;
|
|
1771
|
+
if (before === "oid" && after === "integer")
|
|
1772
|
+
return true;
|
|
1773
|
+
if (before === "cidr" && after === "inet")
|
|
1774
|
+
return true;
|
|
1775
|
+
return before === "xml" && after === "text";
|
|
1776
|
+
}
|
|
1777
|
+
function domainSkipsRewrite(before, after, input) {
|
|
1778
|
+
const next = domainNamed(input, after);
|
|
1779
|
+
if (next !== undefined && next.check.trim().length === 0 && canonical(next.base) === before) {
|
|
1780
|
+
return true;
|
|
1781
|
+
}
|
|
1782
|
+
const previous = domainNamed(input, before);
|
|
1783
|
+
return previous !== undefined && canonical(previous.base) === after;
|
|
1784
|
+
}
|
|
1785
|
+
function domainNamed(input, name) {
|
|
1786
|
+
for (const source of [input.after, input.before]) {
|
|
1787
|
+
for (const object of source.objects) {
|
|
1788
|
+
if (object.kind !== "type" || object.identity.name !== name)
|
|
1789
|
+
continue;
|
|
1790
|
+
if (!isDomain(object.definition))
|
|
1791
|
+
continue;
|
|
1792
|
+
return object.definition;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1304
1797
|
function narrows(before, after) {
|
|
1305
1798
|
const leftInt = INTEGER_WIDTH[before];
|
|
1306
1799
|
const rightInt = INTEGER_WIDTH[after];
|
|
@@ -1325,9 +1818,6 @@ function charLength(type) {
|
|
|
1325
1818
|
return { family: "fixed", length: Number(fixed[1]) };
|
|
1326
1819
|
return;
|
|
1327
1820
|
}
|
|
1328
|
-
function isCharacter(type) {
|
|
1329
|
-
return type === "text" || charLength(type) !== undefined;
|
|
1330
|
-
}
|
|
1331
1821
|
function numericSpec(type) {
|
|
1332
1822
|
const match = /^numeric(?:\((\d+)(?:,(\d+))?\))?$/.exec(type);
|
|
1333
1823
|
if (match === null)
|
|
@@ -1344,7 +1834,7 @@ function tableExists(input) {
|
|
|
1344
1834
|
return name !== undefined && input.existingTables.has(name);
|
|
1345
1835
|
}
|
|
1346
1836
|
function statementTable(step) {
|
|
1347
|
-
const text =
|
|
1837
|
+
const text = normalizedSql(step.sql);
|
|
1348
1838
|
const altered = /^(?:alter|drop) table\s+(\S+)/.exec(text);
|
|
1349
1839
|
if (altered?.[1] !== undefined)
|
|
1350
1840
|
return lastName(altered[1]);
|
|
@@ -1380,6 +1870,134 @@ function isSerial(column) {
|
|
|
1380
1870
|
return /nextval\s*\(/i.test(column.definition.defaultExpression ?? "");
|
|
1381
1871
|
}
|
|
1382
1872
|
|
|
1873
|
+
// src/tooling/migrate/reprint.ts
|
|
1874
|
+
var REPRINT_CODE = "OKM1552";
|
|
1875
|
+
function reprintHits(declared, stored, steps) {
|
|
1876
|
+
const hits = [];
|
|
1877
|
+
const silenced = new Set;
|
|
1878
|
+
for (const difference of differences(declared, stored)) {
|
|
1879
|
+
const index = stepIndex(steps, difference.kind, difference.name);
|
|
1880
|
+
if (index === undefined)
|
|
1881
|
+
continue;
|
|
1882
|
+
const step = steps[index];
|
|
1883
|
+
const allow = step?.allows?.find((item) => item.code === REPRINT_CODE);
|
|
1884
|
+
if (allow !== undefined && allow.reason.trim().length > 0) {
|
|
1885
|
+
silenced.add(index);
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
hits.push({
|
|
1889
|
+
severity: "warning",
|
|
1890
|
+
code: REPRINT_CODE,
|
|
1891
|
+
place: `step ${String(index + 1)}`,
|
|
1892
|
+
reason: difference.reason
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
steps.forEach((step, index) => {
|
|
1896
|
+
for (const allow of step.allows ?? []) {
|
|
1897
|
+
if (allow.code !== REPRINT_CODE)
|
|
1898
|
+
continue;
|
|
1899
|
+
const place = `step ${String(index + 1)}`;
|
|
1900
|
+
if (allow.reason.trim().length === 0) {
|
|
1901
|
+
hits.push({
|
|
1902
|
+
severity: "error",
|
|
1903
|
+
code: "OKM1510",
|
|
1904
|
+
place,
|
|
1905
|
+
reason: "override needs a reason"
|
|
1906
|
+
});
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
if (silenced.has(index))
|
|
1910
|
+
continue;
|
|
1911
|
+
if (hits.some((item) => item.place === place && item.code === REPRINT_CODE))
|
|
1912
|
+
continue;
|
|
1913
|
+
if (!statementMayReprint(step.sql))
|
|
1914
|
+
continue;
|
|
1915
|
+
hits.push({
|
|
1916
|
+
severity: "error",
|
|
1917
|
+
code: "OKM1510",
|
|
1918
|
+
place,
|
|
1919
|
+
reason: `override ${REPRINT_CODE} does not match this statement`
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
return hits;
|
|
1924
|
+
}
|
|
1925
|
+
function differences(declared, stored) {
|
|
1926
|
+
const found = [];
|
|
1927
|
+
const storedDomains = domainChecks(stored);
|
|
1928
|
+
for (const [name, check] of domainChecks(declared)) {
|
|
1929
|
+
const live = storedDomains.get(name);
|
|
1930
|
+
if (live === undefined || live === check)
|
|
1931
|
+
continue;
|
|
1932
|
+
found.push({
|
|
1933
|
+
kind: "domain",
|
|
1934
|
+
name,
|
|
1935
|
+
reason: `domain ${name} check reprints differently`
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
const storedFunctions = sqlBodies(stored);
|
|
1939
|
+
for (const [key, body] of sqlBodies(declared)) {
|
|
1940
|
+
const live = storedFunctions.get(key);
|
|
1941
|
+
if (live === undefined || live.body === body.body)
|
|
1942
|
+
continue;
|
|
1943
|
+
found.push({
|
|
1944
|
+
kind: "function",
|
|
1945
|
+
name: body.name,
|
|
1946
|
+
reason: `function ${body.name} body reprints differently`
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
return found;
|
|
1950
|
+
}
|
|
1951
|
+
function domainChecks(source) {
|
|
1952
|
+
const checks = new Map;
|
|
1953
|
+
for (const object of source.objects) {
|
|
1954
|
+
if (object.kind !== "type" || !isDomain(object.definition))
|
|
1955
|
+
continue;
|
|
1956
|
+
checks.set(object.identity.name, object.definition.check.trim());
|
|
1957
|
+
}
|
|
1958
|
+
return checks;
|
|
1959
|
+
}
|
|
1960
|
+
function sqlBodies(source) {
|
|
1961
|
+
const bodies = new Map;
|
|
1962
|
+
for (const object of source.objects) {
|
|
1963
|
+
if (object.kind !== "function" || object.definition.language !== "sql")
|
|
1964
|
+
continue;
|
|
1965
|
+
bodies.set(functionKey(object), {
|
|
1966
|
+
name: object.identity.name,
|
|
1967
|
+
body: object.definition.body.trim()
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
return bodies;
|
|
1971
|
+
}
|
|
1972
|
+
function functionKey(object) {
|
|
1973
|
+
const args = object.identity.argTypes.map((typeName) => canonicalTypeName(typeName)).join(",");
|
|
1974
|
+
return `${object.identity.name}(${args})`;
|
|
1975
|
+
}
|
|
1976
|
+
function stepIndex(steps, kind, name) {
|
|
1977
|
+
let found;
|
|
1978
|
+
steps.forEach((step, index) => {
|
|
1979
|
+
if (mentions(normalizedSql(step.sql), kind, name))
|
|
1980
|
+
found = index;
|
|
1981
|
+
});
|
|
1982
|
+
return found;
|
|
1983
|
+
}
|
|
1984
|
+
function mentions(text, kind, name) {
|
|
1985
|
+
if (kind === "domain" && !/^(?:create|alter) domain\b/.test(text))
|
|
1986
|
+
return false;
|
|
1987
|
+
if (kind === "function") {
|
|
1988
|
+
if (!/^create(?: or replace)? function\b/.test(text) || !/\blanguage sql\b/.test(text)) {
|
|
1989
|
+
return false;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
const quoted = `"${name}"`;
|
|
1993
|
+
if (text.includes(quoted))
|
|
1994
|
+
return true;
|
|
1995
|
+
return new RegExp(`\\b${escapeRegExp(name.toLowerCase())}\\b`).test(text);
|
|
1996
|
+
}
|
|
1997
|
+
function escapeRegExp(value) {
|
|
1998
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1383
2001
|
// src/tooling/migrate/lint.ts
|
|
1384
2002
|
function lintPlan(plan, before, after) {
|
|
1385
2003
|
const existingTables = tableNames(before);
|
|
@@ -1389,6 +2007,9 @@ function lintPlan(plan, before, after) {
|
|
|
1389
2007
|
});
|
|
1390
2008
|
return findings;
|
|
1391
2009
|
}
|
|
2010
|
+
function lintReprints(declared, stored, steps) {
|
|
2011
|
+
return reprintHits(declared, stored, steps).map((hit) => finding(hit.severity, hit.code, hit.place, hit.reason));
|
|
2012
|
+
}
|
|
1392
2013
|
function lintCatalog(source) {
|
|
1393
2014
|
const findings = [];
|
|
1394
2015
|
for (const object of source.objects) {
|
|
@@ -1404,15 +2025,15 @@ function lintCatalog(source) {
|
|
|
1404
2025
|
return findings;
|
|
1405
2026
|
}
|
|
1406
2027
|
function lintMigrationDirectory(directory, pending) {
|
|
1407
|
-
if (!
|
|
2028
|
+
if (!existsSync3(directory))
|
|
1408
2029
|
return [];
|
|
1409
2030
|
const names = readdirSync2(directory).filter((file) => file.endsWith(".sql")).sort();
|
|
1410
2031
|
let before = catalog([]);
|
|
1411
2032
|
const findings = [];
|
|
1412
2033
|
for (const file of names) {
|
|
1413
2034
|
const id = file.slice(0, -".sql".length);
|
|
1414
|
-
const plan = readMigrationPlan(
|
|
1415
|
-
const after = parseCatalog2(
|
|
2035
|
+
const plan = readMigrationPlan(join3(directory, file));
|
|
2036
|
+
const after = parseCatalog2(readFileSync3(join3(directory, `${id}.catalog.json`), "utf8"));
|
|
1416
2037
|
if (pending === undefined || pending.has(id)) {
|
|
1417
2038
|
for (const item of lintPlan(plan, before, after)) {
|
|
1418
2039
|
findings.push({ ...item, place: `${id} ${item.place}` });
|
|
@@ -1462,6 +2083,8 @@ function lintStep(step, number, before, after, existingTables, earlier, later) {
|
|
|
1462
2083
|
triggered.add(hit.code);
|
|
1463
2084
|
}
|
|
1464
2085
|
}
|
|
2086
|
+
if (statementMayReprint(statement.sql))
|
|
2087
|
+
triggered.add(REPRINT_CODE);
|
|
1465
2088
|
const place = multi ? `step ${String(number)} statement ${String(index + 1)}` : `step ${String(number)}`;
|
|
1466
2089
|
perStatement.push({ place, hits });
|
|
1467
2090
|
});
|
|
@@ -1531,8 +2154,16 @@ function columnPlace(column) {
|
|
|
1531
2154
|
}
|
|
1532
2155
|
|
|
1533
2156
|
// src/tooling/migrate/project.ts
|
|
1534
|
-
import {
|
|
1535
|
-
|
|
2157
|
+
import {
|
|
2158
|
+
existsSync as existsSync4,
|
|
2159
|
+
mkdirSync,
|
|
2160
|
+
readdirSync as readdirSync3,
|
|
2161
|
+
readFileSync as readFileSync4,
|
|
2162
|
+
statSync,
|
|
2163
|
+
unlinkSync,
|
|
2164
|
+
writeFileSync as writeFileSync2
|
|
2165
|
+
} from "node:fs";
|
|
2166
|
+
import { basename, dirname, join as join4 } from "node:path";
|
|
1536
2167
|
import { pathToFileURL } from "node:url";
|
|
1537
2168
|
|
|
1538
2169
|
// src/dialects/pg/declarations.ts
|
|
@@ -2143,6 +2774,131 @@ function columnNames(target, built, table) {
|
|
|
2143
2774
|
return built.flatMap((object) => object.kind === "column" && object.identity.parent.name === table ? [object.identity.name] : []);
|
|
2144
2775
|
}
|
|
2145
2776
|
|
|
2777
|
+
// src/tooling/migrate/place.ts
|
|
2778
|
+
function placeCatalog(source, namespace, externalNamespaces) {
|
|
2779
|
+
if (namespace === "public" && externalNamespaces.size === 0)
|
|
2780
|
+
return source;
|
|
2781
|
+
const homes = tableHomes(source, namespace, externalNamespaces);
|
|
2782
|
+
let changed = false;
|
|
2783
|
+
const objects = source.objects.map((object) => {
|
|
2784
|
+
const next = relocateObject(object, homes, namespace);
|
|
2785
|
+
if (next !== object)
|
|
2786
|
+
changed = true;
|
|
2787
|
+
return next;
|
|
2788
|
+
});
|
|
2789
|
+
if (!changed)
|
|
2790
|
+
return source;
|
|
2791
|
+
return catalog(objects);
|
|
2792
|
+
}
|
|
2793
|
+
function tableHomes(source, namespace, externalNamespaces) {
|
|
2794
|
+
const homes = new Map;
|
|
2795
|
+
for (const object of source.objects) {
|
|
2796
|
+
if (object.kind !== "table" && object.kind !== "view" && object.kind !== "materializedView") {
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
const home = object.owner === "external" ? externalNamespaces.get(object.identity.name) ?? namespace : namespace;
|
|
2800
|
+
homes.set(object.identity.name, home);
|
|
2801
|
+
}
|
|
2802
|
+
return homes;
|
|
2803
|
+
}
|
|
2804
|
+
function relocateObject(object, homes, namespace) {
|
|
2805
|
+
const dependencies = relocateEdges(object.dependencies, homes, namespace);
|
|
2806
|
+
if (object.kind === "extension") {
|
|
2807
|
+
if (object.definition.schema === "public" && namespace !== "public") {
|
|
2808
|
+
return { ...object, definition: { ...object.definition, schema: namespace }, dependencies };
|
|
2809
|
+
}
|
|
2810
|
+
return dependencies === object.dependencies ? object : { ...object, dependencies };
|
|
2811
|
+
}
|
|
2812
|
+
if (object.kind === "role") {
|
|
2813
|
+
return dependencies === object.dependencies ? object : { ...object, dependencies };
|
|
2814
|
+
}
|
|
2815
|
+
const identity = relocateIdentity(object.identity, homes, namespace);
|
|
2816
|
+
if (object.kind === "constraint") {
|
|
2817
|
+
const references = relocateReferences(object, homes);
|
|
2818
|
+
if (identity === object.identity && references === object.definition.references && dependencies === object.dependencies) {
|
|
2819
|
+
return object;
|
|
2820
|
+
}
|
|
2821
|
+
if (references === undefined || references === object.definition.references) {
|
|
2822
|
+
return { ...object, identity, dependencies };
|
|
2823
|
+
}
|
|
2824
|
+
return {
|
|
2825
|
+
...object,
|
|
2826
|
+
identity,
|
|
2827
|
+
definition: { ...object.definition, references },
|
|
2828
|
+
dependencies
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
if (identity === object.identity && dependencies === object.dependencies)
|
|
2832
|
+
return object;
|
|
2833
|
+
return { ...object, identity, dependencies };
|
|
2834
|
+
}
|
|
2835
|
+
function relocateReferences(object, homes) {
|
|
2836
|
+
const references = object.definition.references;
|
|
2837
|
+
if (references === undefined)
|
|
2838
|
+
return;
|
|
2839
|
+
const home = homes.get(references.parent.name);
|
|
2840
|
+
if (home === undefined)
|
|
2841
|
+
return references;
|
|
2842
|
+
const parent = moveRef(references.parent, home);
|
|
2843
|
+
if (parent === references.parent)
|
|
2844
|
+
return references;
|
|
2845
|
+
return { ...references, parent };
|
|
2846
|
+
}
|
|
2847
|
+
function relocateEdges(edges, homes, namespace) {
|
|
2848
|
+
let changed = false;
|
|
2849
|
+
const next = edges.map((edge) => {
|
|
2850
|
+
const target = relocateIdentity(edge.target, homes, namespace);
|
|
2851
|
+
if (target === edge.target)
|
|
2852
|
+
return edge;
|
|
2853
|
+
changed = true;
|
|
2854
|
+
return { target };
|
|
2855
|
+
});
|
|
2856
|
+
return changed ? next : edges;
|
|
2857
|
+
}
|
|
2858
|
+
function relocateIdentity(identity, homes, namespace) {
|
|
2859
|
+
if (identity.kind === "role" || identity.kind === "extension")
|
|
2860
|
+
return identity;
|
|
2861
|
+
if (identity.kind === "column" || identity.kind === "index" || identity.kind === "constraint" || identity.kind === "trigger" || identity.kind === "policy") {
|
|
2862
|
+
const home = homes.get(identity.parent.name) ?? namespace;
|
|
2863
|
+
const parent = moveRef(identity.parent, home);
|
|
2864
|
+
if (parent === identity.parent)
|
|
2865
|
+
return identity;
|
|
2866
|
+
return { ...identity, parent };
|
|
2867
|
+
}
|
|
2868
|
+
if (identity.kind === "grant") {
|
|
2869
|
+
const home = homes.get(identity.object.name) ?? namespace;
|
|
2870
|
+
const moved = moveNamespace(identity.object.namespace, home);
|
|
2871
|
+
if (moved === identity.object.namespace)
|
|
2872
|
+
return identity;
|
|
2873
|
+
return { ...identity, object: { ...identity.object, namespace: moved } };
|
|
2874
|
+
}
|
|
2875
|
+
if (identity.kind === "defaultPrivilege") {
|
|
2876
|
+
const moved = moveNamespace(identity.namespace, namespace);
|
|
2877
|
+
if (moved === identity.namespace)
|
|
2878
|
+
return identity;
|
|
2879
|
+
return { ...identity, namespace: moved };
|
|
2880
|
+
}
|
|
2881
|
+
if (identity.kind === "table" || identity.kind === "view" || identity.kind === "materializedView" || identity.kind === "sequence" || identity.kind === "type" || identity.kind === "function") {
|
|
2882
|
+
const home = homes.get(identity.name) ?? namespace;
|
|
2883
|
+
const moved = moveNamespace(identity.namespace, home);
|
|
2884
|
+
if (moved === identity.namespace)
|
|
2885
|
+
return identity;
|
|
2886
|
+
return { ...identity, namespace: moved };
|
|
2887
|
+
}
|
|
2888
|
+
return identity;
|
|
2889
|
+
}
|
|
2890
|
+
function moveRef(ref, namespace) {
|
|
2891
|
+
const moved = moveNamespace(ref.namespace, namespace);
|
|
2892
|
+
if (moved === ref.namespace)
|
|
2893
|
+
return ref;
|
|
2894
|
+
return { ...ref, namespace: moved };
|
|
2895
|
+
}
|
|
2896
|
+
function moveNamespace(namespace, name) {
|
|
2897
|
+
if (namespace.form !== "static" || namespace.name === name)
|
|
2898
|
+
return namespace;
|
|
2899
|
+
return staticNamespace2(name);
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2146
2902
|
// src/tooling/migrate/estimate.ts
|
|
2147
2903
|
var LARGE_TABLE_ROWS = 1e6;
|
|
2148
2904
|
var ESTIMATE_SQL = `
|
|
@@ -2336,11 +3092,35 @@ function trimZero(text) {
|
|
|
2336
3092
|
return text.endsWith(".0") ? text.slice(0, -2) : text;
|
|
2337
3093
|
}
|
|
2338
3094
|
|
|
3095
|
+
// src/tooling/migrate/plan-json.ts
|
|
3096
|
+
function planDocument(plan, findings, lockText) {
|
|
3097
|
+
return {
|
|
3098
|
+
name: plan.name,
|
|
3099
|
+
class: plan.class,
|
|
3100
|
+
steps: plan.steps.map((step) => ({
|
|
3101
|
+
class: step.class,
|
|
3102
|
+
lock: lockText === undefined ? step.lock : lockText(step),
|
|
3103
|
+
sql: step.sql
|
|
3104
|
+
})),
|
|
3105
|
+
findings: findings.map((finding) => ({
|
|
3106
|
+
severity: finding.severity,
|
|
3107
|
+
code: finding.code,
|
|
3108
|
+
place: finding.place,
|
|
3109
|
+
reason: finding.reason,
|
|
3110
|
+
fix: finding.fix
|
|
3111
|
+
}))
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
function formatPlanJson(plan, findings, lockText) {
|
|
3115
|
+
return `${JSON.stringify(planDocument(plan, findings, lockText))}
|
|
3116
|
+
`;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
2339
3119
|
// src/tooling/migrate/project.ts
|
|
2340
3120
|
async function buildProject(cwd) {
|
|
2341
3121
|
const config = await loadConfig(cwd);
|
|
2342
3122
|
const built = await loadSchema(cwd, config.schema);
|
|
2343
|
-
return writeArtifact(cwd, config, withRoles(built, config));
|
|
3123
|
+
return writeArtifact(cwd, config, withRoles(built, config, cwd));
|
|
2344
3124
|
}
|
|
2345
3125
|
async function generateProject(cwd, name, flags) {
|
|
2346
3126
|
const prepared = await prepare(cwd, name, flags);
|
|
@@ -2352,17 +3132,20 @@ async function generateProject(cwd, name, flags) {
|
|
|
2352
3132
|
const directory = projectPath(cwd, prepared.config.migrations ?? "migrations");
|
|
2353
3133
|
mkdirSync(directory, { recursive: true });
|
|
2354
3134
|
const base = `${nextNumber(directory)}_${safeName(name)}`;
|
|
2355
|
-
const sqlPath =
|
|
2356
|
-
|
|
2357
|
-
|
|
3135
|
+
const sqlPath = join4(directory, `${base}.sql`);
|
|
3136
|
+
writeFileSync2(sqlPath, formatPlan(prepared.plan));
|
|
3137
|
+
writeFileSync2(join4(directory, `${base}.catalog.json`), serializeCatalog2(prepared.built.catalog));
|
|
2358
3138
|
return { path: sqlPath, findings: lintPrepared(prepared) };
|
|
2359
3139
|
}
|
|
2360
3140
|
async function planProject(cwd, name, flags, invoke) {
|
|
2361
3141
|
const prepared = await prepare(cwd, name, flags);
|
|
3142
|
+
const findings = lintPrepared(prepared);
|
|
3143
|
+
const lockText = await planLockText(prepared.config, prepared.plan, invoke);
|
|
2362
3144
|
return {
|
|
2363
3145
|
plan: prepared.plan,
|
|
2364
|
-
findings
|
|
2365
|
-
text:
|
|
3146
|
+
findings,
|
|
3147
|
+
text: formatPlan(prepared.plan, lockText),
|
|
3148
|
+
json: formatPlanJson(prepared.plan, findings, lockText)
|
|
2366
3149
|
};
|
|
2367
3150
|
}
|
|
2368
3151
|
async function checkProject(cwd, flags) {
|
|
@@ -2378,8 +3161,8 @@ async function checkProject(cwd, flags) {
|
|
|
2378
3161
|
assertTargetAlias(targets);
|
|
2379
3162
|
if (opened.config.tables !== undefined) {
|
|
2380
3163
|
const directory = projectPath(cwd, opened.config.tables);
|
|
2381
|
-
const files =
|
|
2382
|
-
const source =
|
|
3164
|
+
const files = existsSync4(directory) ? readdirSync3(directory) : [];
|
|
3165
|
+
const source = readFileSync4(projectPath(cwd, opened.config.schema), "utf8");
|
|
2383
3166
|
const missing = unlistedTableFiles(source, files);
|
|
2384
3167
|
if (missing.length > 0) {
|
|
2385
3168
|
throw new OkmError2("OKM1024", `Table file not in schema: ${missing.join(", ")}.`, {
|
|
@@ -2387,7 +3170,8 @@ async function checkProject(cwd, flags) {
|
|
|
2387
3170
|
});
|
|
2388
3171
|
}
|
|
2389
3172
|
}
|
|
2390
|
-
|
|
3173
|
+
assertSnapshots(cwd, opened.config, opened.built.catalog);
|
|
3174
|
+
const findings = [...lintOpened(opened), ...couplingFindings(opened.built.catalog)];
|
|
2391
3175
|
if (hasError(findings))
|
|
2392
3176
|
throw lintRefusal(findings);
|
|
2393
3177
|
const named = flags?.target;
|
|
@@ -2398,8 +3182,9 @@ async function checkProject(cwd, flags) {
|
|
|
2398
3182
|
const target = selectTarget(opened.config, named);
|
|
2399
3183
|
assertTargetPolicy(target, "check", flags?.allowProtected ?? false);
|
|
2400
3184
|
assertTargetPolicy(target, "drift", flags?.allowProtected ?? false);
|
|
2401
|
-
const { assertAuthorDrift } = await import("./
|
|
3185
|
+
const { assertAuthorDrift } = await import("./ckz05z15.js");
|
|
2402
3186
|
const allowPooler = flags?.allowPooler === true || opened.config.allowPooler === true;
|
|
3187
|
+
await assertLiveExternal(target.url, opened.built.catalog, loadSnapshots(cwd, opened.config));
|
|
2403
3188
|
await assertAuthorDrift(target.url, opened.built.catalog, opened.config.roles, allowPooler);
|
|
2404
3189
|
return findings;
|
|
2405
3190
|
}
|
|
@@ -2429,6 +3214,7 @@ async function prepare(cwd, name, flags) {
|
|
|
2429
3214
|
renames: opened.renames,
|
|
2430
3215
|
replacements: flags.map((flag) => parseReplace(flag)),
|
|
2431
3216
|
name,
|
|
3217
|
+
...placedSchema(opened.config),
|
|
2432
3218
|
...batchSizeField(opened.config.backfill?.batchSize)
|
|
2433
3219
|
});
|
|
2434
3220
|
return {
|
|
@@ -2442,26 +3228,25 @@ async function prepare(cwd, name, flags) {
|
|
|
2442
3228
|
function lintPrepared(prepared) {
|
|
2443
3229
|
return lintPlan(prepared.plan, prepared.before, prepared.after);
|
|
2444
3230
|
}
|
|
2445
|
-
async function
|
|
2446
|
-
const offline = formatPlan(plan);
|
|
3231
|
+
async function planLockText(config, plan, invoke) {
|
|
2447
3232
|
const targets = listTargets(config);
|
|
2448
3233
|
if (targets.length === 0)
|
|
2449
|
-
return
|
|
3234
|
+
return;
|
|
2450
3235
|
assertTargetAlias(targets);
|
|
2451
3236
|
const named = invoke?.target;
|
|
2452
3237
|
if (named === undefined && targets.length !== 1)
|
|
2453
|
-
return
|
|
3238
|
+
return;
|
|
2454
3239
|
const target = selectTarget(config, named);
|
|
2455
3240
|
assertTargetPolicy(target, "plan", invoke?.allowProtected ?? false);
|
|
2456
3241
|
let estimates;
|
|
2457
3242
|
try {
|
|
2458
3243
|
const names = plan.steps.flatMap((step) => step.tables ?? []);
|
|
2459
|
-
estimates = await readRowEstimates(target.url,
|
|
3244
|
+
estimates = await readRowEstimates(target.url, catalogNamespace(config), names);
|
|
2460
3245
|
} catch {
|
|
2461
|
-
return
|
|
3246
|
+
return;
|
|
2462
3247
|
}
|
|
2463
3248
|
const aliases = renameAliases(plan.steps);
|
|
2464
|
-
return
|
|
3249
|
+
return (step) => annotateLock(step, estimates, aliases);
|
|
2465
3250
|
}
|
|
2466
3251
|
function lintOpened(opened) {
|
|
2467
3252
|
const plan = planMigration({
|
|
@@ -2469,6 +3254,7 @@ function lintOpened(opened) {
|
|
|
2469
3254
|
after: opened.built.catalog,
|
|
2470
3255
|
renames: opened.renames,
|
|
2471
3256
|
name: "check",
|
|
3257
|
+
...placedSchema(opened.config),
|
|
2472
3258
|
...batchSizeField(opened.config.backfill?.batchSize)
|
|
2473
3259
|
});
|
|
2474
3260
|
return [
|
|
@@ -2478,7 +3264,7 @@ function lintOpened(opened) {
|
|
|
2478
3264
|
}
|
|
2479
3265
|
async function openProject(cwd) {
|
|
2480
3266
|
const config = await loadConfig(cwd);
|
|
2481
|
-
const built =
|
|
3267
|
+
const built = await loadBuiltSchema(cwd, config);
|
|
2482
3268
|
const declarations = schemaDeclarations(built);
|
|
2483
3269
|
const previous = readPrevious(projectPath(cwd, config.migrations ?? "migrations"));
|
|
2484
3270
|
const stale = staleRenames(previous?.catalog, declarations.renames);
|
|
@@ -2494,15 +3280,22 @@ async function openProject(cwd) {
|
|
|
2494
3280
|
renames: declarations.renames
|
|
2495
3281
|
};
|
|
2496
3282
|
}
|
|
2497
|
-
function withRoles(built, config) {
|
|
3283
|
+
function withRoles(built, config, cwd) {
|
|
2498
3284
|
const marks = ownershipMarks(built);
|
|
2499
3285
|
const stamped = marks.size === 0 ? built.catalog : stampOwnership(built.catalog, marks);
|
|
3286
|
+
const placed = placeCatalog(stamped, catalogNamespace(config), externalNamespaceMap(loadSnapshots(cwd, config)));
|
|
2500
3287
|
const roles = config.roles;
|
|
2501
|
-
const catalog = roles === undefined ?
|
|
3288
|
+
const catalog = roles === undefined ? placed : attachRoles(placed, roles, exposureFor(built, roles));
|
|
2502
3289
|
if (catalog === built.catalog)
|
|
2503
3290
|
return built;
|
|
2504
3291
|
return { ...built, catalog };
|
|
2505
3292
|
}
|
|
3293
|
+
function placedSchema(config) {
|
|
3294
|
+
const namespace = catalogNamespace(config);
|
|
3295
|
+
if (namespace === "public")
|
|
3296
|
+
return;
|
|
3297
|
+
return { schema: namespace };
|
|
3298
|
+
}
|
|
2506
3299
|
function ownershipMarks(built) {
|
|
2507
3300
|
const marks = new Map;
|
|
2508
3301
|
const models = built.model;
|
|
@@ -2539,9 +3332,9 @@ var GIT_ATTRIBUTES = [
|
|
|
2539
3332
|
`);
|
|
2540
3333
|
function writeGitAttributes(cwd) {
|
|
2541
3334
|
const path = projectPath(cwd, ".gitattributes");
|
|
2542
|
-
if (
|
|
3335
|
+
if (existsSync4(path))
|
|
2543
3336
|
return;
|
|
2544
|
-
|
|
3337
|
+
writeFileSync2(path, GIT_ATTRIBUTES);
|
|
2545
3338
|
}
|
|
2546
3339
|
function typesText(cwd, spec, built) {
|
|
2547
3340
|
return `${emitRowTypes(built, jsonColumnTypes(readSchemaSource(cwd, spec)))}${referenceAugmentation(built)}`;
|
|
@@ -2550,7 +3343,7 @@ function readSchemaSource(cwd, spec) {
|
|
|
2550
3343
|
const parts = [];
|
|
2551
3344
|
const seen = new Set;
|
|
2552
3345
|
const visit = (file) => {
|
|
2553
|
-
if (seen.has(file) || !
|
|
3346
|
+
if (seen.has(file) || !existsSync4(file))
|
|
2554
3347
|
return;
|
|
2555
3348
|
let stat;
|
|
2556
3349
|
try {
|
|
@@ -2561,16 +3354,16 @@ function readSchemaSource(cwd, spec) {
|
|
|
2561
3354
|
if (!stat.isFile())
|
|
2562
3355
|
return;
|
|
2563
3356
|
seen.add(file);
|
|
2564
|
-
const text =
|
|
3357
|
+
const text = readFileSync4(file, "utf8");
|
|
2565
3358
|
parts.push(text);
|
|
2566
3359
|
for (const match of text.matchAll(/from\s+["'](\.[^"']+)["']/g)) {
|
|
2567
3360
|
const rel = match[1];
|
|
2568
3361
|
if (rel === undefined)
|
|
2569
3362
|
continue;
|
|
2570
|
-
const base =
|
|
2571
|
-
const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}.mts`,
|
|
3363
|
+
const base = join4(dirname(file), rel);
|
|
3364
|
+
const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}.mts`, join4(base, "index.ts")];
|
|
2572
3365
|
for (const candidate of candidates) {
|
|
2573
|
-
if (
|
|
3366
|
+
if (existsSync4(candidate)) {
|
|
2574
3367
|
visit(candidate);
|
|
2575
3368
|
break;
|
|
2576
3369
|
}
|
|
@@ -2586,19 +3379,35 @@ function writeArtifact(cwd, config, built) {
|
|
|
2586
3379
|
mkdirSync(directory, { recursive: true });
|
|
2587
3380
|
const startup = startupCatalog2(built.catalog);
|
|
2588
3381
|
const text = catalogFileText2(serializeCatalog2(startup));
|
|
2589
|
-
|
|
2590
|
-
|
|
3382
|
+
writeFileSync2(join4(directory, "catalog.json"), text);
|
|
3383
|
+
writeFileSync2(join4(directory, "catalog.hash"), `${sha2562(text)}
|
|
2591
3384
|
`);
|
|
2592
|
-
|
|
3385
|
+
writeFileSync2(join4(directory, "types.d.ts"), typesText(cwd, config.schema, built));
|
|
3386
|
+
writeNamespace(directory, catalogNamespace(config));
|
|
2593
3387
|
return directory;
|
|
2594
3388
|
}
|
|
3389
|
+
function writeNamespace(directory, namespace) {
|
|
3390
|
+
const path = join4(directory, "namespace");
|
|
3391
|
+
if (namespace === "public") {
|
|
3392
|
+
if (existsSync4(path))
|
|
3393
|
+
unlinkSync(path);
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
writeFileSync2(path, `${namespace}
|
|
3397
|
+
`);
|
|
3398
|
+
}
|
|
2595
3399
|
async function loadBuiltSchema(cwd, config) {
|
|
2596
|
-
return withRoles(await loadSchema(cwd, config.schema), config);
|
|
3400
|
+
return withRoles(await loadSchema(cwd, config.schema), config, cwd);
|
|
3401
|
+
}
|
|
3402
|
+
async function exportProject(cwd, args) {
|
|
3403
|
+
const config = await loadConfig(cwd);
|
|
3404
|
+
const built = await loadBuiltSchema(cwd, config);
|
|
3405
|
+
return renderExport(cwd, config, built.catalog, args);
|
|
2597
3406
|
}
|
|
2598
3407
|
async function loadConfig(cwd) {
|
|
2599
3408
|
const path = projectPath(cwd, "okmodel.config.ts");
|
|
2600
3409
|
const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
|
|
2601
|
-
const record =
|
|
3410
|
+
const record = isRecord2(imported) ? imported : {};
|
|
2602
3411
|
const config = record.default ?? record.config;
|
|
2603
3412
|
if (!isConfig(config)) {
|
|
2604
3413
|
throw new OkmError2("invalid", "okmodel.config.ts must default-export defineConfig(...).");
|
|
@@ -2608,7 +3417,7 @@ async function loadConfig(cwd) {
|
|
|
2608
3417
|
async function loadSchema(cwd, spec) {
|
|
2609
3418
|
const path = projectPath(cwd, spec);
|
|
2610
3419
|
const imported = await import(`${pathToFileURL(path).href}?okm=${Date.now()}`);
|
|
2611
|
-
const record =
|
|
3420
|
+
const record = isRecord2(imported) ? imported : {};
|
|
2612
3421
|
for (const value of Object.values(record)) {
|
|
2613
3422
|
if (!isBuilt(value))
|
|
2614
3423
|
continue;
|
|
@@ -2618,13 +3427,13 @@ async function loadSchema(cwd, spec) {
|
|
|
2618
3427
|
throw new OkmError2("invalid", `${spec} must export a schema().`);
|
|
2619
3428
|
}
|
|
2620
3429
|
function readPrevious(directory) {
|
|
2621
|
-
if (!
|
|
3430
|
+
if (!existsSync4(directory))
|
|
2622
3431
|
return;
|
|
2623
3432
|
const catalogs = readdirSync3(directory).filter((file) => file.endsWith(".catalog.json")).sort();
|
|
2624
3433
|
const latest = catalogs.at(-1);
|
|
2625
3434
|
if (latest === undefined)
|
|
2626
3435
|
return;
|
|
2627
|
-
const text =
|
|
3436
|
+
const text = readFileSync4(join4(directory, latest), "utf8");
|
|
2628
3437
|
return { catalog: parseCatalog2(text) };
|
|
2629
3438
|
}
|
|
2630
3439
|
function referenceAugmentation(source) {
|
|
@@ -2649,7 +3458,7 @@ function referenceAugmentation(source) {
|
|
|
2649
3458
|
}
|
|
2650
3459
|
function nextNumber(directory) {
|
|
2651
3460
|
let max = 0;
|
|
2652
|
-
if (
|
|
3461
|
+
if (existsSync4(directory)) {
|
|
2653
3462
|
for (const file of readdirSync3(directory)) {
|
|
2654
3463
|
const match = /^(\d+)_/.exec(file);
|
|
2655
3464
|
const value = match?.[1];
|
|
@@ -2666,14 +3475,14 @@ function safeName(name) {
|
|
|
2666
3475
|
return name;
|
|
2667
3476
|
}
|
|
2668
3477
|
function isBuilt(value) {
|
|
2669
|
-
if (!
|
|
3478
|
+
if (!isRecord2(value) || !Array.isArray(value.tables))
|
|
2670
3479
|
return false;
|
|
2671
3480
|
return "catalog" in value;
|
|
2672
3481
|
}
|
|
2673
3482
|
function isConfig(value) {
|
|
2674
|
-
return
|
|
3483
|
+
return isRecord2(value) && typeof value.schema === "string";
|
|
2675
3484
|
}
|
|
2676
|
-
function
|
|
3485
|
+
function isRecord2(value) {
|
|
2677
3486
|
return typeof value === "object" && value !== null;
|
|
2678
3487
|
}
|
|
2679
3488
|
var SKIP_SCAN = new Set(["node_modules", "dist", "coverage"]);
|
|
@@ -2698,7 +3507,7 @@ function directoryImportsValidate(dir) {
|
|
|
2698
3507
|
for (const entry of entries) {
|
|
2699
3508
|
if (entry.name.startsWith(".") || SKIP_SCAN.has(entry.name))
|
|
2700
3509
|
continue;
|
|
2701
|
-
const path =
|
|
3510
|
+
const path = join4(dir, entry.name);
|
|
2702
3511
|
if (entry.isDirectory()) {
|
|
2703
3512
|
if (directoryImportsValidate(path))
|
|
2704
3513
|
return true;
|
|
@@ -2706,7 +3515,7 @@ function directoryImportsValidate(dir) {
|
|
|
2706
3515
|
}
|
|
2707
3516
|
if (!/\.(?:ts|tsx|mts|cts)$/.test(entry.name))
|
|
2708
3517
|
continue;
|
|
2709
|
-
if (runtimeImportsValidate(
|
|
3518
|
+
if (runtimeImportsValidate(readFileSync4(path, "utf8")))
|
|
2710
3519
|
return true;
|
|
2711
3520
|
}
|
|
2712
3521
|
return false;
|
|
@@ -2728,4 +3537,4 @@ function runtimeImportsValidate(source) {
|
|
|
2728
3537
|
return false;
|
|
2729
3538
|
}
|
|
2730
3539
|
|
|
2731
|
-
export { projectPath, errorDoc, loadMigrations, readMigrationPlan, loadHeadSnapshot, lintMigrationDirectory, formatFindings, hasError, lintRefusal, buildProject, generateProject, planProject, checkProject, projectHead, openProject, loadBuiltSchema, loadConfig, loadSchema };
|
|
3540
|
+
export { projectPath, LOGICAL_FORMAT, catalogNamespace, loadSnapshots, assertSnapshots, assertLiveExternal, errorDoc, loadMigrations, readMigrationPlan, loadHeadSnapshot, lintReprints, lintMigrationDirectory, formatFindings, hasError, lintRefusal, buildProject, generateProject, planProject, checkProject, projectHead, openProject, loadBuiltSchema, exportProject, loadConfig, loadSchema };
|