db-migrate 1.5.0 → 1.7.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/api.js +21 -0
- package/lib/chain.js +2 -2
- package/lib/commands/generated.js +1 -0
- package/lib/commands/run.js +57 -1
- package/lib/commands/set-default-argv.js +17 -1
- package/lib/commands/status.js +36 -0
- package/lib/dml.js +38 -0
- package/lib/learn.js +4 -2
- package/lib/release.js +137 -21
- package/lib/state.js +9 -1
- package/lib/status.js +179 -0
- package/lib/walker.js +27 -6
- package/lib/work.js +4 -1
- package/package.json +1 -1
package/api.js
CHANGED
|
@@ -421,6 +421,27 @@ dbmigrate.prototype = {
|
|
|
421
421
|
this.internals.onComplete = callback;
|
|
422
422
|
},
|
|
423
423
|
|
|
424
|
+
/**
|
|
425
|
+
* What db-migrate knows about the database: pending migrations, the lock,
|
|
426
|
+
* the jobs of background migrations, the release and what is deprecated
|
|
427
|
+
* or due, see lib/status.js.
|
|
428
|
+
*/
|
|
429
|
+
status: function (scope, callback) {
|
|
430
|
+
if (typeof scope === 'function') {
|
|
431
|
+
callback = scope;
|
|
432
|
+
scope = undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (scope) {
|
|
436
|
+
this.internals.migrationMode = scope;
|
|
437
|
+
this.internals.matching = scope;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return Promise.resolve(load('status')(this.internals, this.config)).asCallback(
|
|
441
|
+
callback
|
|
442
|
+
);
|
|
443
|
+
},
|
|
444
|
+
|
|
424
445
|
/**
|
|
425
446
|
* Runs the jobs of background migrations, see lib/work.js for the
|
|
426
447
|
* options. Returns { done, stop }, done resolves with the jobs done and
|
package/lib/chain.js
CHANGED
|
@@ -190,7 +190,7 @@ class Chain {
|
|
|
190
190
|
throw unknownTable(t);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
const entry = Release.entry(internals, t, options);
|
|
193
|
+
const entry = Release.entry(internals, t, options, this.file);
|
|
194
194
|
return this.run('setDeprecated', ['tables', t, null, entry], 'learn');
|
|
195
195
|
};
|
|
196
196
|
|
|
@@ -205,7 +205,7 @@ class Chain {
|
|
|
205
205
|
throw unknownColumn(t, c);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
const entry = Release.entry(internals, c, options);
|
|
208
|
+
const entry = Release.entry(internals, c, options, this.file);
|
|
209
209
|
if (columns[c].notNull) {
|
|
210
210
|
// the application stops writing it. The whole spec, MySQL changes
|
|
211
211
|
// a column by its full definition
|
|
@@ -23,6 +23,7 @@ const files = {
|
|
|
23
23
|
"run": require('./run.js'),
|
|
24
24
|
"seed": require('./seed.js'),
|
|
25
25
|
"set-default-argv": require('./set-default-argv.js'),
|
|
26
|
+
"status": require('./status.js'),
|
|
26
27
|
"sync": require('./sync.js'),
|
|
27
28
|
"undo-seed": require('./undo-seed.js'),
|
|
28
29
|
"up": require('./up.js'),
|
package/lib/commands/run.js
CHANGED
|
@@ -97,6 +97,62 @@ function run (internals, config) {
|
|
|
97
97
|
report(load('db')(internals, config));
|
|
98
98
|
}
|
|
99
99
|
break;
|
|
100
|
+
case 'status':
|
|
101
|
+
if (folder[1]) {
|
|
102
|
+
internals.matching = folder[1];
|
|
103
|
+
internals.migrationMode = folder[1];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
report(
|
|
107
|
+
load('status')(internals, config).then(result => {
|
|
108
|
+
console.log(require('../status').format(result));
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'work': {
|
|
114
|
+
// runs the jobs of background migrations, until none is left or, with
|
|
115
|
+
// --watch, until stopped
|
|
116
|
+
const argv = internals.argv;
|
|
117
|
+
const options = {};
|
|
118
|
+
[
|
|
119
|
+
['parallel', 'parallel'],
|
|
120
|
+
['pause', 'pause'],
|
|
121
|
+
['batch', 'batch'],
|
|
122
|
+
['interval', 'interval'],
|
|
123
|
+
['job-timeout', 'timeout']
|
|
124
|
+
].forEach(([arg, option]) => {
|
|
125
|
+
if (argv[arg] !== undefined) {
|
|
126
|
+
options[option] = Number(argv[arg]);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
options.watch = argv.watch === true;
|
|
130
|
+
|
|
131
|
+
const worker = load('work')(internals, config, options);
|
|
132
|
+
const stop = () => {
|
|
133
|
+
log.info('[jobs] stopping after the current batches');
|
|
134
|
+
worker.stop();
|
|
135
|
+
};
|
|
136
|
+
process.once('SIGINT', stop);
|
|
137
|
+
process.once('SIGTERM', stop);
|
|
138
|
+
|
|
139
|
+
report(
|
|
140
|
+
worker.done.then(result => {
|
|
141
|
+
log.info(`[jobs] ${result.done.length} job(s) done`);
|
|
142
|
+
|
|
143
|
+
if (result.failed.length) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`${result.failed.length} background job(s) failed: ` +
|
|
146
|
+
result.failed.map(f => f.name).join(', ')
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
process.exit(0);
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
100
156
|
case 'seed': {
|
|
101
157
|
// seed [name], seed down [name], seed reset
|
|
102
158
|
const args = internals.argv._.map(String);
|
|
@@ -123,7 +179,7 @@ function run (internals, config) {
|
|
|
123
179
|
);
|
|
124
180
|
} else {
|
|
125
181
|
log.error(
|
|
126
|
-
'Invalid Action: Must be [up|down|check|create|reset|sync|db|seed].'
|
|
182
|
+
'Invalid Action: Must be [up|down|check|create|reset|sync|db|seed|work|status].'
|
|
127
183
|
);
|
|
128
184
|
yargs.showHelp();
|
|
129
185
|
process.exit(1);
|
|
@@ -27,7 +27,7 @@ module.exports = function (internals, isModule) {
|
|
|
27
27
|
internals.argv = yargs
|
|
28
28
|
.default(defaultConfig)
|
|
29
29
|
.usage(
|
|
30
|
-
'Usage: db-migrate [up|down|check|reset|sync|create|db|seed] ' +
|
|
30
|
+
'Usage: db-migrate [up|down|check|reset|sync|create|db|seed|work|status] ' +
|
|
31
31
|
'[[dbname/]migrationName|all] [options]'
|
|
32
32
|
)
|
|
33
33
|
.describe(
|
|
@@ -128,6 +128,22 @@ module.exports = function (internals, isModule) {
|
|
|
128
128
|
' held by another process is considered stale and taken over.'
|
|
129
129
|
)
|
|
130
130
|
.number('lock-timeout')
|
|
131
|
+
.describe('parallel', 'work: jobs of background migrations run at once.')
|
|
132
|
+
.number('parallel')
|
|
133
|
+
.describe('pause', 'work: milliseconds to wait between two batches of a job.')
|
|
134
|
+
.number('pause')
|
|
135
|
+
.describe('batch', 'work: rows per batch of a job.')
|
|
136
|
+
.number('batch')
|
|
137
|
+
.describe('watch', 'work: keep looking for new jobs, until stopped.')
|
|
138
|
+
.boolean('watch')
|
|
139
|
+
.describe('interval', 'work: milliseconds between looking for new jobs.')
|
|
140
|
+
.number('interval')
|
|
141
|
+
.describe(
|
|
142
|
+
'job-timeout',
|
|
143
|
+
'work: milliseconds after which the job of a worker which stopped ' +
|
|
144
|
+
'responding is taken over.'
|
|
145
|
+
)
|
|
146
|
+
.number('job-timeout')
|
|
131
147
|
.describe(
|
|
132
148
|
'lock-interval',
|
|
133
149
|
'Milliseconds between checks while waiting for the migration lock.'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const migrationHook = require('./helper/migration-hook.js');
|
|
5
|
+
const status = require('../status');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The status of the database, see lib/status.js.
|
|
9
|
+
*/
|
|
10
|
+
module.exports = async function (internals, config) {
|
|
11
|
+
await migrationHook(internals);
|
|
12
|
+
const Migrator = require('../walker.js');
|
|
13
|
+
const index = require('../../connect');
|
|
14
|
+
|
|
15
|
+
const migrator = await index.connect(
|
|
16
|
+
{
|
|
17
|
+
config: config.getCurrent().settings,
|
|
18
|
+
internals: internals,
|
|
19
|
+
prefix: 'migration'
|
|
20
|
+
},
|
|
21
|
+
Migrator
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
migrator.directory = path.resolve(
|
|
25
|
+
internals.argv['migrations-dir'],
|
|
26
|
+
internals.locTitle || ''
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await migrator.createMigrationsTable();
|
|
31
|
+
const result = await status(migrator);
|
|
32
|
+
return internals.onComplete(migrator, internals, null, result);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return internals.onComplete(migrator, internals, err);
|
|
35
|
+
}
|
|
36
|
+
};
|
package/lib/dml.js
CHANGED
|
@@ -798,6 +798,35 @@ class Dml {
|
|
|
798
798
|
);
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
+
/**
|
|
802
|
+
* dropBackups([migration]), drops the backups of update and delete of the
|
|
803
|
+
* migrations due, or of the one named, due or not. They can not be
|
|
804
|
+
* reverted afterwards, nor this migration.
|
|
805
|
+
*/
|
|
806
|
+
dropBackups (migration) {
|
|
807
|
+
return this.step(
|
|
808
|
+
'dropBackups',
|
|
809
|
+
migration ? [migration] : [],
|
|
810
|
+
() => ({ t: 3, a: 'dropBackups', c: [migration || null] }),
|
|
811
|
+
async () => {
|
|
812
|
+
const Release = require('./release');
|
|
813
|
+
const groups = await Release.backupsOf(
|
|
814
|
+
this.pdriver,
|
|
815
|
+
this.internals,
|
|
816
|
+
this.internals.releaseCurrent || 0,
|
|
817
|
+
this.internals.releaseIndex || {},
|
|
818
|
+
!!migration
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
for (const group of groups) {
|
|
822
|
+
if (!migration || group.m === migration) {
|
|
823
|
+
await Release.dropBackups(this.driver, this.pdriver, this.internals, group);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
801
830
|
/**
|
|
802
831
|
* Reading is no step, it does not change anything.
|
|
803
832
|
*/
|
|
@@ -835,6 +864,14 @@ class Dml {
|
|
|
835
864
|
entry.b = 1;
|
|
836
865
|
entry.o = 0;
|
|
837
866
|
await this.save();
|
|
867
|
+
// dropped once the migration is final, after enough releases
|
|
868
|
+
await require('./release').registerBackup(
|
|
869
|
+
this.pdriver,
|
|
870
|
+
this.internals,
|
|
871
|
+
entry.c[1],
|
|
872
|
+
this.file.name,
|
|
873
|
+
table
|
|
874
|
+
);
|
|
838
875
|
}
|
|
839
876
|
|
|
840
877
|
/**
|
|
@@ -991,6 +1028,7 @@ const revert = async (context, file, internals, pdriver, d) => {
|
|
|
991
1028
|
entry.b = 2;
|
|
992
1029
|
await d.save();
|
|
993
1030
|
await d.driver.runSql(`DROP TABLE IF EXISTS ${d.ddl(entry.c[1])}`);
|
|
1031
|
+
await require('./release').forgetBackups(pdriver, internals, [entry.c[1]]);
|
|
994
1032
|
}
|
|
995
1033
|
}
|
|
996
1034
|
|
package/lib/learn.js
CHANGED
|
@@ -145,10 +145,12 @@ class STD {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
renameTable (t, n) {
|
|
148
|
-
// deprecations move with the table
|
|
148
|
+
// deprecations move with the table, unless it is renamed for its own
|
|
149
|
+
// deprecation, which stays with its name
|
|
149
150
|
['tables', 'columns'].forEach(kind => {
|
|
150
151
|
const d = this.deprecated[kind];
|
|
151
|
-
|
|
152
|
+
const own = kind === 'tables' && d && d[t] && d[t].to === n;
|
|
153
|
+
if (d && d[t] !== undefined && !own) {
|
|
152
154
|
d[n] = d[t];
|
|
153
155
|
delete d[t];
|
|
154
156
|
}
|
package/lib/release.js
CHANGED
|
@@ -28,6 +28,7 @@ const log = require('db-migrate-shared').log;
|
|
|
28
28
|
|
|
29
29
|
const PREFIX = '__dbmigrate_release__:';
|
|
30
30
|
const PURGES = '__dbmigrate_purges__';
|
|
31
|
+
const BACKUPS = '__dbmigrate_backups__';
|
|
31
32
|
const DEFAULTS = { releases: 4, drop: 'manual' };
|
|
32
33
|
const DROPS = ['auto', 'manual'];
|
|
33
34
|
|
|
@@ -67,10 +68,11 @@ const releases = files => {
|
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
/**
|
|
70
|
-
* The name a deprecated table or column is renamed to
|
|
71
|
+
* The name a deprecated table or column is renamed to, with the time of the
|
|
72
|
+
* migration deprecating it, so fix learns the same name again.
|
|
71
73
|
*/
|
|
72
|
-
const hiddenName = name => {
|
|
73
|
-
const time =
|
|
74
|
+
const hiddenName = (name, file) => {
|
|
75
|
+
const time = String(file.name).split('-')[0];
|
|
74
76
|
const plain = `__dbm_deprecated_${name}_${time}`;
|
|
75
77
|
|
|
76
78
|
if (plain.length <= 60) {
|
|
@@ -116,8 +118,8 @@ const settings = (internals, entry) => {
|
|
|
116
118
|
return Object.assign({}, DEFAULTS, project, entry.o || {});
|
|
117
119
|
};
|
|
118
120
|
|
|
119
|
-
const entry = (internals, name, options) => {
|
|
120
|
-
const e = { r: internals.releaseLabel || null, to: hiddenName(name) };
|
|
121
|
+
const entry = (internals, name, options, file) => {
|
|
122
|
+
const e = { r: internals.releaseLabel || null, to: hiddenName(name, file) };
|
|
121
123
|
const o = checkOptions(options);
|
|
122
124
|
|
|
123
125
|
if (Object.keys(o).length) {
|
|
@@ -220,13 +222,18 @@ const purges = async (pdriver, internals) => {
|
|
|
220
222
|
return { row, value: row ? JSON.parse(row.value) : {} };
|
|
221
223
|
};
|
|
222
224
|
|
|
223
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Changes a row of the state by compare and swap, retried until nobody
|
|
227
|
+
* wrote in between. mutate returns false to leave it as it is.
|
|
228
|
+
*/
|
|
229
|
+
const changeState = async (pdriver, internals, key, mutate) => {
|
|
224
230
|
if (internals.dryRun) {
|
|
225
231
|
return;
|
|
226
232
|
}
|
|
227
233
|
|
|
228
234
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
229
|
-
const
|
|
235
|
+
const row = await pdriver._getKV(internals.migrationState, key);
|
|
236
|
+
const value = row ? JSON.parse(row.value) : {};
|
|
230
237
|
if (mutate(value) === false) {
|
|
231
238
|
return;
|
|
232
239
|
}
|
|
@@ -234,23 +241,26 @@ const changePurges = async (pdriver, internals, mutate) => {
|
|
|
234
241
|
const next = JSON.stringify(value);
|
|
235
242
|
if (!row) {
|
|
236
243
|
try {
|
|
237
|
-
await pdriver._insertKV(internals.migrationState,
|
|
244
|
+
await pdriver._insertKV(internals.migrationState, key, next);
|
|
238
245
|
} catch (err) {
|
|
239
|
-
log.verbose(
|
|
246
|
+
log.verbose(`[release] ${key} inserted concurrently`, err.message);
|
|
240
247
|
}
|
|
241
248
|
} else {
|
|
242
|
-
await pdriver._updateKVC(internals.migrationState,
|
|
249
|
+
await pdriver._updateKVC(internals.migrationState, key, next, 'value', row.value);
|
|
243
250
|
}
|
|
244
251
|
|
|
245
|
-
const after = await pdriver._getKV(internals.migrationState,
|
|
252
|
+
const after = await pdriver._getKV(internals.migrationState, key);
|
|
246
253
|
if (after && after.value === next) {
|
|
247
254
|
return;
|
|
248
255
|
}
|
|
249
256
|
}
|
|
250
257
|
|
|
251
|
-
throw new Error(
|
|
258
|
+
throw new Error(`[release] could not change ${key}, too much contention`);
|
|
252
259
|
};
|
|
253
260
|
|
|
261
|
+
const changePurges = (pdriver, internals, mutate) =>
|
|
262
|
+
changeState(pdriver, internals, PURGES, mutate);
|
|
263
|
+
|
|
254
264
|
/**
|
|
255
265
|
* Schedules purging the rows of a soft delete, { purge: true } takes the
|
|
256
266
|
* options of the project.
|
|
@@ -284,6 +294,77 @@ const forgetPurges = (pdriver, internals, t, prefix) =>
|
|
|
284
294
|
marks.forEach(mark => delete value[mark]);
|
|
285
295
|
});
|
|
286
296
|
|
|
297
|
+
/**
|
|
298
|
+
* The backup tables of update and delete in copy mode, kept to revert their
|
|
299
|
+
* migration: { [backup]: { m: migration, t: table, r: release } }. Once
|
|
300
|
+
* enough releases passed, the migration is final, its backups are dropped.
|
|
301
|
+
*/
|
|
302
|
+
const registerBackup = (pdriver, internals, backup, migration, t) =>
|
|
303
|
+
changeState(pdriver, internals, BACKUPS, value => {
|
|
304
|
+
value[backup] = { m: migration, t, r: internals.releaseLabel || null };
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const forgetBackups = (pdriver, internals, backups) =>
|
|
308
|
+
changeState(pdriver, internals, BACKUPS, value => {
|
|
309
|
+
if (!backups.some(b => value[b])) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
backups.forEach(b => delete value[b]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The migrations with backups, with their age and options, those due only
|
|
318
|
+
* unless all.
|
|
319
|
+
*/
|
|
320
|
+
const backupsOf = async (pdriver, internals, index, releaseIndex, all = false) => {
|
|
321
|
+
const row = await pdriver._getKV(internals.migrationState, BACKUPS);
|
|
322
|
+
const value = row ? JSON.parse(row.value) : {};
|
|
323
|
+
const migrations = {};
|
|
324
|
+
|
|
325
|
+
Object.keys(value).forEach(backup => {
|
|
326
|
+
const e = value[backup];
|
|
327
|
+
if (!migrations[e.m]) {
|
|
328
|
+
const age = index - (e.r === null ? 0 : releaseIndex[e.r] || 0);
|
|
329
|
+
migrations[e.m] = Object.assign({ m: e.m, age, backups: [] }, settings(internals, e));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
migrations[e.m].backups.push(backup);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return Object.keys(migrations)
|
|
336
|
+
.map(m => migrations[m])
|
|
337
|
+
.filter(g => all || g.age >= g.releases);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Drops the backups of a migration, which can not be reverted afterwards:
|
|
342
|
+
* its steps are recorded as irreversible first.
|
|
343
|
+
*/
|
|
344
|
+
const dropBackups = async (driver, pdriver, internals, group) => {
|
|
345
|
+
log.info(`[release] dropping the backups of ${group.m}, it can not be reverted anymore`);
|
|
346
|
+
if (internals.dryRun) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const row = await pdriver._getKV(internals.migrationState, group.m);
|
|
351
|
+
if (row) {
|
|
352
|
+
const state = JSON.parse(row.value);
|
|
353
|
+
state.s = (state.s || []).map(e =>
|
|
354
|
+
e.t === 4 && group.backups.indexOf(e.c[1]) !== -1
|
|
355
|
+
? { t: 3, a: e.a, c: [e.c[0]], n: e.n }
|
|
356
|
+
: e
|
|
357
|
+
);
|
|
358
|
+
await pdriver._updateKV(internals.migrationState, group.m, JSON.stringify(state));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const backup of group.backups) {
|
|
362
|
+
await driver.runSql(`DROP TABLE IF EXISTS ${driver.escapeDDL(backup)}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
await forgetBackups(pdriver, internals, group.backups);
|
|
366
|
+
};
|
|
367
|
+
|
|
287
368
|
const duePurges = async (pdriver, internals, index, releaseIndex) => {
|
|
288
369
|
const { value } = await purges(pdriver, internals);
|
|
289
370
|
|
|
@@ -310,24 +391,36 @@ const unrecorded = walker =>
|
|
|
310
391
|
});
|
|
311
392
|
|
|
312
393
|
/**
|
|
313
|
-
* Runs the steps due before the first migration of the release label.
|
|
394
|
+
* Runs the steps due before the first migration of the release label. fix
|
|
395
|
+
* only learns them again, like the migrations it learns.
|
|
314
396
|
*/
|
|
315
|
-
const start = async (walker, label, index, releaseIndex) => {
|
|
397
|
+
const start = async (walker, label, index, releaseIndex, mode = 'up') => {
|
|
316
398
|
const internals = walker.internals;
|
|
317
399
|
const { renames, drops } = due(internals, index, releaseIndex);
|
|
318
|
-
const purged = (await duePurges(walker._pdriver, internals, index, releaseIndex)).filter(
|
|
319
|
-
e => e.drop === 'auto'
|
|
320
|
-
);
|
|
321
400
|
|
|
322
|
-
if (
|
|
323
|
-
|
|
401
|
+
if (mode === 'up') {
|
|
402
|
+
const purged = (
|
|
403
|
+
await duePurges(walker._pdriver, internals, index, releaseIndex)
|
|
404
|
+
).filter(e => e.drop === 'auto');
|
|
405
|
+
|
|
406
|
+
if (purged.length) {
|
|
407
|
+
await purge(walker, label, purged);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const final = (
|
|
411
|
+
await backupsOf(walker._pdriver, internals, index, releaseIndex)
|
|
412
|
+
).filter(g => g.drop === 'auto');
|
|
413
|
+
|
|
414
|
+
for (const group of final) {
|
|
415
|
+
await dropBackups(walker._driver, walker._pdriver, internals, group);
|
|
416
|
+
}
|
|
324
417
|
}
|
|
325
418
|
|
|
326
419
|
if (!renames.length && !drops.length) {
|
|
327
420
|
return;
|
|
328
421
|
}
|
|
329
422
|
|
|
330
|
-
log.info(`[release] starting release ${label}`);
|
|
423
|
+
log.info(`[release] ${mode === 'fix' ? 'learning' : 'starting'} release ${label}`);
|
|
331
424
|
renames.forEach(item =>
|
|
332
425
|
log.info(`[release] renaming the deprecated ${describe(item)} to ${item.entry.to}`)
|
|
333
426
|
);
|
|
@@ -352,7 +445,11 @@ const start = async (walker, label, index, releaseIndex) => {
|
|
|
352
445
|
return;
|
|
353
446
|
}
|
|
354
447
|
|
|
355
|
-
await require('./executors/versioned/v2')
|
|
448
|
+
await require('./executors/versioned/v2')[mode](
|
|
449
|
+
unrecorded(walker),
|
|
450
|
+
walker.driver,
|
|
451
|
+
file
|
|
452
|
+
);
|
|
356
453
|
};
|
|
357
454
|
|
|
358
455
|
/**
|
|
@@ -454,6 +551,19 @@ const assertRevertible = async (walker, label) => {
|
|
|
454
551
|
/**
|
|
455
552
|
* Warns about what is due for dropping with drop 'manual'.
|
|
456
553
|
*/
|
|
554
|
+
const warnDueBackups = async (pdriver, internals, index, releaseIndex) => {
|
|
555
|
+
(await backupsOf(pdriver, internals, index, releaseIndex))
|
|
556
|
+
.filter(g => g.drop === 'manual')
|
|
557
|
+
.forEach(g =>
|
|
558
|
+
log.warn(
|
|
559
|
+
`[release] the backups of ${g.m} are due for dropping, it ran ` +
|
|
560
|
+
`${g.age} releases ago. Drop them in a dml migration with ` +
|
|
561
|
+
`db.dropBackups(${JSON.stringify(g.m)}), it can not be reverted ` +
|
|
562
|
+
'afterwards.'
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
};
|
|
566
|
+
|
|
457
567
|
const warnDuePurges = async (pdriver, internals, index, releaseIndex) => {
|
|
458
568
|
(await duePurges(pdriver, internals, index, releaseIndex))
|
|
459
569
|
.filter(e => e.drop === 'manual')
|
|
@@ -486,6 +596,12 @@ const warnDue = (internals, index, releaseIndex) => {
|
|
|
486
596
|
module.exports = {
|
|
487
597
|
PREFIX,
|
|
488
598
|
PURGES,
|
|
599
|
+
BACKUPS,
|
|
600
|
+
registerBackup,
|
|
601
|
+
forgetBackups,
|
|
602
|
+
backupsOf,
|
|
603
|
+
dropBackups,
|
|
604
|
+
warnDueBackups,
|
|
489
605
|
schedulePurge,
|
|
490
606
|
forgetPurges,
|
|
491
607
|
warnDuePurges,
|
package/lib/state.js
CHANGED
|
@@ -328,6 +328,9 @@ module.exports = {
|
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
// fix learns the schema from scratch, it must not be loaded again
|
|
332
|
+
internals.learnFromScratch = emptyState === true;
|
|
333
|
+
|
|
331
334
|
const schema = emptyState !== true ? _schema : null;
|
|
332
335
|
if (schema) {
|
|
333
336
|
internals.schema = parseSchema(schema.value);
|
|
@@ -351,6 +354,10 @@ module.exports = {
|
|
|
351
354
|
* so the schema loaded during init is outdated.
|
|
352
355
|
*/
|
|
353
356
|
reloadSchema: async function (driver, internals) {
|
|
357
|
+
if (internals.learnFromScratch) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
354
361
|
const schema = await driver._getKV(internals.migrationState, SSTATE);
|
|
355
362
|
if (schema) {
|
|
356
363
|
internals.schema = parseSchema(schema.value);
|
|
@@ -376,7 +383,8 @@ module.exports = {
|
|
|
376
383
|
) {
|
|
377
384
|
const mig = await driver._getKV(internals.migrationState, file.name);
|
|
378
385
|
|
|
379
|
-
|
|
386
|
+
// fix learns the records again from scratch
|
|
387
|
+
if (mig && mig.value !== '{}' && op !== 'fix') {
|
|
380
388
|
internals.modSchema = JSON.parse(mig.value);
|
|
381
389
|
// records of older runs, they belong to no step of this run
|
|
382
390
|
(internals.modSchema.s || []).forEach(entry => {
|
package/lib/status.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const dbmUtil = require('db-migrate-shared').util;
|
|
4
|
+
const File = require('./file');
|
|
5
|
+
const State = require('./state');
|
|
6
|
+
const Jobs = require('./jobs');
|
|
7
|
+
const Release = require('./release');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What db-migrate knows about the database, without changing anything:
|
|
11
|
+
*
|
|
12
|
+
* pending: migrations not run yet, background migrations running excluded
|
|
13
|
+
* lock: the process holding the migration lock, and the migration it was
|
|
14
|
+
* interrupted in, if any
|
|
15
|
+
* jobs: the jobs of background migrations, paused by whom if so
|
|
16
|
+
* release: the latest release migrated
|
|
17
|
+
* deprecated: tables and columns deprecated, renamed or not, and when due
|
|
18
|
+
* purges, backups: soft deletes and backups due
|
|
19
|
+
*/
|
|
20
|
+
module.exports = async function status (walker) {
|
|
21
|
+
const internals = walker.internals;
|
|
22
|
+
const pdriver = walker._pdriver;
|
|
23
|
+
const supports = State.supportsLock(pdriver);
|
|
24
|
+
|
|
25
|
+
const [allFiles, completed] = await Promise.all([
|
|
26
|
+
File.loadFromFileystem(walker.directory, walker.prefix, internals).catch(err => {
|
|
27
|
+
if (err.code === 'ENOENT') {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
throw err;
|
|
32
|
+
}),
|
|
33
|
+
File.loadFromDatabase(walker.directory, walker.prefix, walker._driver, internals)
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
walker._releases(allFiles, completed);
|
|
37
|
+
const release = walker._release;
|
|
38
|
+
const index = walker._applied;
|
|
39
|
+
|
|
40
|
+
const jobs = supports ? await Jobs.list(pdriver, internals) : {};
|
|
41
|
+
const pending = dbmUtil
|
|
42
|
+
.filterUp(allFiles, completed, undefined, Number.MAX_VALUE)
|
|
43
|
+
.filter(file => !jobs[file.name] || jobs[file.name].s === 'failed')
|
|
44
|
+
.map(file => file.name);
|
|
45
|
+
|
|
46
|
+
const result = {
|
|
47
|
+
pending,
|
|
48
|
+
lock: null,
|
|
49
|
+
jobs: [],
|
|
50
|
+
paused: null,
|
|
51
|
+
release: null,
|
|
52
|
+
deprecated: [],
|
|
53
|
+
purges: [],
|
|
54
|
+
backups: []
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const labels = Object.keys(release.index);
|
|
58
|
+
result.release = labels.find(label => release.index[label] === index) || null;
|
|
59
|
+
|
|
60
|
+
if (supports) {
|
|
61
|
+
const lock = await State.readLock(pdriver, internals);
|
|
62
|
+
if (lock) {
|
|
63
|
+
const s = JSON.parse(lock.value).s;
|
|
64
|
+
result.lock = {
|
|
65
|
+
held: !!s.ID,
|
|
66
|
+
since: s.date,
|
|
67
|
+
interrupted: s.fin === 0 && s.f ? { migration: s.f, op: s.o, step: s.step, done: s.done } : null
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const paused = await Jobs.paused(pdriver, internals);
|
|
72
|
+
result.paused = paused ? paused.ID : null;
|
|
73
|
+
result.jobs = Object.keys(jobs)
|
|
74
|
+
.sort()
|
|
75
|
+
.map(name => {
|
|
76
|
+
const job = jobs[name];
|
|
77
|
+
return {
|
|
78
|
+
name,
|
|
79
|
+
state: job.s,
|
|
80
|
+
blocking: !!job.blocking,
|
|
81
|
+
step: job.step,
|
|
82
|
+
done: job.done,
|
|
83
|
+
error: job.err || null
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const due = (e, age) => {
|
|
88
|
+
const options = Release.settings(internals, e);
|
|
89
|
+
return Object.assign({ age, due: age >= options.releases }, options);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
result.deprecated = Release.list(internals, index, release.index).map(item =>
|
|
93
|
+
Object.assign(
|
|
94
|
+
{
|
|
95
|
+
table: item.t,
|
|
96
|
+
column: item.c || null,
|
|
97
|
+
name: item.name,
|
|
98
|
+
renamed: item.renamed,
|
|
99
|
+
release: item.entry.r
|
|
100
|
+
},
|
|
101
|
+
due(item.entry, item.age)
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const purges = await pdriver._getKV(internals.migrationState, Release.PURGES);
|
|
106
|
+
const scheduled = purges ? JSON.parse(purges.value) : {};
|
|
107
|
+
result.purges = Object.keys(scheduled).map(mark => {
|
|
108
|
+
const e = scheduled[mark];
|
|
109
|
+
const age = index - (e.r === null ? 0 : release.index[e.r] || 0);
|
|
110
|
+
return Object.assign({ table: e.t, by: mark.slice(5), release: e.r }, due(e, age));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
result.backups = (
|
|
114
|
+
await Release.backupsOf(pdriver, internals, index, release.index, true)
|
|
115
|
+
).map(g => ({
|
|
116
|
+
migration: g.m,
|
|
117
|
+
tables: g.backups,
|
|
118
|
+
age: g.age,
|
|
119
|
+
due: g.age >= g.releases,
|
|
120
|
+
releases: g.releases,
|
|
121
|
+
drop: g.drop
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The status as text, for the command line.
|
|
130
|
+
*/
|
|
131
|
+
module.exports.format = s => {
|
|
132
|
+
const lines = [];
|
|
133
|
+
const section = (title, items, line) => {
|
|
134
|
+
lines.push(`${title}:${items.length ? '' : ' none'}`);
|
|
135
|
+
items.forEach(item => lines.push(` ${line(item)}`));
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
section('Pending migrations', s.pending, name => name);
|
|
139
|
+
lines.push(`Release: ${s.release === null ? 'none' : s.release}`);
|
|
140
|
+
|
|
141
|
+
if (s.lock) {
|
|
142
|
+
lines.push(
|
|
143
|
+
`Migration lock: ${s.lock.held ? `held since ${s.lock.since}` : 'free'}` +
|
|
144
|
+
(s.lock.interrupted
|
|
145
|
+
? `, ${s.lock.interrupted.op} of ${s.lock.interrupted.migration} ` +
|
|
146
|
+
`interrupted at step ${s.lock.interrupted.step}`
|
|
147
|
+
: '')
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
section(
|
|
152
|
+
`Background jobs${s.paused ? ' (paused for migrations)' : ''}`,
|
|
153
|
+
s.jobs,
|
|
154
|
+
job =>
|
|
155
|
+
`${job.name}: ${job.state}${job.blocking ? ', blocking' : ''}` +
|
|
156
|
+
(job.step ? `, step ${job.step}, ${job.done} done` : '') +
|
|
157
|
+
(job.error ? `, ${job.error}` : '')
|
|
158
|
+
);
|
|
159
|
+
section(
|
|
160
|
+
'Deprecated',
|
|
161
|
+
s.deprecated,
|
|
162
|
+
d =>
|
|
163
|
+
`${d.column ? `column "${d.column}" of "${d.table}"` : `table "${d.table}"`}` +
|
|
164
|
+
`${d.renamed ? ` renamed to ${d.name}` : ''}, ${d.age} of ${d.releases} ` +
|
|
165
|
+
`releases${d.due ? `, due to drop (${d.drop})` : ''}`
|
|
166
|
+
);
|
|
167
|
+
section(
|
|
168
|
+
'Purges',
|
|
169
|
+
s.purges,
|
|
170
|
+
p => `"${p.table}" by ${p.by}, ${p.age} of ${p.releases} releases${p.due ? `, due (${p.drop})` : ''}`
|
|
171
|
+
);
|
|
172
|
+
section(
|
|
173
|
+
'Backups',
|
|
174
|
+
s.backups,
|
|
175
|
+
b => `${b.migration}: ${b.tables.length} table(s), ${b.age} of ${b.releases} releases${b.due ? `, due (${b.drop})` : ''}`
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return lines.join('\n');
|
|
179
|
+
};
|
package/lib/walker.js
CHANGED
|
@@ -363,13 +363,13 @@ Walker.prototype = {
|
|
|
363
363
|
/**
|
|
364
364
|
* Runs the steps due, before the first migration of a new release.
|
|
365
365
|
*/
|
|
366
|
-
_enterRelease: async function (file) {
|
|
366
|
+
_enterRelease: async function (file, mode = 'up') {
|
|
367
367
|
const release = this._release || { map: {}, index: {} };
|
|
368
368
|
const r = release.map[file.name] || { label: null, index: 0 };
|
|
369
369
|
|
|
370
370
|
if (r.index > this._applied) {
|
|
371
371
|
this.internals.releaseIndex = release.index;
|
|
372
|
-
await Release.start(this, r.label, r.index, release.index);
|
|
372
|
+
await Release.start(this, r.label, r.index, release.index, mode);
|
|
373
373
|
this._applied = r.index;
|
|
374
374
|
}
|
|
375
375
|
|
|
@@ -425,6 +425,12 @@ Walker.prototype = {
|
|
|
425
425
|
this._applied,
|
|
426
426
|
this._release.index
|
|
427
427
|
);
|
|
428
|
+
await Release.warnDueBackups(
|
|
429
|
+
this._pdriver,
|
|
430
|
+
this.internals,
|
|
431
|
+
this._applied,
|
|
432
|
+
this._release.index
|
|
433
|
+
);
|
|
428
434
|
}
|
|
429
435
|
},
|
|
430
436
|
|
|
@@ -513,9 +519,23 @@ Walker.prototype = {
|
|
|
513
519
|
this.prefix,
|
|
514
520
|
this._driver,
|
|
515
521
|
this.internals
|
|
516
|
-
).then(completedFiles =>
|
|
517
|
-
|
|
518
|
-
|
|
522
|
+
).then(async completedFiles => {
|
|
523
|
+
// the releases, learned again from the start
|
|
524
|
+
const allFiles = await File.loadFromFileystem(
|
|
525
|
+
this.directory,
|
|
526
|
+
this.prefix,
|
|
527
|
+
this.internals
|
|
528
|
+
).catch(err => {
|
|
529
|
+
if (err.code === 'ENOENT') {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
throw err;
|
|
534
|
+
});
|
|
535
|
+
this._releases(allFiles, []);
|
|
536
|
+
|
|
537
|
+
return dbmUtil.filterDown(completedFiles, partialName, count).sort(sortFn);
|
|
538
|
+
});
|
|
519
539
|
|
|
520
540
|
if (this.internals.check) {
|
|
521
541
|
return determine()
|
|
@@ -529,8 +549,9 @@ Walker.prototype = {
|
|
|
529
549
|
log.info(this.title + 'Nothing to run');
|
|
530
550
|
}
|
|
531
551
|
|
|
532
|
-
return Promise.each(toRun, file => {
|
|
552
|
+
return Promise.each(toRun, async file => {
|
|
533
553
|
log.verbose(this.title + 'preparing to run fix:', file.name);
|
|
554
|
+
await this._enterRelease(file, 'fix').catch(failedIn(file));
|
|
534
555
|
const _meta = file.get()._meta || {};
|
|
535
556
|
const version = _meta.version || 1;
|
|
536
557
|
if (version < 2) {
|
package/lib/work.js
CHANGED
|
@@ -102,7 +102,10 @@ const runJob = async (walker, name, id, options, stopping, result, seen) => {
|
|
|
102
102
|
);
|
|
103
103
|
const file = files.find(f => f.name === name);
|
|
104
104
|
// the release of the migration, a soft delete schedules its purge by it
|
|
105
|
-
|
|
105
|
+
const release = Release.releases(files);
|
|
106
|
+
internals.releaseLabel = (release.map[name] || {}).label || null;
|
|
107
|
+
internals.releaseIndex = release.index;
|
|
108
|
+
internals.releaseCurrent = (release.map[name] || {}).index || 0;
|
|
106
109
|
|
|
107
110
|
if (!file) {
|
|
108
111
|
await update(job => {
|