@apso/cli 0.43.4 → 0.44.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 +7 -11
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/migrate/generate.d.ts +3 -1
- package/dist/commands/migrate/generate.js +42 -8
- package/dist/commands/migrate/plan.d.ts +12 -0
- package/dist/commands/migrate/plan.js +88 -0
- package/dist/commands/migrate/sandbox.d.ts +11 -0
- package/dist/commands/migrate/sandbox.js +132 -0
- package/dist/commands/migrate/verify.d.ts +5 -2
- package/dist/commands/migrate/verify.js +44 -9
- package/dist/commands/migrate.d.ts +16 -3
- package/dist/commands/migrate.js +171 -96
- package/dist/lib/ddl/catalog.d.ts +21 -0
- package/dist/lib/ddl/catalog.js +58 -0
- package/dist/lib/ddl/change.d.ts +19 -0
- package/dist/lib/ddl/change.js +393 -0
- package/dist/lib/ddl/index.d.ts +80 -0
- package/dist/lib/ddl/index.js +309 -0
- package/dist/lib/ddl/migration-file.d.ts +42 -0
- package/dist/lib/ddl/migration-file.js +222 -0
- package/dist/lib/generators/go.js +3 -2
- package/dist/lib/generators/python.js +3 -2
- package/dist/lib/plan/baseline.d.ts +22 -0
- package/dist/lib/plan/baseline.js +100 -0
- package/dist/lib/plan/index.d.ts +52 -0
- package/dist/lib/plan/index.js +272 -0
- package/dist/lib/plan/prompts.d.ts +18 -0
- package/dist/lib/plan/prompts.js +77 -0
- package/dist/lib/verify/index.d.ts +27 -9
- package/dist/lib/verify/index.js +249 -21
- package/dist/lib/verify/review.d.ts +14 -0
- package/dist/lib/verify/review.js +98 -0
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +176 -23
- package/package.json +1 -1
package/dist/commands/migrate.js
CHANGED
|
@@ -1,131 +1,206 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
4
5
|
const path = tslib_1.__importStar(require("path"));
|
|
5
6
|
const core_1 = require("@oclif/core");
|
|
7
|
+
const inquirer = tslib_1.__importStar(require("inquirer"));
|
|
6
8
|
const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
7
9
|
const apsorc_parser_1 = require("../lib/apsorc-parser");
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
const migration_file_1 = require("../lib/ddl/migration-file");
|
|
11
|
+
const plan_1 = require("../lib/plan");
|
|
12
|
+
const baseline_1 = require("../lib/plan/baseline");
|
|
13
|
+
const prompts_1 = require("../lib/plan/prompts");
|
|
14
|
+
const verify_1 = require("../lib/verify");
|
|
15
|
+
const required = (v) => v.trim() !== "" || "Enter an expression";
|
|
11
16
|
class Migrate extends base_command_1.default {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
this.json = false;
|
|
20
|
+
}
|
|
21
|
+
/** Progress for people; agents get only the final JSON. */
|
|
22
|
+
say(s) {
|
|
23
|
+
if (!this.json)
|
|
24
|
+
this.log(s);
|
|
25
|
+
}
|
|
26
|
+
/** Print the outcome: one JSON object for agents, nothing extra for people. */
|
|
27
|
+
out(status, extra) {
|
|
28
|
+
if (this.json)
|
|
29
|
+
this.log(JSON.stringify({ status, ...extra }, null, 2));
|
|
30
|
+
}
|
|
31
|
+
fail(message, extra = {}) {
|
|
32
|
+
if (this.json) {
|
|
33
|
+
this.out("failed", { error: message, ...extra });
|
|
34
|
+
this.exit(1);
|
|
35
|
+
}
|
|
36
|
+
this.error(message, { exit: 1 });
|
|
37
|
+
}
|
|
12
38
|
async run() {
|
|
39
|
+
var _a, _b, _c;
|
|
13
40
|
const { flags } = await this.parse(Migrate);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
this.json = flags.json || !process.stdout.isTTY;
|
|
43
|
+
const interactive = !this.json && Boolean(process.stdin.isTTY);
|
|
44
|
+
// Keep stdout pure JSON for agents: parser notices go to stderr.
|
|
45
|
+
if (this.json)
|
|
46
|
+
console.info = (...m) => process.stderr.write(m.join(" ") + "\n");
|
|
47
|
+
const { apsorc } = (0, apsorc_parser_1.readApsorcFile)();
|
|
48
|
+
const cliBin = (0, verify_1.cliBinPath)(this.config.root);
|
|
49
|
+
if (!flags["skip-generate"]) {
|
|
50
|
+
this.say("Generating code from .apsorc...");
|
|
51
|
+
// Agents run without a terminal, where generate needs the language spelled out.
|
|
52
|
+
const language = (_a = apsorc.language) !== null && _a !== void 0 ? _a : "typescript";
|
|
53
|
+
const gen = await (0, verify_1.run)(process.execPath, [cliBin, "generate", "--language", language], cwd);
|
|
54
|
+
if (!gen.ok)
|
|
55
|
+
this.fail(`apso generate failed:\n${gen.output.trim().split("\n").slice(-15).join("\n")}`);
|
|
19
56
|
}
|
|
20
|
-
|
|
21
|
-
let
|
|
57
|
+
const planPath = path.resolve(".apso/migration-plan.json");
|
|
58
|
+
let baseline;
|
|
22
59
|
try {
|
|
23
|
-
|
|
60
|
+
baseline = (0, baseline_1.resolveBaseline)(cwd, apsorc, flags.baseline, planPath);
|
|
24
61
|
}
|
|
25
62
|
catch (error) {
|
|
26
|
-
|
|
27
|
-
this.error(`Failed to parse .apsorc: ${msg}`);
|
|
28
|
-
}
|
|
29
|
-
if (!flags.sql) {
|
|
30
|
-
this.log("Schema changes detected. Running migration sandbox...");
|
|
63
|
+
this.fail(error.message);
|
|
31
64
|
}
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
65
|
+
this.say(`Comparing with ${baseline.source}.`);
|
|
66
|
+
const previous = fs.existsSync(planPath)
|
|
67
|
+
? JSON.parse(fs.readFileSync(planPath).toString())
|
|
68
|
+
: undefined;
|
|
69
|
+
let plan;
|
|
70
|
+
try {
|
|
71
|
+
plan = (0, plan_1.planWithAnswers)(baseline.schema, apsorc, previous, flags.answer);
|
|
39
72
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Raw SQL output mode
|
|
43
|
-
for (const sql of result.upSql) {
|
|
44
|
-
this.log(sql + ";");
|
|
45
|
-
}
|
|
46
|
-
return;
|
|
73
|
+
catch (error) {
|
|
74
|
+
this.fail(error.message);
|
|
47
75
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const language = parsed.language || "typescript";
|
|
61
|
-
if (language !== "typescript") {
|
|
62
|
-
const generatorConfig = {
|
|
63
|
-
language,
|
|
64
|
-
rootFolder: parsed.rootFolder || "src",
|
|
65
|
-
apiType: parsed.apiType || "rest",
|
|
66
|
-
entities: parsed.entities,
|
|
67
|
-
relationshipMap: parsed.relationshipMap,
|
|
68
|
-
};
|
|
69
|
-
const generator = (0, lib_1.createGenerator)(generatorConfig);
|
|
70
|
-
const migrationFiles = await generator.generateMigration({
|
|
71
|
-
entities: parsed.entities,
|
|
72
|
-
relationshipMap: parsed.relationshipMap,
|
|
73
|
-
upSql: result.upSql,
|
|
74
|
-
downSql: result.downSql,
|
|
75
|
-
});
|
|
76
|
-
for (const file of migrationFiles) {
|
|
77
|
-
const fullPath = path.join(process.cwd(), file.path);
|
|
78
|
-
// eslint-disable-next-line no-await-in-loop
|
|
79
|
-
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
80
|
-
this.log(`Generated migration file: ${file.path}`);
|
|
76
|
+
const save = () => {
|
|
77
|
+
fs.mkdirSync(path.dirname(planPath), { recursive: true });
|
|
78
|
+
fs.writeFileSync(planPath, JSON.stringify({ ...plan, before: baseline.schema }, null, 2) + "\n");
|
|
79
|
+
};
|
|
80
|
+
// Ask until nothing is open. Answers can raise new questions (dropping a
|
|
81
|
+
// field leaves a required one to backfill), so re-plan after each round.
|
|
82
|
+
for (let open = (0, plan_1.openQuestions)(plan); open.length > 0; open = (0, plan_1.openQuestions)(plan)) {
|
|
83
|
+
save();
|
|
84
|
+
if (!interactive) {
|
|
85
|
+
const next = 'Rerun apso migrate --answer <id>=<option> for each. For "sql" and "split:" answers, also set the question\'s "sql" in the plan file: a string, or one expression per split field.';
|
|
86
|
+
if (this.json) {
|
|
87
|
+
this.out("questions", { plan: path.relative(cwd, planPath), baseline: baseline.source, open: open.map((q) => (0, prompts_1.describeQuestion)(q)), next });
|
|
81
88
|
}
|
|
89
|
+
else {
|
|
90
|
+
for (const q of open)
|
|
91
|
+
this.log(`\n${q.id}\n ${q.subject}\n options: ${q.options.join(" | ")}`);
|
|
92
|
+
this.log(`\n${next}`);
|
|
93
|
+
}
|
|
94
|
+
this.exit(2);
|
|
82
95
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
relationshipMap: parsed.relationshipMap,
|
|
87
|
-
});
|
|
88
|
-
this.log("Snapshot updated.");
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
this.log("Run with --apply to update the snapshot.");
|
|
96
|
+
for (const q of open) {
|
|
97
|
+
// eslint-disable-next-line no-await-in-loop
|
|
98
|
+
await this.ask(q);
|
|
92
99
|
}
|
|
100
|
+
plan = (0, plan_1.planWithAnswers)(baseline.schema, apsorc, plan, []);
|
|
93
101
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
save();
|
|
103
|
+
const written = (0, migration_file_1.writePlannedMigration)(cwd, baseline.schema, flags.name, planPath);
|
|
104
|
+
if (!written.ok)
|
|
105
|
+
this.fail(written.steps.map((s) => s.output).filter(Boolean).join("\n"));
|
|
106
|
+
if (!written.needed) {
|
|
107
|
+
this.say("The database already matches .apsorc. Nothing to migrate.");
|
|
108
|
+
this.out("up-to-date", {});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.say(`Wrote ${written.file}`);
|
|
112
|
+
for (const f of (_b = written.findings) !== null && _b !== void 0 ? _b : [])
|
|
113
|
+
this.say(` warning ${f.code}: ${f.message}`);
|
|
114
|
+
let verified = "skipped";
|
|
115
|
+
// Python and Go verify without the project's own database driver.
|
|
116
|
+
const canVerify = ((_c = apsorc.language) !== null && _c !== void 0 ? _c : "typescript") !== "typescript" || (0, verify_1.supportsPglite)(cwd);
|
|
117
|
+
if (!flags["skip-verify"] && canVerify) {
|
|
118
|
+
this.say("Testing the migration on a local copy of the deployed schema...");
|
|
119
|
+
const result = await (0, verify_1.verifyMigration)(cwd, (0, baseline_1.baselineFile)(baseline), written.file, cliBin, {
|
|
120
|
+
seedFile: flags.seed,
|
|
121
|
+
checkFile: flags.check,
|
|
122
|
+
acceptLoss: flags["accept-loss"],
|
|
123
|
+
});
|
|
124
|
+
verified = result.ok;
|
|
125
|
+
if (!this.json)
|
|
126
|
+
(0, verify_1.report)((s) => this.log(s), { ...result, findings: [] }, false);
|
|
127
|
+
if (!result.ok)
|
|
128
|
+
this.fail(`The migration in ${written.file} failed its test. Fix the answers in ${written.planFile} or the .apsorc, then run apso migrate again.`, { file: written.file, planFile: written.planFile, steps: result.steps });
|
|
129
|
+
}
|
|
130
|
+
this.say(`\nReview and commit ${written.file} and ${written.planFile}, then deploy.`);
|
|
131
|
+
if (verified === true && !flags.seed)
|
|
132
|
+
this.say("Tip: --seed <rows.sql> also checks that existing rows survive the migration.");
|
|
133
|
+
this.out("written", { file: written.file, planFile: written.planFile, verified });
|
|
134
|
+
}
|
|
135
|
+
async ask(q) {
|
|
136
|
+
const { choice } = await inquirer.prompt([
|
|
137
|
+
{
|
|
138
|
+
type: "list",
|
|
139
|
+
name: "choice",
|
|
140
|
+
message: q.subject,
|
|
141
|
+
choices: q.options.map((o) => ({ name: (0, prompts_1.optionLabel)(q, o), value: o })),
|
|
142
|
+
default: q.suggested,
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
q.answer = choice;
|
|
146
|
+
if (choice === "sql") {
|
|
147
|
+
const { sql } = await inquirer.prompt([
|
|
148
|
+
{ type: "input", name: "sql", message: (0, prompts_1.sqlHint)(q), validate: required },
|
|
149
|
+
]);
|
|
150
|
+
q.sql = sql;
|
|
151
|
+
}
|
|
152
|
+
if (choice.startsWith("split:")) {
|
|
153
|
+
const targets = choice.slice("split:".length).split(",");
|
|
154
|
+
const defaults = (0, prompts_1.splitDefaults)(q, targets);
|
|
155
|
+
const sql = {};
|
|
156
|
+
for (const t of targets) {
|
|
157
|
+
// eslint-disable-next-line no-await-in-loop
|
|
158
|
+
const { expr } = await inquirer.prompt([
|
|
159
|
+
{ type: "input", name: "expr", message: `SQL for ${t}`, default: defaults[t], validate: required },
|
|
160
|
+
]);
|
|
161
|
+
sql[t] = expr;
|
|
106
162
|
}
|
|
107
|
-
|
|
163
|
+
q.sql = sql;
|
|
108
164
|
}
|
|
109
165
|
}
|
|
110
166
|
}
|
|
111
|
-
Migrate.description = "
|
|
167
|
+
Migrate.description = "Migrate the database to this .apsorc in one step: regenerate the code, compare with the deployed schema, ask about anything ambiguous (a rename or a drop, how a field splits, what replaces a removed value), write the migration, and test it on a local copy. In a terminal it asks; for an agent it prints the open questions as JSON and exits 2, and the agent reruns it with --answer. The steps are also separate commands: apso migrate plan, generate and verify.";
|
|
112
168
|
Migrate.examples = [
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
`$ apso migrate --sql`,
|
|
169
|
+
"$ apso migrate",
|
|
170
|
+
"$ apso migrate --answer field:Contact.phone=rename:mobilePhone",
|
|
171
|
+
"$ apso migrate --seed seed.sql --check checks.sql",
|
|
117
172
|
];
|
|
118
173
|
Migrate.flags = {
|
|
119
|
-
|
|
120
|
-
description: "
|
|
174
|
+
baseline: core_1.Flags.string({
|
|
175
|
+
description: "The deployed .apsorc, the first time only. Later runs find it from the last migration",
|
|
176
|
+
}),
|
|
177
|
+
answer: core_1.Flags.string({
|
|
178
|
+
description: "Answer a question: <question id>=<option>. Repeatable.",
|
|
179
|
+
multiple: true,
|
|
180
|
+
default: [],
|
|
181
|
+
}),
|
|
182
|
+
seed: core_1.Flags.string({
|
|
183
|
+
description: "SQL file of rows to load first; the test then fails if the migration loses any",
|
|
184
|
+
}),
|
|
185
|
+
check: core_1.Flags.string({
|
|
186
|
+
description: "SQL file of SELECTs with a boolean ok column, run after the migration",
|
|
187
|
+
}),
|
|
188
|
+
"accept-loss": core_1.Flags.string({
|
|
189
|
+
description: "A table or table.column whose seeded data may be dropped on purpose (repeatable)",
|
|
190
|
+
multiple: true,
|
|
191
|
+
default: [],
|
|
192
|
+
}),
|
|
193
|
+
name: core_1.Flags.string({ description: "Migration name", default: "SchemaUpdate" }),
|
|
194
|
+
"skip-generate": core_1.Flags.boolean({
|
|
195
|
+
description: "Don't regenerate code from .apsorc first",
|
|
121
196
|
default: false,
|
|
122
197
|
}),
|
|
123
|
-
|
|
124
|
-
description: "
|
|
198
|
+
"skip-verify": core_1.Flags.boolean({
|
|
199
|
+
description: "Write the migration without testing it",
|
|
125
200
|
default: false,
|
|
126
201
|
}),
|
|
127
|
-
|
|
128
|
-
description: "
|
|
202
|
+
json: core_1.Flags.boolean({
|
|
203
|
+
description: "Print the result as JSON (the default when output is not a terminal)",
|
|
129
204
|
default: false,
|
|
130
205
|
}),
|
|
131
206
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a database's schema back out of Postgres's own catalog, in a form two
|
|
3
|
+
* databases can be compared by. Postgres normalizes what it stores (types,
|
|
4
|
+
* defaults, constraint definitions), so two databases built by different SQL
|
|
5
|
+
* compare equal when they ended up the same.
|
|
6
|
+
*/
|
|
7
|
+
export interface Catalog {
|
|
8
|
+
columns: string[];
|
|
9
|
+
constraints: string[];
|
|
10
|
+
indexes: string[];
|
|
11
|
+
enums: string[];
|
|
12
|
+
}
|
|
13
|
+
interface Queryable {
|
|
14
|
+
query<T>(sql: string): Promise<{
|
|
15
|
+
rows: T[];
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export declare function readCatalog(db: Queryable): Promise<Catalog>;
|
|
19
|
+
/** Lines present in one catalog and not the other, prefixed - or +. */
|
|
20
|
+
export declare function catalogDiff(a: Catalog, b: Catalog): string[];
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Read a database's schema back out of Postgres's own catalog, in a form two
|
|
4
|
+
* databases can be compared by. Postgres normalizes what it stores (types,
|
|
5
|
+
* defaults, constraint definitions), so two databases built by different SQL
|
|
6
|
+
* compare equal when they ended up the same.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.catalogDiff = exports.readCatalog = void 0;
|
|
10
|
+
const COLUMNS = `
|
|
11
|
+
SELECT table_name || '.' || column_name || ' ' || format_type(a.atttypid, a.atttypmod)
|
|
12
|
+
|| CASE WHEN c.is_nullable = 'NO' THEN ' NOT NULL' ELSE '' END
|
|
13
|
+
|| COALESCE(' DEFAULT ' || c.column_default, '') AS line
|
|
14
|
+
FROM information_schema.columns c
|
|
15
|
+
JOIN pg_attribute a ON a.attrelid = ('"' || c.table_name || '"')::regclass AND a.attname = c.column_name
|
|
16
|
+
WHERE c.table_schema = 'public'`;
|
|
17
|
+
const CONSTRAINTS = `
|
|
18
|
+
SELECT conrelid::regclass::text || ' ' || conname || ' ' || pg_get_constraintdef(oid) AS line
|
|
19
|
+
FROM pg_constraint
|
|
20
|
+
WHERE connamespace = 'public'::regnamespace AND contype IN ('p', 'u', 'f', 'c')`;
|
|
21
|
+
const INDEXES = `
|
|
22
|
+
SELECT indexdef AS line FROM pg_indexes
|
|
23
|
+
WHERE schemaname = 'public'
|
|
24
|
+
AND indexname NOT IN (SELECT conname FROM pg_constraint WHERE connamespace = 'public'::regnamespace)`;
|
|
25
|
+
const ENUMS = `
|
|
26
|
+
SELECT t.typname || ' ' || string_agg(e.enumlabel, ',' ORDER BY e.enumsortorder) AS line
|
|
27
|
+
FROM pg_type t JOIN pg_enum e ON e.enumtypid = t.oid
|
|
28
|
+
WHERE t.typnamespace = 'public'::regnamespace
|
|
29
|
+
GROUP BY t.typname`;
|
|
30
|
+
const lines = async (db, sql) => {
|
|
31
|
+
const result = await db.query(sql);
|
|
32
|
+
return result.rows.map((r) => r.line).sort();
|
|
33
|
+
};
|
|
34
|
+
async function readCatalog(db) {
|
|
35
|
+
return {
|
|
36
|
+
columns: await lines(db, COLUMNS),
|
|
37
|
+
constraints: await lines(db, CONSTRAINTS),
|
|
38
|
+
indexes: await lines(db, INDEXES),
|
|
39
|
+
enums: await lines(db, ENUMS),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
exports.readCatalog = readCatalog;
|
|
43
|
+
/** Lines present in one catalog and not the other, prefixed - or +. */
|
|
44
|
+
function catalogDiff(a, b) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const key of Object.keys(a)) {
|
|
47
|
+
const left = new Set(a[key]);
|
|
48
|
+
const right = new Set(b[key]);
|
|
49
|
+
for (const l of a[key])
|
|
50
|
+
if (!right.has(l))
|
|
51
|
+
out.push(`- ${key}: ${l}`);
|
|
52
|
+
for (const l of b[key])
|
|
53
|
+
if (!left.has(l))
|
|
54
|
+
out.push(`+ ${key}: ${l}`);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
exports.catalogDiff = catalogDiff;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ApsorcType } from "../apsorc-parser";
|
|
2
|
+
import { Plan } from "../plan";
|
|
3
|
+
export interface ChangeDdl {
|
|
4
|
+
up: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function changeDdl(before: ApsorcType, after: ApsorcType, plan: Plan): ChangeDdl;
|
|
7
|
+
export type DownDdl = {
|
|
8
|
+
down: string[];
|
|
9
|
+
} | {
|
|
10
|
+
irreversible: string[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The reverse migration: the plan from the new schema back to the deployed
|
|
14
|
+
* one, answered from the forward answers. Renames invert, added fields drop,
|
|
15
|
+
* removed enum values map back, a split rejoins, and data that moved (to a
|
|
16
|
+
* new table, or into many-to-many links) is copied back. Anything left
|
|
17
|
+
* unanswered is reported instead of guessed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function downDdl(before: ApsorcType, after: ApsorcType, plan: Plan): DownDdl;
|