okmodel 0.2.1 → 0.3.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 +710 -36
- package/dist/contracts/catalog/enum.d.ts +31 -4
- package/dist/contracts/catalog/extension.d.ts +61 -0
- package/dist/contracts/catalog/privilege.d.ts +64 -0
- package/dist/contracts/catalog/routine.d.ts +56 -0
- package/dist/contracts/catalog/types.d.ts +164 -10
- package/dist/contracts/catalog/view.d.ts +73 -0
- package/dist/dialects/pg/column.d.ts +3 -1
- package/dist/dialects/pg/ddl.d.ts +65 -1
- package/dist/dialects/pg/enum.d.ts +10 -8
- package/dist/dialects/pg/ext/citext.d.ts +14 -0
- package/dist/dialects/pg/ext/citext.js +11 -0
- package/dist/dialects/pg/ext/index.d.ts +52 -0
- package/dist/dialects/pg/ext/index.js +6 -0
- package/dist/dialects/pg/ext/pg-trgm.d.ts +53 -0
- package/dist/dialects/pg/ext/pg-trgm.js +42 -0
- package/dist/dialects/pg/fn/index.d.ts +107 -0
- package/dist/dialects/pg/fn/index.js +175 -0
- package/dist/dialects/pg/fn/touch.d.ts +14 -0
- package/dist/dialects/pg/index.js +104 -19
- package/dist/dialects/pg/introspect.d.ts +10 -1
- package/dist/dialects/pg/operators.d.ts +8 -1
- package/dist/dialects/pg/role/check.d.ts +45 -0
- package/dist/dialects/pg/role/index.d.ts +50 -0
- package/dist/dialects/pg/role/sql.d.ts +44 -0
- package/dist/dialects/pg/table.d.ts +7 -0
- package/dist/dialects/pg/tenancy.d.ts +9 -0
- package/dist/dialects/pg/view/client.d.ts +30 -0
- package/dist/dialects/pg/view/depend.d.ts +35 -0
- package/dist/dialects/pg/view/index.d.ts +114 -0
- package/dist/dialects/pg/view/index.js +289 -0
- package/dist/dialects/pg/view/scratch.d.ts +21 -0
- package/dist/dialects/pg/where.d.ts +2 -2
- package/dist/runtime/pg/bun.js +3 -3
- package/dist/runtime/pg/pg.js +3 -3
- package/dist/runtime/pg/pglite.js +3 -3
- package/dist/runtime/pg/postgresjs.js +3 -3
- package/dist/runtime/tenancy/index.js +6 -3
- package/dist/runtime/traits/index.d.ts +7 -2
- package/dist/runtime/traits/index.js +149 -12
- package/dist/runtime/validate/index.js +4 -4
- package/dist/shared/{ytg3xf9p.js → 01yr20s5.js} +3 -3
- package/dist/shared/{cstgkzdx.js → 0jc3ffzf.js} +1 -1
- package/dist/shared/{xkz8g6by.js → 1g0drakv.js} +2 -2
- package/dist/shared/{eaxwrgby.js → 1tfdfvhh.js} +4 -3
- package/dist/shared/2ssqrv66.js +1576 -0
- package/dist/shared/{10tjenmd.js → 3ekj9x92.js} +1 -1
- package/dist/shared/49ypnfwt.js +1122 -0
- package/dist/shared/{sacqha1y.js → 4m9fn52z.js} +2 -2
- package/dist/shared/5nmdenb6.js +1229 -0
- package/dist/shared/{qa5dsc7z.js → 6bazvqwm.js} +3 -3
- package/dist/shared/{x9v1qgeb.js → 6w5j844a.js} +1 -1
- package/dist/shared/{0qkxbm1f.js → 75xkjv0t.js} +1 -1
- package/dist/shared/7fvtrkwr.js +84 -0
- package/dist/shared/94ez8kzh.js +19 -0
- package/dist/shared/{1dp5knt7.js → 999ade2q.js} +2 -2
- package/dist/shared/{3a5ne750.js → 9m80mkac.js} +2 -2
- package/dist/shared/9x25fxhc.js +150 -0
- package/dist/shared/{4b9da9d7.js → axhk06q1.js} +19 -1
- package/dist/shared/{38crxbgn.js → bqha5cg6.js} +3 -3
- package/dist/shared/{zd93g3rq.js → bryqk9k9.js} +1 -1
- package/dist/shared/{n47rd821.js → c8aghh05.js} +1 -1
- package/dist/shared/{dd4f8mnx.js → c9z5g0fm.js} +4 -2
- package/dist/shared/{eqmtn1bj.js → dbjej1tw.js} +71 -14
- package/dist/shared/{ps43134h.js → dyj86cmj.js} +4 -4
- package/dist/shared/{sfh4pk75.js → fptpmvxp.js} +3 -3
- package/dist/shared/{97g2jnrw.js → fs0ex00p.js} +3 -3
- package/dist/shared/{rtnaqre0.js → g4f5kcx9.js} +2 -2
- package/dist/shared/{yw5c610b.js → gjthds9j.js} +2 -2
- package/dist/shared/hcmdtwgh.js +102 -0
- package/dist/shared/{qxa0yb9j.js → jkc6ws1p.js} +2 -54
- package/dist/shared/{mxmcdsh5.js → k34e0xf2.js} +9 -9
- package/dist/shared/{py3nh7jy.js → kgnc8grt.js} +2 -2
- package/dist/shared/{g9f0zqce.js → m78s7898.js} +1 -1
- package/dist/shared/meck53jv.js +99 -0
- package/dist/shared/{v18jva5b.js → mt5wswjz.js} +1 -1
- package/dist/shared/{g1b5zzj9.js → n0dgs0pb.js} +1 -1
- package/dist/shared/{zt8y0wzb.js → n7mcc1gk.js} +1 -1
- package/dist/shared/ne02ng5c.js +68 -0
- package/dist/shared/{e7a268fy.js → p24x2334.js} +1 -1
- package/dist/shared/qbxzvwwc.js +88 -0
- package/dist/shared/{a1gdp7j9.js → qybh48nd.js} +1 -1
- package/dist/shared/{d04j53v8.js → scwxrpx2.js} +5 -4
- package/dist/shared/{d2whjz4k.js → t616ynfn.js} +530 -24
- package/dist/shared/th14ygv2.js +69 -0
- package/dist/shared/{xatfg0gn.js → tmbdywx0.js} +4 -19
- package/dist/shared/{ge03bfca.js → ty3v3hfk.js} +2 -2
- package/dist/shared/wekze34d.js +246 -0
- package/dist/shared/{7mht49yd.js → x6wysh83.js} +59 -18
- package/dist/shared/{4jzzghr6.js → xva8v7qk.js} +8 -8
- package/dist/shared/{ecdzej21.js → xw1d69vd.js} +7 -7
- package/dist/shared/y005b761.js +112 -0
- package/dist/shared/{mrq77sgw.js → ydaz1sv3.js} +1 -1
- package/dist/tooling/cli.js +32 -8
- package/dist/tooling/migrate/apply.d.ts +16 -0
- package/dist/tooling/migrate/commands.d.ts +2 -1
- package/dist/tooling/migrate/config.d.ts +8 -0
- package/dist/tooling/migrate/doctor.d.ts +25 -0
- package/dist/tooling/migrate/drift.d.ts +19 -0
- package/dist/tooling/migrate/ext.d.ts +17 -0
- package/dist/tooling/migrate/extensions.d.ts +51 -0
- package/dist/tooling/migrate/managed.d.ts +17 -0
- package/dist/tooling/migrate/plan.d.ts +14 -1
- package/dist/tooling/migrate/project.d.ts +25 -1
- package/dist/tooling/migrate/type-name.d.ts +19 -0
- package/package.json +22 -2
- package/dist/shared/5z4q2zda.js +0 -2073
- /package/dist/shared/{9k6vetvf.js → rrstp3kb.js} +0 -0
|
@@ -0,0 +1,1576 @@
|
|
|
1
|
+
import {
|
|
2
|
+
creationOrder2,
|
|
3
|
+
renameColumn2,
|
|
4
|
+
renameTable2
|
|
5
|
+
} from "./t616ynfn.js";
|
|
6
|
+
import {
|
|
7
|
+
quoteIdent,
|
|
8
|
+
ownedByView,
|
|
9
|
+
dropObjectSql,
|
|
10
|
+
createObjectSql,
|
|
11
|
+
functionSql,
|
|
12
|
+
viewSql,
|
|
13
|
+
refreshMaterializedViewSql,
|
|
14
|
+
alterColumnSql,
|
|
15
|
+
identitySequence,
|
|
16
|
+
createTableSql,
|
|
17
|
+
qualify
|
|
18
|
+
} from "./wekze34d.js";
|
|
19
|
+
import {
|
|
20
|
+
isExactVersion,
|
|
21
|
+
compareExtensionVersions,
|
|
22
|
+
sameExtensionDefinition
|
|
23
|
+
} from "./th14ygv2.js";
|
|
24
|
+
import {
|
|
25
|
+
isDomain,
|
|
26
|
+
sameEnumLabels
|
|
27
|
+
} from "./qbxzvwwc.js";
|
|
28
|
+
import {
|
|
29
|
+
quoteLiteral
|
|
30
|
+
} from "./94ez8kzh.js";
|
|
31
|
+
import {
|
|
32
|
+
staticNamespace2,
|
|
33
|
+
identityKey2,
|
|
34
|
+
identityLabel2,
|
|
35
|
+
compareText
|
|
36
|
+
} from "./t4tbsy9h.js";
|
|
37
|
+
import {
|
|
38
|
+
fitIdentifier2
|
|
39
|
+
} from "./nfzc5z0w.js";
|
|
40
|
+
import {
|
|
41
|
+
OkmError2
|
|
42
|
+
} from "./52szzakm.js";
|
|
43
|
+
|
|
44
|
+
// src/dialects/pg/role/sql.ts
|
|
45
|
+
var PRIVILEGE = /^(select|insert|update|delete|execute|usage)$/;
|
|
46
|
+
function privilegeSql(before, after, schema) {
|
|
47
|
+
const beforeBy = index(before);
|
|
48
|
+
const afterBy = index(after);
|
|
49
|
+
const revoke = [];
|
|
50
|
+
const roles = [];
|
|
51
|
+
const defaults = [];
|
|
52
|
+
const grant = [];
|
|
53
|
+
for (const [key, object] of beforeBy) {
|
|
54
|
+
if (afterBy.has(key))
|
|
55
|
+
continue;
|
|
56
|
+
if (object.kind === "grant")
|
|
57
|
+
revoke.push(grantStatement(object, schema, true));
|
|
58
|
+
if (object.kind === "defaultPrivilege")
|
|
59
|
+
revoke.push(defaultStatement(object, true));
|
|
60
|
+
}
|
|
61
|
+
for (const [key, object] of afterBy) {
|
|
62
|
+
const previous = beforeBy.get(key);
|
|
63
|
+
if (object.kind === "role") {
|
|
64
|
+
if (object.owner !== "managed")
|
|
65
|
+
continue;
|
|
66
|
+
if (previous === undefined) {
|
|
67
|
+
roles.push(createRoleSql(object));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (previous.kind !== "role" || previous.owner !== "managed")
|
|
71
|
+
continue;
|
|
72
|
+
if (previous.definition.login !== object.definition.login || previous.definition.inherit !== object.definition.inherit) {
|
|
73
|
+
roles.push(alterRoleSql(object));
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (previous !== undefined)
|
|
78
|
+
continue;
|
|
79
|
+
if (object.kind === "defaultPrivilege")
|
|
80
|
+
defaults.push(defaultStatement(object, false));
|
|
81
|
+
if (object.kind === "grant")
|
|
82
|
+
grant.push(grantStatement(object, schema, false));
|
|
83
|
+
}
|
|
84
|
+
return { revoke, prepare: [...roles, ...defaults], grant };
|
|
85
|
+
}
|
|
86
|
+
function createdRoleName(sql) {
|
|
87
|
+
const match = /^\s*create\s+role\s+(?:"([^"]+)"|([A-Za-z_][\w$]*))/i.exec(sql);
|
|
88
|
+
const quoted = match?.[1];
|
|
89
|
+
if (quoted !== undefined)
|
|
90
|
+
return quoted.replaceAll('""', '"');
|
|
91
|
+
return match?.[2];
|
|
92
|
+
}
|
|
93
|
+
function changesRole(sql) {
|
|
94
|
+
return /^\s*(create|alter)\s+role\b/i.test(sql);
|
|
95
|
+
}
|
|
96
|
+
function index(objects) {
|
|
97
|
+
const by = new Map;
|
|
98
|
+
for (const object of objects) {
|
|
99
|
+
if (object.kind !== "role" && object.kind !== "grant" && object.kind !== "defaultPrivilege") {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
by.set(identityKey2(object.identity), object);
|
|
103
|
+
}
|
|
104
|
+
return by;
|
|
105
|
+
}
|
|
106
|
+
function createRoleSql(object) {
|
|
107
|
+
return `create role ${quoteIdent(object.identity.name)} with nosuperuser nocreatedb nocreaterole ${inherit(object)} ${login(object)}`;
|
|
108
|
+
}
|
|
109
|
+
function alterRoleSql(object) {
|
|
110
|
+
return `alter role ${quoteIdent(object.identity.name)} with ${inherit(object)} ${login(object)}`;
|
|
111
|
+
}
|
|
112
|
+
function login(object) {
|
|
113
|
+
return object.definition.login ? "login" : "nologin";
|
|
114
|
+
}
|
|
115
|
+
function inherit(object) {
|
|
116
|
+
return object.definition.inherit ? "inherit" : "noinherit";
|
|
117
|
+
}
|
|
118
|
+
function grantStatement(object, schema, revoke) {
|
|
119
|
+
const privilege = object.identity.privilege.toLowerCase();
|
|
120
|
+
assertPrivilege(privilege);
|
|
121
|
+
const verb = revoke ? "revoke" : "grant";
|
|
122
|
+
const direction = revoke ? "from" : "to";
|
|
123
|
+
return `${verb} ${privilege} on ${grantTarget(object.identity.object, schema)} ${direction} ${quoteIdent(object.identity.role)}`;
|
|
124
|
+
}
|
|
125
|
+
function defaultStatement(object, revoke) {
|
|
126
|
+
const privilege = object.identity.privilege.toLowerCase();
|
|
127
|
+
assertPrivilege(privilege);
|
|
128
|
+
const verb = revoke ? "revoke" : "grant";
|
|
129
|
+
const direction = revoke ? "from" : "to";
|
|
130
|
+
const namespace = object.identity.namespace.form === "static" ? object.identity.namespace.name : "public";
|
|
131
|
+
return `alter default privileges for role ${quoteIdent(object.identity.forRole)} in schema ${quoteIdent(namespace)} ${verb} ${privilege} on ${objectKindSql(object.identity.objectKind)} ${direction} ${quoteIdent(object.identity.grantee)}`;
|
|
132
|
+
}
|
|
133
|
+
function grantTarget(object, schema) {
|
|
134
|
+
const name = object.namespace.form === "static" ? object.namespace.name : schema;
|
|
135
|
+
if (object.kind === "sequence")
|
|
136
|
+
return `sequence ${qualify(name, object.name)}`;
|
|
137
|
+
if (object.kind === "function")
|
|
138
|
+
return functionTarget(object.name, name);
|
|
139
|
+
if (object.kind === "namespace")
|
|
140
|
+
return `schema ${quoteIdent(object.name)}`;
|
|
141
|
+
return `table ${qualify(name, object.name)}`;
|
|
142
|
+
}
|
|
143
|
+
function functionTarget(name, schema) {
|
|
144
|
+
const open = name.lastIndexOf("(");
|
|
145
|
+
if (open < 0 || !name.endsWith(")"))
|
|
146
|
+
return `function ${qualify(schema, name)}()`;
|
|
147
|
+
const fn = name.slice(0, open);
|
|
148
|
+
const args = name.slice(open + 1, -1);
|
|
149
|
+
return `function ${qualify(schema, fn)}(${args})`;
|
|
150
|
+
}
|
|
151
|
+
function objectKindSql(kind) {
|
|
152
|
+
if (kind === "function")
|
|
153
|
+
return "functions";
|
|
154
|
+
if (kind === "sequence")
|
|
155
|
+
return "sequences";
|
|
156
|
+
return "tables";
|
|
157
|
+
}
|
|
158
|
+
function assertPrivilege(privilege) {
|
|
159
|
+
if (!PRIVILEGE.test(privilege)) {
|
|
160
|
+
throw new OkmError2("invalid", `Privilege ${privilege} is not emitted.`, {
|
|
161
|
+
fix: {
|
|
162
|
+
summary: "Grants in this version are select, insert, update, delete, execute, and usage."
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/tooling/migrate/extensions.ts
|
|
169
|
+
var CREATE_EXTENSION = /^\s*create\s+extension\s+(?:"([^"]+)"|([A-Za-z_][\w$]*))/i;
|
|
170
|
+
var UPDATE_EXTENSION = /^\s*alter\s+extension\s+(?:"([^"]+)"|([A-Za-z_][\w$]*))\s+update\s+to\s+'([^']*)'/i;
|
|
171
|
+
function extensionAlterSteps(beforeBy, afterBy, dropKeys) {
|
|
172
|
+
refuseDrops(beforeBy, afterBy, dropKeys);
|
|
173
|
+
const steps = [];
|
|
174
|
+
for (const after of afterBy.values()) {
|
|
175
|
+
if (after.kind !== "extension")
|
|
176
|
+
continue;
|
|
177
|
+
const before = beforeBy.get(identityKey2(after.identity));
|
|
178
|
+
if (before === undefined || before.kind !== "extension")
|
|
179
|
+
continue;
|
|
180
|
+
if (sameExtensionDefinition(before, after))
|
|
181
|
+
continue;
|
|
182
|
+
steps.push(...alterExtension(before, after));
|
|
183
|
+
}
|
|
184
|
+
return steps;
|
|
185
|
+
}
|
|
186
|
+
async function assertExtensionsAvailable(connection, migrations) {
|
|
187
|
+
const creates = new Set;
|
|
188
|
+
const updates = [];
|
|
189
|
+
for (const migration of migrations) {
|
|
190
|
+
for (const step of migration.steps) {
|
|
191
|
+
const created = CREATE_EXTENSION.exec(step.sql);
|
|
192
|
+
const name = created?.[1] ?? created?.[2];
|
|
193
|
+
if (name !== undefined)
|
|
194
|
+
creates.add(name.replaceAll('""', '"'));
|
|
195
|
+
const updated = UPDATE_EXTENSION.exec(step.sql);
|
|
196
|
+
const updateName = updated?.[1] ?? updated?.[2];
|
|
197
|
+
const version = updated?.[3];
|
|
198
|
+
if (updateName !== undefined && version !== undefined) {
|
|
199
|
+
updates.push({ name: updateName.replaceAll('""', '"'), version });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (creates.size === 0 && updates.length === 0)
|
|
204
|
+
return;
|
|
205
|
+
const names = [...creates, ...updates.map((item) => item.name)];
|
|
206
|
+
const available = await availableExtensions(connection, names);
|
|
207
|
+
for (const name of creates) {
|
|
208
|
+
if (!available.has(name)) {
|
|
209
|
+
throw new OkmError2("OKM1811", `Extension ${name} is not available on the server. Nothing was changed.`, { fix: { summary: "Install the extension, or remove the declaration." } });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const update of updates) {
|
|
213
|
+
if (!available.has(update.name)) {
|
|
214
|
+
throw new OkmError2("OKM1811", `Extension ${update.name} is not available on the server. Nothing was changed.`, { fix: { summary: "Install the extension, or remove the declaration." } });
|
|
215
|
+
}
|
|
216
|
+
const installed = await installedVersion(connection, update.name);
|
|
217
|
+
if (installed === undefined) {
|
|
218
|
+
throw new OkmError2("OKM1811", `Extension ${update.name} is not installed, so it cannot be updated. Nothing was changed.`, { fix: { summary: "Create the extension before updating it." } });
|
|
219
|
+
}
|
|
220
|
+
const path = await updatePath(connection, update.name, installed, update.version);
|
|
221
|
+
if (!path) {
|
|
222
|
+
throw new OkmError2("OKM1814", `Extension ${update.name} has no update path from ${installed} to ${update.version}. Nothing was changed.`, {
|
|
223
|
+
fix: {
|
|
224
|
+
summary: "Drop or move the dependents first. Postgres cannot lower an extension version."
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function refuseDrops(beforeBy, afterBy, dropKeys) {
|
|
231
|
+
for (const key of dropKeys) {
|
|
232
|
+
const object = beforeBy.get(key);
|
|
233
|
+
if (object?.kind !== "extension")
|
|
234
|
+
continue;
|
|
235
|
+
for (const after of afterBy.values()) {
|
|
236
|
+
const used = after.dependencies.some((edge) => edge.target.kind === "extension" && edge.target.name === object.identity.name);
|
|
237
|
+
if (!used)
|
|
238
|
+
continue;
|
|
239
|
+
throw new OkmError2("OKM1814", `Extension ${object.identity.name} still has dependents. Nothing was dropped.`, {
|
|
240
|
+
fix: {
|
|
241
|
+
summary: "Drop or move the dependents first. Postgres cannot lower an extension version."
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function alterExtension(before, after) {
|
|
248
|
+
const steps = [];
|
|
249
|
+
const name = quoteIdent(after.identity.name);
|
|
250
|
+
if (before.definition.schema !== after.definition.schema) {
|
|
251
|
+
if (!after.definition.relocatable) {
|
|
252
|
+
throw new OkmError2("OKM1814", `Extension ${after.identity.name} is not relocatable, so it cannot move to ${after.definition.schema}.`, {
|
|
253
|
+
fix: {
|
|
254
|
+
summary: "Drop or move the dependents first. Postgres cannot lower an extension version."
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
steps.push({
|
|
259
|
+
sql: `alter extension ${name} set schema ${quoteIdent(after.definition.schema)}`,
|
|
260
|
+
class: "contract",
|
|
261
|
+
action: "ddl",
|
|
262
|
+
lock: "ACCESS EXCLUSIVE",
|
|
263
|
+
transactional: true
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const previous = before.definition.version;
|
|
267
|
+
const next = after.definition.version;
|
|
268
|
+
if (!isExactVersion(previous) || !isExactVersion(next) || previous === next)
|
|
269
|
+
return steps;
|
|
270
|
+
if (compareExtensionVersions(next, previous) < 0) {
|
|
271
|
+
throw new OkmError2("OKM1814", `Extension ${after.identity.name} cannot move from ${previous} to ${next}. Postgres cannot lower an extension version.`, {
|
|
272
|
+
fix: {
|
|
273
|
+
summary: "Drop or move the dependents first. Postgres cannot lower an extension version."
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
steps.push({
|
|
278
|
+
sql: `alter extension ${name} update to ${quoteLiteral(next)}`,
|
|
279
|
+
class: "expand",
|
|
280
|
+
action: "ddl",
|
|
281
|
+
lock: "ACCESS EXCLUSIVE",
|
|
282
|
+
transactional: true,
|
|
283
|
+
path: "unverified"
|
|
284
|
+
});
|
|
285
|
+
return steps;
|
|
286
|
+
}
|
|
287
|
+
async function availableExtensions(connection, names) {
|
|
288
|
+
const listed = await connection.execute(`select name from pg_available_extensions where name in (${names.map((_, index) => `$${String(index + 1)}`).join(", ")})`, names);
|
|
289
|
+
const found = new Set;
|
|
290
|
+
for (const row of listed.rows) {
|
|
291
|
+
const name = row[0];
|
|
292
|
+
if (typeof name === "string")
|
|
293
|
+
found.add(name);
|
|
294
|
+
}
|
|
295
|
+
return found;
|
|
296
|
+
}
|
|
297
|
+
async function installedVersion(connection, name) {
|
|
298
|
+
const result = await connection.execute("select extversion from pg_extension where extname = $1", [name]);
|
|
299
|
+
const version = result.rows[0]?.[0];
|
|
300
|
+
return typeof version === "string" ? version : undefined;
|
|
301
|
+
}
|
|
302
|
+
async function updatePath(connection, name, source, target) {
|
|
303
|
+
const result = await connection.execute("select path from pg_extension_update_paths($1) where source = $2 and target = $3", [name, source, target]);
|
|
304
|
+
const path = result.rows[0]?.[0];
|
|
305
|
+
return typeof path === "string" && path.length > 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/tooling/migrate/managed.ts
|
|
309
|
+
var MANAGED_TABLES = new Set(["okm_meta", "okm_history"]);
|
|
310
|
+
function omitManagedObjects(source) {
|
|
311
|
+
let dropped = false;
|
|
312
|
+
const objects = [];
|
|
313
|
+
for (const object of source.objects) {
|
|
314
|
+
if (ownedByMigration(object)) {
|
|
315
|
+
dropped = true;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
objects.push(object);
|
|
319
|
+
}
|
|
320
|
+
if (!dropped)
|
|
321
|
+
return source;
|
|
322
|
+
return { version: source.version, objects };
|
|
323
|
+
}
|
|
324
|
+
function ownedByMigration(object) {
|
|
325
|
+
switch (object.kind) {
|
|
326
|
+
case "table":
|
|
327
|
+
case "view":
|
|
328
|
+
case "materializedView":
|
|
329
|
+
return MANAGED_TABLES.has(object.identity.name);
|
|
330
|
+
case "column":
|
|
331
|
+
case "index":
|
|
332
|
+
case "constraint":
|
|
333
|
+
case "trigger":
|
|
334
|
+
return MANAGED_TABLES.has(object.identity.parent.name);
|
|
335
|
+
case "sequence":
|
|
336
|
+
return managedSequence(object.identity.name);
|
|
337
|
+
case "grant": {
|
|
338
|
+
const target = object.identity.object;
|
|
339
|
+
if (target.kind === "namespace")
|
|
340
|
+
return false;
|
|
341
|
+
if (target.kind === "sequence" || target.kind === "function") {
|
|
342
|
+
return managedSequence(target.name);
|
|
343
|
+
}
|
|
344
|
+
return MANAGED_TABLES.has(target.name);
|
|
345
|
+
}
|
|
346
|
+
default:
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function managedSequence(name) {
|
|
351
|
+
return name === "okm_meta" || name === "okm_history" || name.startsWith("okm_meta_") || name.startsWith("okm_history_");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/tooling/migrate/type-name.ts
|
|
355
|
+
function canonicalTypeName(typeName) {
|
|
356
|
+
const suffix = arraySuffix(typeName);
|
|
357
|
+
const scalar = suffix.length === 0 ? typeName : typeName.slice(0, -suffix.length);
|
|
358
|
+
return canonicalScalar(scalar) + suffix;
|
|
359
|
+
}
|
|
360
|
+
function arraySuffix(typeName) {
|
|
361
|
+
let end = typeName.length;
|
|
362
|
+
let suffix = "";
|
|
363
|
+
while (end >= 2 && typeName.endsWith("[]", end)) {
|
|
364
|
+
suffix = `[]${suffix}`;
|
|
365
|
+
end -= 2;
|
|
366
|
+
}
|
|
367
|
+
return suffix;
|
|
368
|
+
}
|
|
369
|
+
function canonicalScalar(typeName) {
|
|
370
|
+
const varying = /^(?:varchar|character varying)(\(\d+\))$/.exec(typeName);
|
|
371
|
+
if (varying !== null)
|
|
372
|
+
return `character varying${varying[1] ?? ""}`;
|
|
373
|
+
const fixed = /^(?:char|character)(\(\d+\))$/.exec(typeName);
|
|
374
|
+
if (fixed !== null)
|
|
375
|
+
return `character${fixed[1] ?? ""}`;
|
|
376
|
+
const timed = /^(timestamp with time zone|timestamp without time zone|time with time zone|time without time zone|timestamptz|timestamp|timetz|time)(\(\d+\))?$/.exec(typeName);
|
|
377
|
+
if (timed !== null)
|
|
378
|
+
return canonicalTime(timed[1] ?? "", timed[2] ?? "");
|
|
379
|
+
const numeric = /^(?:decimal|numeric)(\(\d+(?:,\d+)?\))?$/.exec(typeName);
|
|
380
|
+
if (numeric !== null)
|
|
381
|
+
return `numeric${numeric[1] ?? ""}`;
|
|
382
|
+
switch (typeName) {
|
|
383
|
+
case "int":
|
|
384
|
+
case "int4":
|
|
385
|
+
case "integer":
|
|
386
|
+
return "integer";
|
|
387
|
+
case "int8":
|
|
388
|
+
case "bigint":
|
|
389
|
+
return "bigint";
|
|
390
|
+
case "int2":
|
|
391
|
+
case "smallint":
|
|
392
|
+
return "smallint";
|
|
393
|
+
case "bool":
|
|
394
|
+
case "boolean":
|
|
395
|
+
return "boolean";
|
|
396
|
+
case "float8":
|
|
397
|
+
case "double precision":
|
|
398
|
+
return "double precision";
|
|
399
|
+
case "float4":
|
|
400
|
+
case "real":
|
|
401
|
+
return "real";
|
|
402
|
+
default:
|
|
403
|
+
return typeName;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function canonicalTime(name, precision) {
|
|
407
|
+
if (name === "timestamptz" || name === "timestamp with time zone") {
|
|
408
|
+
return `timestamp${precision} with time zone`;
|
|
409
|
+
}
|
|
410
|
+
if (name === "timestamp" || name === "timestamp without time zone") {
|
|
411
|
+
return `timestamp${precision} without time zone`;
|
|
412
|
+
}
|
|
413
|
+
if (name === "timetz" || name === "time with time zone") {
|
|
414
|
+
return `time${precision} with time zone`;
|
|
415
|
+
}
|
|
416
|
+
return `time${precision} without time zone`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/tooling/migrate/values.ts
|
|
420
|
+
function parseReplace(flag) {
|
|
421
|
+
const eq = flag.indexOf("=");
|
|
422
|
+
if (eq <= 0) {
|
|
423
|
+
throw new OkmError2("OKM1541", `--replace ${flag} must be table.column.old=new.`, {
|
|
424
|
+
fix: {
|
|
425
|
+
summary: "Pass --replace <table>.<column>.<old>=<new>, or =null on a nullable column."
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
const left = flag.slice(0, eq);
|
|
430
|
+
const raw = flag.slice(eq + 1);
|
|
431
|
+
const first = left.indexOf(".");
|
|
432
|
+
const second = first < 0 ? -1 : left.indexOf(".", first + 1);
|
|
433
|
+
if (first <= 0 || second <= first + 1 || second === left.length - 1) {
|
|
434
|
+
throw new OkmError2("OKM1541", `--replace ${flag} must be table.column.old=new.`, {
|
|
435
|
+
fix: {
|
|
436
|
+
summary: "Pass --replace <table>.<column>.<old>=<new>, or =null on a nullable column."
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const table = left.slice(0, first);
|
|
441
|
+
const column = left.slice(first + 1, second);
|
|
442
|
+
const from = left.slice(second + 1);
|
|
443
|
+
return { table, column, from, to: raw === "null" ? null : raw };
|
|
444
|
+
}
|
|
445
|
+
function assertNoChains(replacements) {
|
|
446
|
+
const removed = new Set(replacements.map((item) => pair(item.table, item.column, item.from)));
|
|
447
|
+
for (const item of replacements) {
|
|
448
|
+
if (item.to !== null && removed.has(pair(item.table, item.column, item.to))) {
|
|
449
|
+
const flag = `--replace ${item.table}.${item.column}.${item.from}=${item.to}`;
|
|
450
|
+
throw new OkmError2("OKM1541", `${flag} chains through another removed value.`, {
|
|
451
|
+
fix: { summary: "The replacement must be in the new list. No chains." }
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function pair(table, column, value) {
|
|
457
|
+
return `${table}\x00${column}\x00${value}`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/tooling/migrate/plan.ts
|
|
461
|
+
var ACCESS = "ACCESS EXCLUSIVE";
|
|
462
|
+
var SHARE = "SHARE";
|
|
463
|
+
var SHARE_UPDATE = "SHARE UPDATE EXCLUSIVE";
|
|
464
|
+
var ROW = "ROW EXCLUSIVE";
|
|
465
|
+
function planMigration(input) {
|
|
466
|
+
const request = {
|
|
467
|
+
...input,
|
|
468
|
+
before: omitManagedObjects(input.before),
|
|
469
|
+
after: omitManagedObjects(input.after)
|
|
470
|
+
};
|
|
471
|
+
const schema = request.schema ?? "public";
|
|
472
|
+
const renames = request.renames ?? [];
|
|
473
|
+
const replacements = request.replacements ?? [];
|
|
474
|
+
assertNoChains(replacements);
|
|
475
|
+
const renamed = applyRenames(request.before, renames);
|
|
476
|
+
const beforeList = index2(renamed);
|
|
477
|
+
const afterList = index2(request.after);
|
|
478
|
+
const beforeBy = new Map(beforeList.map((item) => [item.key, item.object]));
|
|
479
|
+
const afterBy = new Map(afterList.map((item) => [item.key, item.object]));
|
|
480
|
+
assertUnambiguous(beforeBy, afterBy);
|
|
481
|
+
const dropKeys = new Set;
|
|
482
|
+
const createKeys = new Set;
|
|
483
|
+
const replaces = [];
|
|
484
|
+
const alters = [];
|
|
485
|
+
for (const item of beforeList) {
|
|
486
|
+
if (!afterBy.has(item.key)) {
|
|
487
|
+
if (item.object.kind === "role")
|
|
488
|
+
continue;
|
|
489
|
+
dropKeys.add(item.key);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (const item of afterList) {
|
|
493
|
+
if (!beforeBy.has(item.key))
|
|
494
|
+
createKeys.add(item.key);
|
|
495
|
+
}
|
|
496
|
+
for (const item of afterList) {
|
|
497
|
+
const previous = beforeBy.get(item.key);
|
|
498
|
+
if (previous === undefined || sameDefinition(previous, item.object))
|
|
499
|
+
continue;
|
|
500
|
+
if (previous.kind === "extension" && item.object.kind === "extension")
|
|
501
|
+
continue;
|
|
502
|
+
if (previous.kind === "role" && item.object.kind === "role")
|
|
503
|
+
continue;
|
|
504
|
+
if (previous.kind === "function" && item.object.kind === "function" && canonicalTypeName(previous.definition.returns) === canonicalTypeName(item.object.definition.returns)) {
|
|
505
|
+
replaces.push({ kind: "function", before: previous, after: item.object });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (previous.kind === "view" && item.object.kind === "view") {
|
|
509
|
+
if (columnsAppended(previous.definition.columns, item.object.definition.columns)) {
|
|
510
|
+
replaces.push({ kind: "view", before: previous, after: item.object });
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (previous.kind === "type" && item.object.kind === "type") {
|
|
515
|
+
refuseDomainBaseChange(previous, item.object);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (previous.kind === "column" && item.object.kind === "column" && columnAlterable(previous, item.object)) {
|
|
519
|
+
alters.push({ before: previous, after: item.object });
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
dropKeys.add(item.key);
|
|
523
|
+
createKeys.add(item.key);
|
|
524
|
+
}
|
|
525
|
+
const keptNames = pairSameShape(beforeBy, afterBy, dropKeys, createKeys);
|
|
526
|
+
const picklists = picklistChanges(beforeBy, afterBy, request.after);
|
|
527
|
+
const enums = enumEdits(beforeBy, afterBy, schema);
|
|
528
|
+
requireReplacements(picklists, enums, replacements);
|
|
529
|
+
recreateDependents(beforeList, afterList, alters, dropKeys, createKeys, picklists);
|
|
530
|
+
recreateRoutineDependents(afterList, dropKeys, createKeys);
|
|
531
|
+
recreateViewDependents(afterList, dropKeys, createKeys);
|
|
532
|
+
refuseRoutineDrops(beforeBy, afterBy, dropKeys);
|
|
533
|
+
omitOwnedSequences(createKeys, afterBy, request.after);
|
|
534
|
+
omitOwnedSequences(dropKeys, beforeBy, renamed);
|
|
535
|
+
const privileges = privilegeSql(renamed.objects, request.after.objects, schema);
|
|
536
|
+
const steps = [];
|
|
537
|
+
steps.push(...renameSteps(request.before, renames, schema));
|
|
538
|
+
for (const sql of privileges.revoke)
|
|
539
|
+
steps.push(step(sql, "contract", "ddl", ACCESS));
|
|
540
|
+
steps.push(...keptNames.map((item) => renameShapeStep(item, schema)));
|
|
541
|
+
steps.push(...enumAddSteps(enums));
|
|
542
|
+
steps.push(...domainCheckSteps(beforeBy, afterBy, schema));
|
|
543
|
+
steps.push(...expandBackfills(picklists, enums, replacements, schema));
|
|
544
|
+
const droppedTables = new Set([...dropKeys].map((key) => beforeBy.get(key)).filter((object) => object?.kind === "table").map((object) => object.identity.name));
|
|
545
|
+
const deferred = new Set(picklists.map((change) => identityKey2(change.before.identity)));
|
|
546
|
+
const typeDrops = [];
|
|
547
|
+
const extensionDrops = [];
|
|
548
|
+
const reverse = [...creationOrder2(renamed)].reverse();
|
|
549
|
+
const emitDrop = (object) => {
|
|
550
|
+
const key = identityKey2(object.identity);
|
|
551
|
+
if (!dropKeys.has(key) || deferred.has(key) || covered(object, droppedTables))
|
|
552
|
+
return;
|
|
553
|
+
const sql = dropObjectSql(object, schema);
|
|
554
|
+
if (sql === undefined)
|
|
555
|
+
return;
|
|
556
|
+
steps.push(step(sql, "contract", "ddl", ACCESS));
|
|
557
|
+
};
|
|
558
|
+
for (const object of reverse)
|
|
559
|
+
if (object.kind === "trigger")
|
|
560
|
+
emitDrop(object);
|
|
561
|
+
for (const object of reverse)
|
|
562
|
+
if (object.kind === "function")
|
|
563
|
+
emitDrop(object);
|
|
564
|
+
for (const object of reverse)
|
|
565
|
+
if (ownedByView(object))
|
|
566
|
+
emitDrop(object);
|
|
567
|
+
for (const object of reverse) {
|
|
568
|
+
if (object.kind === "view" || object.kind === "materializedView")
|
|
569
|
+
emitDrop(object);
|
|
570
|
+
}
|
|
571
|
+
for (const object of reverse) {
|
|
572
|
+
if (object.kind === "trigger" || object.kind === "function" || object.kind === "view" || object.kind === "materializedView" || ownedByView(object)) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const key = identityKey2(object.identity);
|
|
576
|
+
if (!dropKeys.has(key) || deferred.has(key) || covered(object, droppedTables))
|
|
577
|
+
continue;
|
|
578
|
+
if (object.kind === "type") {
|
|
579
|
+
typeDrops.push(object);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (object.kind === "extension") {
|
|
583
|
+
extensionDrops.push(object);
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
emitDrop(object);
|
|
587
|
+
}
|
|
588
|
+
for (const object of typeDrops) {
|
|
589
|
+
const sql = dropObjectSql(object, schema);
|
|
590
|
+
if (sql === undefined)
|
|
591
|
+
continue;
|
|
592
|
+
steps.push(step(sql, "contract", "ddl", ACCESS));
|
|
593
|
+
}
|
|
594
|
+
for (const object of extensionDrops) {
|
|
595
|
+
const sql = dropObjectSql(object, schema);
|
|
596
|
+
if (sql === undefined)
|
|
597
|
+
continue;
|
|
598
|
+
steps.push(step(sql, "contract", "ddl", ACCESS));
|
|
599
|
+
}
|
|
600
|
+
steps.push(...extensionAlterSteps(beforeBy, afterBy, dropKeys));
|
|
601
|
+
for (const change of alters) {
|
|
602
|
+
for (const sql of alterColumnSql(alignedColumnType(change.before, change.after), change.after, schema)) {
|
|
603
|
+
steps.push(step(sql, alterClass(sql), "ddl", ACCESS));
|
|
604
|
+
}
|
|
605
|
+
const identitySql = identityChangeSql(change.before, change.after, schema);
|
|
606
|
+
if (identitySql !== undefined)
|
|
607
|
+
steps.push(step(identitySql, "expand", "ddl", ACCESS));
|
|
608
|
+
}
|
|
609
|
+
const createdTables = new Set([...createKeys].map((key) => afterBy.get(key)).filter((object) => object?.kind === "table").map((object) => object.identity.name));
|
|
610
|
+
const emitted = new Set;
|
|
611
|
+
for (const sql of privileges.prepare)
|
|
612
|
+
steps.push(step(sql, "expand", "ddl", ACCESS));
|
|
613
|
+
for (const object of creationOrder2(request.after)) {
|
|
614
|
+
if (object.kind !== "extension")
|
|
615
|
+
continue;
|
|
616
|
+
const key = identityKey2(object.identity);
|
|
617
|
+
if (!createKeys.has(key) || emitted.has(key))
|
|
618
|
+
continue;
|
|
619
|
+
emitted.add(key);
|
|
620
|
+
const sql = createObjectSql(object, schema);
|
|
621
|
+
if (sql === undefined)
|
|
622
|
+
continue;
|
|
623
|
+
steps.push(step(sql, "expand", "ddl", ACCESS));
|
|
624
|
+
}
|
|
625
|
+
for (const object of creationOrder2(request.after)) {
|
|
626
|
+
if (object.kind !== "type")
|
|
627
|
+
continue;
|
|
628
|
+
const key = identityKey2(object.identity);
|
|
629
|
+
if (!createKeys.has(key) || emitted.has(key))
|
|
630
|
+
continue;
|
|
631
|
+
emitted.add(key);
|
|
632
|
+
const sql = createObjectSql(object, schema);
|
|
633
|
+
if (sql === undefined)
|
|
634
|
+
continue;
|
|
635
|
+
steps.push(step(sql, "expand", "ddl", ACCESS));
|
|
636
|
+
}
|
|
637
|
+
for (const object of creationOrder2(request.after)) {
|
|
638
|
+
if (object.kind === "type" || object.kind === "extension" || object.kind === "function" || object.kind === "trigger" || object.kind === "view" || object.kind === "materializedView" || ownedByView(object)) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
const key = identityKey2(object.identity);
|
|
642
|
+
if (deferred.has(key))
|
|
643
|
+
continue;
|
|
644
|
+
const parent = anchoredParent(object);
|
|
645
|
+
const tableIsNew = parent !== undefined && createdTables.has(parent);
|
|
646
|
+
if (tableIsNew && folded(object, request.after))
|
|
647
|
+
continue;
|
|
648
|
+
const wanted = createKeys.has(key) || tableIsNew && !folded(object, request.after);
|
|
649
|
+
if (!wanted || emitted.has(key))
|
|
650
|
+
continue;
|
|
651
|
+
emitted.add(key);
|
|
652
|
+
if (object.kind === "table") {
|
|
653
|
+
steps.push(step(createTableSql(object, request.after, schema), "expand", "ddl", ACCESS));
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const sql = createObjectSql(object, schema);
|
|
657
|
+
if (sql === undefined)
|
|
658
|
+
continue;
|
|
659
|
+
steps.push(step(sql, "expand", "ddl", object.kind === "index" ? SHARE : ACCESS));
|
|
660
|
+
}
|
|
661
|
+
for (const change of replaces) {
|
|
662
|
+
if (sameDefinition(change.before, change.after))
|
|
663
|
+
continue;
|
|
664
|
+
const key = identityKey2(change.after.identity);
|
|
665
|
+
if (dropKeys.has(key) && createKeys.has(key))
|
|
666
|
+
continue;
|
|
667
|
+
const sql = change.kind === "function" ? functionSql(change.after, schema, true) : viewSql(change.after, schema, true);
|
|
668
|
+
steps.push({
|
|
669
|
+
sql,
|
|
670
|
+
class: "expand",
|
|
671
|
+
action: "ddl",
|
|
672
|
+
lock: ACCESS,
|
|
673
|
+
transactional: true,
|
|
674
|
+
behavior: "change"
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
for (const kind of ["function", "trigger", "view", "materializedView"]) {
|
|
678
|
+
for (const object of creationOrder2(request.after)) {
|
|
679
|
+
if (object.kind !== kind)
|
|
680
|
+
continue;
|
|
681
|
+
const key = identityKey2(object.identity);
|
|
682
|
+
if (!createKeys.has(key) || emitted.has(key))
|
|
683
|
+
continue;
|
|
684
|
+
emitted.add(key);
|
|
685
|
+
const sql = createObjectSql(object, schema);
|
|
686
|
+
if (sql === undefined)
|
|
687
|
+
continue;
|
|
688
|
+
steps.push(step(sql, "expand", "ddl", ACCESS));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (const object of creationOrder2(request.after)) {
|
|
692
|
+
if (!ownedByView(object))
|
|
693
|
+
continue;
|
|
694
|
+
const key = identityKey2(object.identity);
|
|
695
|
+
if (!createKeys.has(key) || emitted.has(key))
|
|
696
|
+
continue;
|
|
697
|
+
emitted.add(key);
|
|
698
|
+
const sql = createObjectSql(object, schema);
|
|
699
|
+
if (sql === undefined)
|
|
700
|
+
continue;
|
|
701
|
+
steps.push(step(sql, "expand", "ddl", SHARE));
|
|
702
|
+
}
|
|
703
|
+
for (const object of creationOrder2(request.after)) {
|
|
704
|
+
if (object.kind !== "materializedView")
|
|
705
|
+
continue;
|
|
706
|
+
const key = identityKey2(object.identity);
|
|
707
|
+
if (!createKeys.has(key))
|
|
708
|
+
continue;
|
|
709
|
+
assertConcurrentRefresh(object, request.after);
|
|
710
|
+
steps.push(step(refreshMaterializedViewSql(object, schema, false), "expand", "backfill", ACCESS));
|
|
711
|
+
}
|
|
712
|
+
for (const sql of privileges.grant)
|
|
713
|
+
steps.push(step(sql, "expand", "ddl", ACCESS));
|
|
714
|
+
steps.push(...contractSwaps(picklists, enums, replacements, schema));
|
|
715
|
+
return {
|
|
716
|
+
name: request.name ?? "migration",
|
|
717
|
+
class: overall(steps),
|
|
718
|
+
steps
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function formatPlan(plan) {
|
|
722
|
+
const lines = [`-- class: ${plan.class}`, `-- name: ${plan.name}`, ""];
|
|
723
|
+
if (plan.steps.length === 0) {
|
|
724
|
+
lines.push("-- no steps");
|
|
725
|
+
lines.push("");
|
|
726
|
+
return lines.join(`
|
|
727
|
+
`);
|
|
728
|
+
}
|
|
729
|
+
for (const item of plan.steps) {
|
|
730
|
+
lines.push(`-- class: ${item.class}`);
|
|
731
|
+
lines.push(`-- action: ${item.action}`);
|
|
732
|
+
lines.push(`-- lock: ${item.lock}`);
|
|
733
|
+
if (!item.transactional)
|
|
734
|
+
lines.push("-- transactional: false");
|
|
735
|
+
if (item.path !== undefined)
|
|
736
|
+
lines.push(`-- path: ${item.path}`);
|
|
737
|
+
if (item.behavior === "change")
|
|
738
|
+
lines.push("-- behavior: change");
|
|
739
|
+
lines.push(`${item.sql};`);
|
|
740
|
+
lines.push("");
|
|
741
|
+
}
|
|
742
|
+
return lines.join(`
|
|
743
|
+
`);
|
|
744
|
+
}
|
|
745
|
+
function parsePlan(text) {
|
|
746
|
+
const lines = text.split(`
|
|
747
|
+
`);
|
|
748
|
+
let name = "migration";
|
|
749
|
+
let planClass = "expand";
|
|
750
|
+
let index = 0;
|
|
751
|
+
while (index < lines.length && lines[index] !== "") {
|
|
752
|
+
const line = lines[index] ?? "";
|
|
753
|
+
if (line.startsWith("-- name: "))
|
|
754
|
+
name = line.slice("-- name: ".length);
|
|
755
|
+
if (line.startsWith("-- class: "))
|
|
756
|
+
planClass = readClass(line.slice("-- class: ".length));
|
|
757
|
+
index += 1;
|
|
758
|
+
}
|
|
759
|
+
const steps = [];
|
|
760
|
+
while (index < lines.length) {
|
|
761
|
+
if ((lines[index] ?? "").trim() === "") {
|
|
762
|
+
index += 1;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (lines[index] === "-- no steps")
|
|
766
|
+
break;
|
|
767
|
+
let stepClass = "expand";
|
|
768
|
+
let action = "ddl";
|
|
769
|
+
let lock = "";
|
|
770
|
+
let transactional = true;
|
|
771
|
+
let path;
|
|
772
|
+
let behavior;
|
|
773
|
+
const sql = [];
|
|
774
|
+
while (index < lines.length && (lines[index] ?? "") !== "") {
|
|
775
|
+
const line = lines[index] ?? "";
|
|
776
|
+
index += 1;
|
|
777
|
+
if (line.startsWith("-- class: "))
|
|
778
|
+
stepClass = readClass(line.slice("-- class: ".length));
|
|
779
|
+
else if (line.startsWith("-- action: "))
|
|
780
|
+
action = line.endsWith("backfill") ? "backfill" : "ddl";
|
|
781
|
+
else if (line.startsWith("-- lock: "))
|
|
782
|
+
lock = line.slice("-- lock: ".length);
|
|
783
|
+
else if (line === "-- transactional: false")
|
|
784
|
+
transactional = false;
|
|
785
|
+
else if (line === "-- path: unverified")
|
|
786
|
+
path = "unverified";
|
|
787
|
+
else if (line === "-- behavior: change")
|
|
788
|
+
behavior = "change";
|
|
789
|
+
else if (!line.startsWith("--"))
|
|
790
|
+
sql.push(line);
|
|
791
|
+
}
|
|
792
|
+
const statement = sql.join(`
|
|
793
|
+
`).replace(/;\s*$/, "");
|
|
794
|
+
if (statement.length > 0) {
|
|
795
|
+
steps.push({
|
|
796
|
+
sql: statement,
|
|
797
|
+
class: stepClass,
|
|
798
|
+
action,
|
|
799
|
+
lock,
|
|
800
|
+
transactional,
|
|
801
|
+
...path !== undefined ? { path } : {},
|
|
802
|
+
...behavior !== undefined ? { behavior } : {}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return { name, class: steps.length === 0 ? planClass : overall(steps), steps };
|
|
807
|
+
}
|
|
808
|
+
function readClass(value) {
|
|
809
|
+
if (value === "contract" || value === "unclassified")
|
|
810
|
+
return value;
|
|
811
|
+
return "expand";
|
|
812
|
+
}
|
|
813
|
+
function staleRenames(previous, renames) {
|
|
814
|
+
if (previous === undefined)
|
|
815
|
+
return [];
|
|
816
|
+
const messages = [];
|
|
817
|
+
for (const rename of renames) {
|
|
818
|
+
if (rename.kind === "table") {
|
|
819
|
+
if (!hasTable(previous, rename.from)) {
|
|
820
|
+
messages.push(`Table renamedFrom ${rename.from} is not in the previous snapshot.`);
|
|
821
|
+
}
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const tableRename = renames.find((item) => item.kind === "table" && item.to === rename.table);
|
|
825
|
+
const previousTable = tableRename?.kind === "table" ? tableRename.from : rename.table;
|
|
826
|
+
if (!hasColumn(previous, previousTable, rename.from)) {
|
|
827
|
+
messages.push(`Column ${rename.table}.${rename.from} renamedFrom is not in the previous snapshot.`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return messages;
|
|
831
|
+
}
|
|
832
|
+
function applyRenames(source, renames) {
|
|
833
|
+
let current = source;
|
|
834
|
+
const namespace = staticNamespace2("public");
|
|
835
|
+
for (const rename of renames) {
|
|
836
|
+
if (rename.kind !== "table")
|
|
837
|
+
continue;
|
|
838
|
+
if (!hasTable(current, rename.from))
|
|
839
|
+
continue;
|
|
840
|
+
current = renameTable2(current, { namespace, from: rename.from, to: rename.to });
|
|
841
|
+
}
|
|
842
|
+
for (const rename of renames) {
|
|
843
|
+
if (rename.kind !== "column")
|
|
844
|
+
continue;
|
|
845
|
+
const parent = { namespace, name: rename.table };
|
|
846
|
+
if (!hasColumn(current, rename.table, rename.from))
|
|
847
|
+
continue;
|
|
848
|
+
current = renameColumn2(current, { parent, from: rename.from, to: rename.to });
|
|
849
|
+
}
|
|
850
|
+
return current;
|
|
851
|
+
}
|
|
852
|
+
function renameSteps(source, renames, schema) {
|
|
853
|
+
const steps = [];
|
|
854
|
+
const tables = new Map;
|
|
855
|
+
for (const rename of renames) {
|
|
856
|
+
if (rename.kind !== "table" || !hasTable(source, rename.from))
|
|
857
|
+
continue;
|
|
858
|
+
tables.set(rename.to, rename.from);
|
|
859
|
+
steps.push(step(`alter table ${qualify(schema, rename.from)} rename to ${quoteIdent(rename.to)}`, "contract", "ddl", ACCESS));
|
|
860
|
+
}
|
|
861
|
+
for (const rename of renames) {
|
|
862
|
+
if (rename.kind !== "column")
|
|
863
|
+
continue;
|
|
864
|
+
const previousTable = tables.get(rename.table) ?? rename.table;
|
|
865
|
+
if (!hasColumn(source, previousTable, rename.from))
|
|
866
|
+
continue;
|
|
867
|
+
steps.push(step(`alter table ${qualify(schema, rename.table)} rename column ${quoteIdent(rename.from)} to ${quoteIdent(rename.to)}`, "contract", "ddl", ACCESS));
|
|
868
|
+
}
|
|
869
|
+
return steps;
|
|
870
|
+
}
|
|
871
|
+
function pairSameShape(beforeBy, afterBy, dropKeys, createKeys) {
|
|
872
|
+
const dropped = [...dropKeys].map((key) => beforeBy.get(key)).filter((object) => object !== undefined);
|
|
873
|
+
const created = [...createKeys].map((key) => afterBy.get(key)).filter((object) => object !== undefined);
|
|
874
|
+
const pairs = [];
|
|
875
|
+
const used = new Set;
|
|
876
|
+
for (const left of dropped) {
|
|
877
|
+
if (left.kind !== "index" && left.kind !== "constraint")
|
|
878
|
+
continue;
|
|
879
|
+
const leftKey = identityKey2(left.identity);
|
|
880
|
+
const hits = created.filter((right) => {
|
|
881
|
+
const key = identityKey2(right.identity);
|
|
882
|
+
return key !== leftKey && !used.has(key) && sameShape(left, right);
|
|
883
|
+
});
|
|
884
|
+
const right = hits.length === 1 ? hits[0] : undefined;
|
|
885
|
+
if (right === undefined)
|
|
886
|
+
continue;
|
|
887
|
+
const rivals = dropped.filter((item) => sameShape(item, right));
|
|
888
|
+
if (rivals.length !== 1)
|
|
889
|
+
continue;
|
|
890
|
+
const rightKey = identityKey2(right.identity);
|
|
891
|
+
used.add(rightKey);
|
|
892
|
+
dropKeys.delete(identityKey2(left.identity));
|
|
893
|
+
createKeys.delete(rightKey);
|
|
894
|
+
if (anchoredName(left) !== anchoredName(right))
|
|
895
|
+
pairs.push({ from: left, to: right });
|
|
896
|
+
}
|
|
897
|
+
return pairs;
|
|
898
|
+
}
|
|
899
|
+
function sameShape(left, right) {
|
|
900
|
+
if (left.kind !== right.kind)
|
|
901
|
+
return false;
|
|
902
|
+
if (left.kind !== "index" && left.kind !== "constraint" || right.kind !== "index" && right.kind !== "constraint") {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
if (left.identity.parent.name !== right.identity.parent.name)
|
|
906
|
+
return false;
|
|
907
|
+
return stable(withoutNameKey(left.definition)) === stable(withoutNameKey(right.definition));
|
|
908
|
+
}
|
|
909
|
+
function withoutNameKey(value) {
|
|
910
|
+
const copy = { ...value };
|
|
911
|
+
delete copy.nameKey;
|
|
912
|
+
return copy;
|
|
913
|
+
}
|
|
914
|
+
function renameShapeStep(pair, schema) {
|
|
915
|
+
if (pair.from.kind === "index" && pair.to.kind === "index") {
|
|
916
|
+
return step(`alter index ${qualify(schema, pair.from.identity.name)} rename to ${quoteIdent(pair.to.identity.name)}`, "contract", "ddl", ACCESS);
|
|
917
|
+
}
|
|
918
|
+
if (pair.from.kind !== "constraint" || pair.to.kind !== "constraint") {
|
|
919
|
+
return step("select 1", "unclassified", "ddl", ACCESS);
|
|
920
|
+
}
|
|
921
|
+
return step(`alter table ${qualify(schema, pair.from.identity.parent.name)} rename constraint ${quoteIdent(pair.from.identity.name)} to ${quoteIdent(pair.to.identity.name)}`, "contract", "ddl", ACCESS);
|
|
922
|
+
}
|
|
923
|
+
function assertUnambiguous(beforeBy, afterBy) {
|
|
924
|
+
const dropped = new Map;
|
|
925
|
+
const added = new Map;
|
|
926
|
+
for (const [key, object] of beforeBy) {
|
|
927
|
+
if (afterBy.has(key))
|
|
928
|
+
continue;
|
|
929
|
+
const group = groupOf(object);
|
|
930
|
+
if (group === undefined)
|
|
931
|
+
continue;
|
|
932
|
+
const list = dropped.get(group) ?? [];
|
|
933
|
+
const name = anchoredName(object);
|
|
934
|
+
if (name === undefined)
|
|
935
|
+
continue;
|
|
936
|
+
list.push(name);
|
|
937
|
+
dropped.set(group, list);
|
|
938
|
+
}
|
|
939
|
+
for (const [key, object] of afterBy) {
|
|
940
|
+
if (beforeBy.has(key))
|
|
941
|
+
continue;
|
|
942
|
+
const group = groupOf(object);
|
|
943
|
+
if (group === undefined)
|
|
944
|
+
continue;
|
|
945
|
+
const list = added.get(group) ?? [];
|
|
946
|
+
const name = anchoredName(object);
|
|
947
|
+
if (name === undefined)
|
|
948
|
+
continue;
|
|
949
|
+
list.push(name);
|
|
950
|
+
added.set(group, list);
|
|
951
|
+
}
|
|
952
|
+
for (const [group, names] of added) {
|
|
953
|
+
const gone = dropped.get(group);
|
|
954
|
+
if (gone === undefined || gone.length === 0 || names.length === 0)
|
|
955
|
+
continue;
|
|
956
|
+
const from = gone[0] ?? "";
|
|
957
|
+
const to = names[0] ?? "";
|
|
958
|
+
const line = group.startsWith("table:") ? `Add renamedFrom: "${from}" on ${to}.` : `Add .renamedFrom("${from}") on ${to}.`;
|
|
959
|
+
throw new OkmError2("OKM1530", `Drop ${from} and add ${to} could be a rename. ${line}`, {
|
|
960
|
+
fix: { summary: line }
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function anchoredName(object) {
|
|
965
|
+
switch (object.kind) {
|
|
966
|
+
case "grant":
|
|
967
|
+
case "defaultPrivilege":
|
|
968
|
+
return;
|
|
969
|
+
default:
|
|
970
|
+
return object.identity.name;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function groupOf(object) {
|
|
974
|
+
if (object.kind === "table")
|
|
975
|
+
return "table:public";
|
|
976
|
+
if (object.kind === "column")
|
|
977
|
+
return `column:${object.identity.parent.name}`;
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
function recreateDependents(beforeList, afterList, alters, dropKeys, createKeys, picklists) {
|
|
981
|
+
const changed = new Set(alters.filter((item) => canonicalTypeName(item.before.definition.dataType) !== canonicalTypeName(item.after.definition.dataType)).map((item) => identityKey2(item.after.identity)));
|
|
982
|
+
if (changed.size === 0)
|
|
983
|
+
return;
|
|
984
|
+
const deferred = new Set(picklists.map((change) => identityKey2(change.before.identity)));
|
|
985
|
+
const afterBy = new Map(afterList.map((item) => [item.key, item.object]));
|
|
986
|
+
for (const item of beforeList) {
|
|
987
|
+
const key = identityKey2(item.object.identity);
|
|
988
|
+
if (deferred.has(key) || !afterBy.has(key))
|
|
989
|
+
continue;
|
|
990
|
+
if (!dependsOnChanged(item.object, changed))
|
|
991
|
+
continue;
|
|
992
|
+
dropKeys.add(key);
|
|
993
|
+
createKeys.add(key);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
function recreateRoutineDependents(afterList, dropKeys, createKeys) {
|
|
997
|
+
const recreated = new Set;
|
|
998
|
+
for (const item of afterList) {
|
|
999
|
+
if (!dropKeys.has(item.key) || !createKeys.has(item.key))
|
|
1000
|
+
continue;
|
|
1001
|
+
if (item.object.kind === "function" || item.object.kind === "column" || item.object.kind === "table") {
|
|
1002
|
+
recreated.add(item.key);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (recreated.size === 0)
|
|
1006
|
+
return;
|
|
1007
|
+
for (const item of afterList) {
|
|
1008
|
+
if (item.object.kind !== "trigger" && item.object.kind !== "function")
|
|
1009
|
+
continue;
|
|
1010
|
+
const hit = item.object.dependencies.some((edge) => recreated.has(identityKey2(edge.target)));
|
|
1011
|
+
if (!hit)
|
|
1012
|
+
continue;
|
|
1013
|
+
dropKeys.add(item.key);
|
|
1014
|
+
createKeys.add(item.key);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function refuseRoutineDrops(beforeBy, afterBy, dropKeys) {
|
|
1018
|
+
for (const after of afterBy.values()) {
|
|
1019
|
+
const key = identityKey2(after.identity);
|
|
1020
|
+
if (dropKeys.has(key))
|
|
1021
|
+
continue;
|
|
1022
|
+
if (after.kind !== "trigger" && after.kind !== "function" && after.kind !== "view" && after.kind !== "materializedView") {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
for (const edge of after.dependencies) {
|
|
1026
|
+
const targetKey = identityKey2(edge.target);
|
|
1027
|
+
if (!dropKeys.has(targetKey))
|
|
1028
|
+
continue;
|
|
1029
|
+
const target = beforeBy.get(targetKey);
|
|
1030
|
+
if (target === undefined)
|
|
1031
|
+
continue;
|
|
1032
|
+
if (target.kind !== "function" && target.kind !== "table" && target.kind !== "column" && target.kind !== "view" && target.kind !== "materializedView") {
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
throw new OkmError2("OKM1821", `${identityLabel2(after.identity)} depends on ${identityLabel2(target.identity)}, which this plan drops. CASCADE is never used.`, { fix: { summary: "Review the dependents the plan lists and recreate them explicitly." } });
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function dependsOnChanged(object, columns) {
|
|
1040
|
+
if (object.kind !== "index" && object.kind !== "constraint" && object.kind !== "view" && object.kind !== "materializedView") {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
return object.dependencies.some((edge) => columns.has(identityKey2(edge.target)));
|
|
1044
|
+
}
|
|
1045
|
+
function recreateViewDependents(afterList, dropKeys, createKeys) {
|
|
1046
|
+
let changed = true;
|
|
1047
|
+
while (changed) {
|
|
1048
|
+
changed = false;
|
|
1049
|
+
for (const item of afterList) {
|
|
1050
|
+
if (dropKeys.has(item.key) && createKeys.has(item.key))
|
|
1051
|
+
continue;
|
|
1052
|
+
const kind = item.object.kind;
|
|
1053
|
+
if (kind !== "view" && kind !== "materializedView" && kind !== "index")
|
|
1054
|
+
continue;
|
|
1055
|
+
const hit = item.object.dependencies.some((edge) => {
|
|
1056
|
+
const key = identityKey2(edge.target);
|
|
1057
|
+
return dropKeys.has(key) && createKeys.has(key);
|
|
1058
|
+
});
|
|
1059
|
+
if (!hit)
|
|
1060
|
+
continue;
|
|
1061
|
+
dropKeys.add(item.key);
|
|
1062
|
+
createKeys.add(item.key);
|
|
1063
|
+
changed = true;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function columnsAppended(before, after) {
|
|
1068
|
+
if (after.length < before.length)
|
|
1069
|
+
return false;
|
|
1070
|
+
for (let index = 0;index < before.length; index += 1) {
|
|
1071
|
+
const left = before[index];
|
|
1072
|
+
const right = after[index];
|
|
1073
|
+
if (left === undefined || right === undefined)
|
|
1074
|
+
return false;
|
|
1075
|
+
if (left.name !== right.name || canonicalTypeName(left.dataType) !== canonicalTypeName(right.dataType)) {
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
function assertConcurrentRefresh(object, source) {
|
|
1082
|
+
if (object.definition.refresh !== "concurrently")
|
|
1083
|
+
return;
|
|
1084
|
+
const unique = source.objects.some((item) => item.kind === "index" && item.definition.unique && item.identity.parent.name === object.identity.name);
|
|
1085
|
+
if (unique)
|
|
1086
|
+
return;
|
|
1087
|
+
throw new OkmError2("OKM1822", `Materialized view ${object.identity.name} refreshes concurrently and has no unique index.`, { fix: { summary: "Add a unique index, or refresh without CONCURRENTLY." } });
|
|
1088
|
+
}
|
|
1089
|
+
function enumAddSteps(enums) {
|
|
1090
|
+
const steps = [];
|
|
1091
|
+
for (const change of enums) {
|
|
1092
|
+
if (change.kind !== "add")
|
|
1093
|
+
continue;
|
|
1094
|
+
for (const sql of change.statements) {
|
|
1095
|
+
steps.push(step(sql, "expand", "ddl", ACCESS, false));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return steps;
|
|
1099
|
+
}
|
|
1100
|
+
function expandBackfills(picklists, enums, replacements, schema) {
|
|
1101
|
+
return dataSteps(picklists, enums, replacements, schema, "expand");
|
|
1102
|
+
}
|
|
1103
|
+
function contractSwaps(picklists, enums, replacements, schema) {
|
|
1104
|
+
const steps = [];
|
|
1105
|
+
steps.push(...dataSteps(picklists, enums, replacements, schema, "contract"));
|
|
1106
|
+
for (const change of picklists) {
|
|
1107
|
+
const table = qualify(schema, change.after.identity.parent.name);
|
|
1108
|
+
const name = quoteIdent(change.after.identity.name);
|
|
1109
|
+
steps.push(step(`alter table ${table} drop constraint ${name}`, change.removed.length > 0 ? "contract" : "expand", "ddl", ACCESS));
|
|
1110
|
+
const expression = change.after.definition.expression ?? "true";
|
|
1111
|
+
steps.push(step(`alter table ${table} add constraint ${name} check (${expression}) not valid`, change.removed.length > 0 ? "contract" : "expand", "ddl", ACCESS));
|
|
1112
|
+
steps.push(step(`alter table ${table} validate constraint ${name}`, change.removed.length > 0 ? "contract" : "expand", "ddl", SHARE_UPDATE));
|
|
1113
|
+
}
|
|
1114
|
+
for (const change of enums) {
|
|
1115
|
+
if (change.kind !== "replace")
|
|
1116
|
+
continue;
|
|
1117
|
+
const type = qualify(schema, change.name);
|
|
1118
|
+
const old = quoteIdent(`${change.name}_old`);
|
|
1119
|
+
const labels = change.labels.map((label) => quoteLiteral(label)).join(", ");
|
|
1120
|
+
steps.push(step(`alter type ${type} rename to ${old}`, "contract", "ddl", ACCESS));
|
|
1121
|
+
steps.push(step(`create type ${type} as enum (${labels})`, "contract", "ddl", ACCESS));
|
|
1122
|
+
for (const column of change.columns) {
|
|
1123
|
+
const table = qualify(schema, column.table);
|
|
1124
|
+
const name = quoteIdent(column.column);
|
|
1125
|
+
steps.push(step(`alter table ${table} alter column ${name} type ${type} using ${name}::text::${type}`, "contract", "ddl", ACCESS));
|
|
1126
|
+
}
|
|
1127
|
+
steps.push(step(`drop type ${qualify(schema, `${change.name}_old`)}`, "contract", "ddl", ACCESS));
|
|
1128
|
+
}
|
|
1129
|
+
return steps;
|
|
1130
|
+
}
|
|
1131
|
+
function dataSteps(picklists, enums, replacements, schema, phase) {
|
|
1132
|
+
const steps = [];
|
|
1133
|
+
for (const change of picklists) {
|
|
1134
|
+
if (change.removed.length === 0)
|
|
1135
|
+
continue;
|
|
1136
|
+
const table = change.after.identity.parent.name;
|
|
1137
|
+
for (const value of change.removed) {
|
|
1138
|
+
const replacement = findReplacement(replacements, table, change.column, value);
|
|
1139
|
+
if (replacement === undefined)
|
|
1140
|
+
continue;
|
|
1141
|
+
steps.push(step(updateSql(schema, table, change.column, value, replacement.to), phase, "backfill", ROW));
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
for (const change of enums) {
|
|
1145
|
+
if (change.kind !== "replace" || change.removed.length === 0)
|
|
1146
|
+
continue;
|
|
1147
|
+
for (const column of change.columns) {
|
|
1148
|
+
if (!column.existed)
|
|
1149
|
+
continue;
|
|
1150
|
+
for (const value of change.removed) {
|
|
1151
|
+
const replacement = findReplacement(replacements, column.table, column.column, value);
|
|
1152
|
+
if (replacement === undefined)
|
|
1153
|
+
continue;
|
|
1154
|
+
steps.push(step(updateSql(schema, column.table, column.column, value, replacement.to, true), phase, "backfill", ROW));
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return steps;
|
|
1159
|
+
}
|
|
1160
|
+
function requireReplacements(picklists, enums, replacements) {
|
|
1161
|
+
const missing = [];
|
|
1162
|
+
for (const change of picklists) {
|
|
1163
|
+
for (const value of change.removed) {
|
|
1164
|
+
const table = change.after.identity.parent.name;
|
|
1165
|
+
const found = findReplacement(replacements, table, change.column, value);
|
|
1166
|
+
if (found === undefined) {
|
|
1167
|
+
missing.push(`--replace ${table}.${change.column}.${value}=<new>`);
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
assertReplacement(found, change.after.definition.expression, change.nullable, parseValues(change.after.definition.expression));
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
for (const change of enums) {
|
|
1174
|
+
if (change.kind !== "replace")
|
|
1175
|
+
continue;
|
|
1176
|
+
for (const column of change.columns) {
|
|
1177
|
+
if (!column.existed)
|
|
1178
|
+
continue;
|
|
1179
|
+
for (const value of change.removed) {
|
|
1180
|
+
const found = findReplacement(replacements, column.table, column.column, value);
|
|
1181
|
+
if (found === undefined) {
|
|
1182
|
+
missing.push(`--replace ${column.table}.${column.column}.${value}=<new>`);
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
assertReplacement(found, undefined, column.nullable, change.labels);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (missing.length > 0) {
|
|
1190
|
+
throw new OkmError2("OKM1541", `Removed values need a replacement: ${missing.join(", ")}.`, {
|
|
1191
|
+
fix: { summary: missing.join(" ") }
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
function assertReplacement(replacement, expression, nullable, allowed) {
|
|
1196
|
+
if (replacement.to === null) {
|
|
1197
|
+
if (!nullable) {
|
|
1198
|
+
throw new OkmError2("OKM1541", `--replace ${replacement.table}.${replacement.column}.${replacement.from}=null needs a nullable column.`, { fix: { summary: "Pass a value that is in the new list, or make the column nullable." } });
|
|
1199
|
+
}
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const list = allowed ?? (expression === undefined ? [] : parseValues(expression) ?? []);
|
|
1203
|
+
if (!list.includes(replacement.to)) {
|
|
1204
|
+
const flag = `--replace ${replacement.table}.${replacement.column}.${replacement.from}=${replacement.to}`;
|
|
1205
|
+
throw new OkmError2("OKM1541", `${flag} is not in the new list.`, {
|
|
1206
|
+
fix: { summary: "The replacement must be in the new list. No chains." }
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function parseValues(expression) {
|
|
1211
|
+
if (expression === undefined)
|
|
1212
|
+
return;
|
|
1213
|
+
return parseInList(expression)?.values;
|
|
1214
|
+
}
|
|
1215
|
+
function findReplacement(replacements, table, column, from) {
|
|
1216
|
+
return replacements.find((item) => item.table === table && item.column === column && item.from === from);
|
|
1217
|
+
}
|
|
1218
|
+
function picklistChanges(beforeBy, afterBy, after) {
|
|
1219
|
+
const changes = [];
|
|
1220
|
+
for (const [key, object] of beforeBy) {
|
|
1221
|
+
if (object.kind !== "constraint" || object.definition.constraintKind !== "check")
|
|
1222
|
+
continue;
|
|
1223
|
+
const next = afterBy.get(key);
|
|
1224
|
+
if (next === undefined || next.kind !== "constraint")
|
|
1225
|
+
continue;
|
|
1226
|
+
const left = parseInList(object.definition.expression ?? "");
|
|
1227
|
+
const right = parseInList(next.definition.expression ?? "");
|
|
1228
|
+
if (left === undefined || right === undefined || left.column !== right.column)
|
|
1229
|
+
continue;
|
|
1230
|
+
const removed = left.values.filter((value) => !right.values.includes(value));
|
|
1231
|
+
const added = right.values.filter((value) => !left.values.includes(value));
|
|
1232
|
+
if (removed.length === 0 && added.length === 0)
|
|
1233
|
+
continue;
|
|
1234
|
+
changes.push({
|
|
1235
|
+
before: object,
|
|
1236
|
+
after: next,
|
|
1237
|
+
column: right.column,
|
|
1238
|
+
removed,
|
|
1239
|
+
added,
|
|
1240
|
+
nullable: columnNullable(after, next.identity.parent.name, right.column)
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
return changes;
|
|
1244
|
+
}
|
|
1245
|
+
function refuseDomainBaseChange(before, after) {
|
|
1246
|
+
if (before.kind !== "type" || after.kind !== "type")
|
|
1247
|
+
return;
|
|
1248
|
+
if (!isDomain(before.definition) || !isDomain(after.definition))
|
|
1249
|
+
return;
|
|
1250
|
+
if (before.definition.base === after.definition.base)
|
|
1251
|
+
return;
|
|
1252
|
+
throw new OkmError2("OKM1020", `Domain ${after.identity.name} cannot change its base type from ${before.definition.base} to ${after.definition.base}. The base type of a domain stays as it was created.`, {
|
|
1253
|
+
fix: {
|
|
1254
|
+
summary: "Keep the base type. Add a new domain when the column needs another type."
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
function domainCheckSteps(beforeBy, afterBy, schema) {
|
|
1259
|
+
const steps = [];
|
|
1260
|
+
for (const [key, next] of afterBy) {
|
|
1261
|
+
if (next.kind !== "type" || !isDomain(next.definition))
|
|
1262
|
+
continue;
|
|
1263
|
+
const previous = beforeBy.get(key);
|
|
1264
|
+
if (previous === undefined || previous.kind !== "type" || !isDomain(previous.definition)) {
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (previous.definition.check === next.definition.check)
|
|
1268
|
+
continue;
|
|
1269
|
+
const name = next.identity.name;
|
|
1270
|
+
const type = qualify(schema, name);
|
|
1271
|
+
const current = quoteIdent(fitIdentifier2(`${name}_check`));
|
|
1272
|
+
const upcoming = quoteIdent(fitIdentifier2(`${name}_check_next`));
|
|
1273
|
+
const expression = next.definition.check;
|
|
1274
|
+
steps.push(step(`alter domain ${type} add constraint ${upcoming} check (${expression}) not valid`, "expand", "ddl", ACCESS));
|
|
1275
|
+
steps.push(step(`alter domain ${type} validate constraint ${upcoming}`, "expand", "ddl", ACCESS));
|
|
1276
|
+
steps.push(step(`alter domain ${type} drop constraint ${current}`, "contract", "ddl", ACCESS));
|
|
1277
|
+
steps.push(step(`alter domain ${type} rename constraint ${upcoming} to ${current}`, "expand", "ddl", ACCESS));
|
|
1278
|
+
}
|
|
1279
|
+
return steps;
|
|
1280
|
+
}
|
|
1281
|
+
function enumEdits(beforeBy, afterBy, schema) {
|
|
1282
|
+
const edits = [];
|
|
1283
|
+
for (const [key, next] of afterBy) {
|
|
1284
|
+
if (next.kind !== "type" || isDomain(next.definition))
|
|
1285
|
+
continue;
|
|
1286
|
+
const previous = beforeBy.get(key);
|
|
1287
|
+
if (previous === undefined || previous.kind !== "type" || isDomain(previous.definition)) {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
const beforeLabels = previous.definition.labels;
|
|
1291
|
+
const afterLabels = next.definition.labels;
|
|
1292
|
+
if (sameEnumLabels(beforeLabels, afterLabels))
|
|
1293
|
+
continue;
|
|
1294
|
+
const removed = beforeLabels.filter((label) => !afterLabels.includes(label));
|
|
1295
|
+
const columns = enumColumns(previous.identity.name, beforeBy, afterBy);
|
|
1296
|
+
if (removed.length === 0 && labelSubsequence(beforeLabels, afterLabels)) {
|
|
1297
|
+
edits.push({
|
|
1298
|
+
kind: "add",
|
|
1299
|
+
name: next.identity.name,
|
|
1300
|
+
statements: addValueStatements(schema, next.identity.name, beforeLabels, afterLabels)
|
|
1301
|
+
});
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
edits.push({
|
|
1305
|
+
kind: "replace",
|
|
1306
|
+
name: next.identity.name,
|
|
1307
|
+
labels: afterLabels,
|
|
1308
|
+
removed,
|
|
1309
|
+
columns
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
return edits;
|
|
1313
|
+
}
|
|
1314
|
+
function enumColumns(typeName, beforeBy, afterBy) {
|
|
1315
|
+
const columns = [];
|
|
1316
|
+
for (const object of afterBy.values()) {
|
|
1317
|
+
if (object.kind !== "column")
|
|
1318
|
+
continue;
|
|
1319
|
+
if (!usesType(object, typeName))
|
|
1320
|
+
continue;
|
|
1321
|
+
columns.push({
|
|
1322
|
+
table: object.identity.parent.name,
|
|
1323
|
+
column: object.identity.name,
|
|
1324
|
+
nullable: object.definition.nullable,
|
|
1325
|
+
existed: beforeBy.has(identityKey2(object.identity))
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
columns.sort((left, right) => {
|
|
1329
|
+
const table = compareText(left.table, right.table);
|
|
1330
|
+
return table === 0 ? compareText(left.column, right.column) : table;
|
|
1331
|
+
});
|
|
1332
|
+
return columns;
|
|
1333
|
+
}
|
|
1334
|
+
function usesType(object, typeName) {
|
|
1335
|
+
return object.dependencies.some((edge) => edge.target.kind === "type" && edge.target.name === typeName);
|
|
1336
|
+
}
|
|
1337
|
+
function labelSubsequence(before, after) {
|
|
1338
|
+
let index = 0;
|
|
1339
|
+
for (const label of after) {
|
|
1340
|
+
if (before[index] === label)
|
|
1341
|
+
index += 1;
|
|
1342
|
+
}
|
|
1343
|
+
return index === before.length;
|
|
1344
|
+
}
|
|
1345
|
+
function addValueStatements(schema, name, before, after) {
|
|
1346
|
+
const present = new Set(before);
|
|
1347
|
+
const statements = [];
|
|
1348
|
+
const type = qualify(schema, name);
|
|
1349
|
+
for (let index = 0;index < after.length; index += 1) {
|
|
1350
|
+
const label = after[index];
|
|
1351
|
+
if (label === undefined || present.has(label))
|
|
1352
|
+
continue;
|
|
1353
|
+
let beforeNeighbor;
|
|
1354
|
+
for (let cursor = index + 1;cursor < after.length; cursor += 1) {
|
|
1355
|
+
const next = after[cursor];
|
|
1356
|
+
if (next !== undefined && present.has(next)) {
|
|
1357
|
+
beforeNeighbor = next;
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
let sql = `alter type ${type} add value ${quoteLiteral(label)}`;
|
|
1362
|
+
if (beforeNeighbor !== undefined) {
|
|
1363
|
+
sql += ` before ${quoteLiteral(beforeNeighbor)}`;
|
|
1364
|
+
} else {
|
|
1365
|
+
let afterNeighbor;
|
|
1366
|
+
for (let cursor = index - 1;cursor >= 0; cursor -= 1) {
|
|
1367
|
+
const previous = after[cursor];
|
|
1368
|
+
if (previous !== undefined && present.has(previous)) {
|
|
1369
|
+
afterNeighbor = previous;
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (afterNeighbor !== undefined)
|
|
1374
|
+
sql += ` after ${quoteLiteral(afterNeighbor)}`;
|
|
1375
|
+
}
|
|
1376
|
+
statements.push(sql);
|
|
1377
|
+
present.add(label);
|
|
1378
|
+
}
|
|
1379
|
+
return statements;
|
|
1380
|
+
}
|
|
1381
|
+
function parseInList(expression) {
|
|
1382
|
+
const match = /^\(\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.*)\)\s*\)$/i.exec(expression.trim());
|
|
1383
|
+
const column = match?.[1];
|
|
1384
|
+
const body = match?.[2];
|
|
1385
|
+
if (column === undefined || body === undefined)
|
|
1386
|
+
return;
|
|
1387
|
+
const values = [];
|
|
1388
|
+
let index = 0;
|
|
1389
|
+
while (index < body.length) {
|
|
1390
|
+
const char = body[index] ?? "";
|
|
1391
|
+
if (char === " " || char === ",") {
|
|
1392
|
+
index += 1;
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
if (char !== "'")
|
|
1396
|
+
return;
|
|
1397
|
+
let end = index + 1;
|
|
1398
|
+
let value = "";
|
|
1399
|
+
while (end < body.length) {
|
|
1400
|
+
const next = body[end] ?? "";
|
|
1401
|
+
if (next === "'" && body[end + 1] === "'") {
|
|
1402
|
+
value += "'";
|
|
1403
|
+
end += 2;
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
if (next === "'") {
|
|
1407
|
+
end += 1;
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
value += next;
|
|
1411
|
+
end += 1;
|
|
1412
|
+
}
|
|
1413
|
+
values.push(value);
|
|
1414
|
+
index = end;
|
|
1415
|
+
}
|
|
1416
|
+
return { column, values };
|
|
1417
|
+
}
|
|
1418
|
+
function columnNullable(source, table, column) {
|
|
1419
|
+
for (const object of source.objects) {
|
|
1420
|
+
if (object.kind === "column" && object.identity.parent.name === table && object.identity.name === column) {
|
|
1421
|
+
return object.definition.nullable;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
return false;
|
|
1425
|
+
}
|
|
1426
|
+
function columnAlterable(before, after) {
|
|
1427
|
+
return stable(before.definition.generated) === stable(after.definition.generated);
|
|
1428
|
+
}
|
|
1429
|
+
function identityChangeSql(before, after, schema) {
|
|
1430
|
+
if (stable(before.definition.identity) === stable(after.definition.identity))
|
|
1431
|
+
return;
|
|
1432
|
+
const table = qualify(schema, after.identity.parent.name);
|
|
1433
|
+
const name = quoteIdent(after.identity.name);
|
|
1434
|
+
const next = after.definition.identity;
|
|
1435
|
+
const previous = before.definition.identity;
|
|
1436
|
+
if (next === undefined)
|
|
1437
|
+
return `alter table ${table} alter column ${name} drop identity`;
|
|
1438
|
+
if (previous === undefined) {
|
|
1439
|
+
const generated = next.always ? "always" : "by default";
|
|
1440
|
+
return `alter table ${table} alter column ${name} add generated ${generated} as identity`;
|
|
1441
|
+
}
|
|
1442
|
+
return `alter table ${table} alter column ${name} set generated ${next.always ? "always" : "by default"}`;
|
|
1443
|
+
}
|
|
1444
|
+
function omitOwnedSequences(keys, by, source) {
|
|
1445
|
+
for (const key of keys) {
|
|
1446
|
+
const object = by.get(key);
|
|
1447
|
+
if (object?.kind === "sequence" && folded(object, source))
|
|
1448
|
+
keys.delete(key);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function sameDefinition(left, right) {
|
|
1452
|
+
if (left.kind === "materializedView" && right.kind === "materializedView") {
|
|
1453
|
+
return stable(canonicalColumns(left.definition.columns)) === stable(canonicalColumns(right.definition.columns)) && left.definition.query === right.definition.query;
|
|
1454
|
+
}
|
|
1455
|
+
return stable(forComparison(left)) === stable(forComparison(right));
|
|
1456
|
+
}
|
|
1457
|
+
function forComparison(object) {
|
|
1458
|
+
switch (object.kind) {
|
|
1459
|
+
case "column":
|
|
1460
|
+
case "sequence":
|
|
1461
|
+
return { ...object.definition, dataType: canonicalTypeName(object.definition.dataType) };
|
|
1462
|
+
case "view":
|
|
1463
|
+
return { ...object.definition, columns: canonicalColumns(object.definition.columns) };
|
|
1464
|
+
case "function":
|
|
1465
|
+
return {
|
|
1466
|
+
...object.definition,
|
|
1467
|
+
returns: canonicalTypeName(object.definition.returns),
|
|
1468
|
+
arguments: object.definition.arguments.map((argument) => ({
|
|
1469
|
+
...argument,
|
|
1470
|
+
type: canonicalTypeName(argument.type)
|
|
1471
|
+
}))
|
|
1472
|
+
};
|
|
1473
|
+
case "trigger":
|
|
1474
|
+
return {
|
|
1475
|
+
...object.definition,
|
|
1476
|
+
calls: {
|
|
1477
|
+
...object.definition.calls,
|
|
1478
|
+
argTypes: object.definition.calls.argTypes.map((typeName) => canonicalTypeName(typeName))
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
case "type":
|
|
1482
|
+
if (!isDomain(object.definition))
|
|
1483
|
+
return object.definition;
|
|
1484
|
+
return { ...object.definition, base: canonicalTypeName(object.definition.base) };
|
|
1485
|
+
default:
|
|
1486
|
+
return object.definition;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function canonicalColumns(columns) {
|
|
1490
|
+
return columns.map((column) => ({ ...column, dataType: canonicalTypeName(column.dataType) }));
|
|
1491
|
+
}
|
|
1492
|
+
function alignedColumnType(before, after) {
|
|
1493
|
+
if (canonicalTypeName(before.definition.dataType) !== canonicalTypeName(after.definition.dataType)) {
|
|
1494
|
+
return before;
|
|
1495
|
+
}
|
|
1496
|
+
if (before.definition.dataType === after.definition.dataType)
|
|
1497
|
+
return before;
|
|
1498
|
+
return { ...before, definition: { ...before.definition, dataType: after.definition.dataType } };
|
|
1499
|
+
}
|
|
1500
|
+
function covered(object, tables) {
|
|
1501
|
+
if (object.kind === "column" || object.kind === "index" || object.kind === "constraint") {
|
|
1502
|
+
return tables.has(object.identity.parent.name);
|
|
1503
|
+
}
|
|
1504
|
+
if (object.kind === "sequence") {
|
|
1505
|
+
return object.dependencies.some((edge) => edge.target.kind === "table" && tables.has(edge.target.name));
|
|
1506
|
+
}
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
function folded(object, source) {
|
|
1510
|
+
if (object.kind === "column")
|
|
1511
|
+
return true;
|
|
1512
|
+
if (object.kind === "constraint" && object.definition.constraintKind === "primaryKey")
|
|
1513
|
+
return true;
|
|
1514
|
+
if (object.kind !== "sequence")
|
|
1515
|
+
return false;
|
|
1516
|
+
if (identitySequence(object, source))
|
|
1517
|
+
return true;
|
|
1518
|
+
const name = object.identity.name;
|
|
1519
|
+
return source.objects.some((column) => {
|
|
1520
|
+
if (column.kind !== "column" || column.definition.identity === undefined)
|
|
1521
|
+
return false;
|
|
1522
|
+
return name === `${column.identity.parent.name}_${column.identity.name}_seq`;
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
function anchoredParent(object) {
|
|
1526
|
+
if (object.kind === "column" || object.kind === "index" || object.kind === "constraint") {
|
|
1527
|
+
return object.identity.parent.name;
|
|
1528
|
+
}
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
function alterClass(sql) {
|
|
1532
|
+
if (sql.includes("set data type") || sql.includes("set not null"))
|
|
1533
|
+
return "contract";
|
|
1534
|
+
return "expand";
|
|
1535
|
+
}
|
|
1536
|
+
function overall(steps) {
|
|
1537
|
+
if (steps.some((item) => item.class === "contract"))
|
|
1538
|
+
return "contract";
|
|
1539
|
+
if (steps.some((item) => item.class === "unclassified"))
|
|
1540
|
+
return "unclassified";
|
|
1541
|
+
return "expand";
|
|
1542
|
+
}
|
|
1543
|
+
function updateSql(schema, table, column, from, to, cast = false) {
|
|
1544
|
+
const value = to === null ? "null" : quoteLiteral(to);
|
|
1545
|
+
const compare = cast ? `${quoteIdent(column)}::text` : quoteIdent(column);
|
|
1546
|
+
return `update ${qualify(schema, table)} set ${quoteIdent(column)} = ${value} where ${compare} = ${quoteLiteral(from)}`;
|
|
1547
|
+
}
|
|
1548
|
+
function hasTable(source, name) {
|
|
1549
|
+
return source.objects.some((object) => object.kind === "table" && object.identity.name === name);
|
|
1550
|
+
}
|
|
1551
|
+
function hasColumn(source, table, name) {
|
|
1552
|
+
return source.objects.some((object) => object.kind === "column" && object.identity.parent.name === table && object.identity.name === name);
|
|
1553
|
+
}
|
|
1554
|
+
function index2(source) {
|
|
1555
|
+
return source.objects.map((object) => ({ key: identityKey2(object.identity), object }));
|
|
1556
|
+
}
|
|
1557
|
+
function step(sql, classification, action, lock, transactional = true) {
|
|
1558
|
+
return { sql, class: classification, action, lock, transactional };
|
|
1559
|
+
}
|
|
1560
|
+
function stable(value) {
|
|
1561
|
+
return JSON.stringify(order(value));
|
|
1562
|
+
}
|
|
1563
|
+
function order(value) {
|
|
1564
|
+
if (Array.isArray(value))
|
|
1565
|
+
return value.map((item) => order(item));
|
|
1566
|
+
if (value !== null && typeof value === "object") {
|
|
1567
|
+
const record = value;
|
|
1568
|
+
const next = {};
|
|
1569
|
+
for (const key of Object.keys(record).sort())
|
|
1570
|
+
next[key] = order(record[key]);
|
|
1571
|
+
return next;
|
|
1572
|
+
}
|
|
1573
|
+
return value;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
export { createdRoleName, changesRole, assertExtensionsAvailable, omitManagedObjects, parseReplace, planMigration, formatPlan, parsePlan, staleRenames };
|