db-migrate 1.6.0 → 1.8.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 +40 -0
- package/connect.js +2 -3
- package/index.js +10 -1
- package/lib/commands/down.js +4 -1
- package/lib/commands/generated.js +1 -0
- package/lib/commands/run.js +14 -1
- package/lib/commands/set-default-argv.js +1 -1
- package/lib/commands/status.js +36 -0
- package/lib/config.js +33 -12
- package/lib/dml.js +38 -0
- package/lib/file.js +16 -1
- package/lib/learn.js +4 -2
- package/lib/release.js +114 -7
- package/lib/status.js +179 -0
- package/lib/template.js +0 -9
- package/lib/walker.js +12 -0
- package/lib/work.js +4 -1
- package/package.json +1 -1
package/api.js
CHANGED
|
@@ -192,6 +192,18 @@ dbmigrate.prototype = {
|
|
|
192
192
|
this.internals.argv = argv;
|
|
193
193
|
},
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Every call starts from the options of the instance: the count,
|
|
197
|
+
* destination and scope of an earlier call do not carry over.
|
|
198
|
+
*/
|
|
199
|
+
_begin: function () {
|
|
200
|
+
delete this.internals.argv.count;
|
|
201
|
+
delete this.internals.argv.destination;
|
|
202
|
+
this.internals.migrationMode = undefined;
|
|
203
|
+
this.internals.matching = '';
|
|
204
|
+
this.internals.locTitle = undefined;
|
|
205
|
+
},
|
|
206
|
+
|
|
195
207
|
/**
|
|
196
208
|
* Executes up a given number of migrations or a specific one.
|
|
197
209
|
*
|
|
@@ -199,6 +211,7 @@ dbmigrate.prototype = {
|
|
|
199
211
|
*/
|
|
200
212
|
up: function (specification, opts, callback) {
|
|
201
213
|
var executeUp = load('up');
|
|
214
|
+
this._begin();
|
|
202
215
|
|
|
203
216
|
if (arguments.length > 0) {
|
|
204
217
|
if (typeof specification === 'string') {
|
|
@@ -229,6 +242,7 @@ dbmigrate.prototype = {
|
|
|
229
242
|
*/
|
|
230
243
|
down: function (specification, opts, callback) {
|
|
231
244
|
var executeDown = load('down');
|
|
245
|
+
this._begin();
|
|
232
246
|
|
|
233
247
|
if (arguments.length > 0) {
|
|
234
248
|
if (typeof specification === 'number') {
|
|
@@ -255,6 +269,7 @@ dbmigrate.prototype = {
|
|
|
255
269
|
|
|
256
270
|
fix: function (specification, opts, callback) {
|
|
257
271
|
var executeFix = load('fix');
|
|
272
|
+
this._begin();
|
|
258
273
|
|
|
259
274
|
if (arguments.length > 0) {
|
|
260
275
|
if (typeof specification === 'string') {
|
|
@@ -280,6 +295,7 @@ dbmigrate.prototype = {
|
|
|
280
295
|
|
|
281
296
|
check: function (specification, opts, callback) {
|
|
282
297
|
var executeCheck = load('check');
|
|
298
|
+
this._begin();
|
|
283
299
|
|
|
284
300
|
if (arguments.length > 0) {
|
|
285
301
|
if (typeof specification === 'number') {
|
|
@@ -308,6 +324,7 @@ dbmigrate.prototype = {
|
|
|
308
324
|
*/
|
|
309
325
|
sync: function (specification, opts, callback) {
|
|
310
326
|
var executeSync = load('sync');
|
|
327
|
+
this._begin();
|
|
311
328
|
|
|
312
329
|
if (arguments.length > 0) {
|
|
313
330
|
if (typeof specification === 'string') {
|
|
@@ -332,6 +349,7 @@ dbmigrate.prototype = {
|
|
|
332
349
|
*/
|
|
333
350
|
reset: function (scope, callback) {
|
|
334
351
|
var executeDown = load('down');
|
|
352
|
+
this._begin();
|
|
335
353
|
|
|
336
354
|
if (typeof scope === 'string') {
|
|
337
355
|
this.internals.migrationMode = scope;
|
|
@@ -421,6 +439,28 @@ dbmigrate.prototype = {
|
|
|
421
439
|
this.internals.onComplete = callback;
|
|
422
440
|
},
|
|
423
441
|
|
|
442
|
+
/**
|
|
443
|
+
* What db-migrate knows about the database: pending migrations, the lock,
|
|
444
|
+
* the jobs of background migrations, the release and what is deprecated
|
|
445
|
+
* or due, see lib/status.js.
|
|
446
|
+
*/
|
|
447
|
+
status: function (scope, callback) {
|
|
448
|
+
this._begin();
|
|
449
|
+
if (typeof scope === 'function') {
|
|
450
|
+
callback = scope;
|
|
451
|
+
scope = undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (scope) {
|
|
455
|
+
this.internals.migrationMode = scope;
|
|
456
|
+
this.internals.matching = scope;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return Promise.resolve(load('status')(this.internals, this.config)).asCallback(
|
|
460
|
+
callback
|
|
461
|
+
);
|
|
462
|
+
},
|
|
463
|
+
|
|
424
464
|
/**
|
|
425
465
|
* Runs the jobs of background migrations, see lib/work.js for the
|
|
426
466
|
* options. Returns { done, stop }, done resolves with the jobs done and
|
package/connect.js
CHANGED
|
@@ -71,9 +71,8 @@ exports.connect = async function (config, PassedClass) {
|
|
|
71
71
|
await Promise.promisify(db2.switchDatabase, { context: db2 })(switchTo);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
74
|
+
// of this run only, an instance of the API runs several
|
|
75
|
+
internals.locTitle = scope || undefined;
|
|
77
76
|
|
|
78
77
|
return new PassedClass(
|
|
79
78
|
db,
|
package/index.js
CHANGED
|
@@ -45,7 +45,16 @@ function loadPlugins (options) {
|
|
|
45
45
|
var hooks = {};
|
|
46
46
|
|
|
47
47
|
for (; i < length; ++i) {
|
|
48
|
-
var plugin
|
|
48
|
+
var plugin;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// resolved like node does from the project, so plugins hoisted in a
|
|
52
|
+
// monorepo are found as well
|
|
53
|
+
plugin = require(require.resolve(plugins[i], { paths: [options.cwd] }));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
log.warn(`Could not load the plugin ${plugins[i]}: ${err.message}`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
49
58
|
|
|
50
59
|
if (!plugin.hooks || !plugin.loadPlugin) {
|
|
51
60
|
continue;
|
package/lib/commands/down.js
CHANGED
|
@@ -23,7 +23,10 @@ async function prepare (internals, config) {
|
|
|
23
23
|
Migrator
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
-
migrator.directory = path.resolve(
|
|
26
|
+
migrator.directory = path.resolve(
|
|
27
|
+
internals.argv['migrations-dir'],
|
|
28
|
+
internals.locTitle || ''
|
|
29
|
+
);
|
|
27
30
|
|
|
28
31
|
await migrator.createMigrationsTable();
|
|
29
32
|
log.verbose('migration table created');
|
|
@@ -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,19 @@ 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
|
+
|
|
100
113
|
case 'work': {
|
|
101
114
|
// runs the jobs of background migrations, until none is left or, with
|
|
102
115
|
// --watch, until stopped
|
|
@@ -166,7 +179,7 @@ function run (internals, config) {
|
|
|
166
179
|
);
|
|
167
180
|
} else {
|
|
168
181
|
log.error(
|
|
169
|
-
'Invalid Action: Must be [up|down|check|create|reset|sync|db|seed|work].'
|
|
182
|
+
'Invalid Action: Must be [up|down|check|create|reset|sync|db|seed|work|status].'
|
|
170
183
|
);
|
|
171
184
|
yargs.showHelp();
|
|
172
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|work] ' +
|
|
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(
|
|
@@ -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/config.js
CHANGED
|
@@ -97,16 +97,28 @@ exports.loadFile = function (fileName, currentEnv, plugins) {
|
|
|
97
97
|
// resolves { "ENV": ... } entries of a config, in place
|
|
98
98
|
exports.resolveEnv = walkConfig;
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The value of { "ENV": "NAME", "default": value }, the default if the
|
|
102
|
+
* variable is not set or empty.
|
|
103
|
+
*/
|
|
104
|
+
function fromEnv (entry) {
|
|
105
|
+
var value = process.env[entry.ENV];
|
|
106
|
+
|
|
107
|
+
if (value === undefined || value === '') {
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(entry, 'default')) {
|
|
109
|
+
return entry.default;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
log.verbose('Environment variable ' + entry.ENV + ' is empty!');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
100
118
|
function walkConfig (level) {
|
|
101
119
|
for (var configEntry in level) {
|
|
102
120
|
if (level[configEntry] && level[configEntry].ENV) {
|
|
103
|
-
|
|
104
|
-
log.verbose(
|
|
105
|
-
'Environment variable ' + level[configEntry].ENV + ' is empty!'
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
level[configEntry] = process.env[level[configEntry].ENV];
|
|
121
|
+
level[configEntry] = fromEnv(level[configEntry]);
|
|
110
122
|
} else if (level[configEntry] && typeof level[configEntry] === 'object') {
|
|
111
123
|
level[configEntry] = walkConfig(level[configEntry]);
|
|
112
124
|
}
|
|
@@ -121,11 +133,19 @@ exports.loadObject = function (_config, currentEnv) {
|
|
|
121
133
|
var config = JSON.parse(JSON.stringify(_config));
|
|
122
134
|
|
|
123
135
|
for (var env in config) {
|
|
136
|
+
if (env === 'defaultEnv' || env === 'default') {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
if (config[env].ENV) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
// a database url from the environment, the other keys next to it
|
|
142
|
+
// are added to it
|
|
143
|
+
var url = fromEnv(config[env]);
|
|
144
|
+
if (url) {
|
|
145
|
+
var rest = Object.assign({}, config[env]);
|
|
146
|
+
delete rest.ENV;
|
|
147
|
+
delete rest.default;
|
|
148
|
+
out[env] = Object.assign(parseDatabaseUrl(url), walkConfig(rest));
|
|
129
149
|
}
|
|
130
150
|
} else if (typeof config[env] === 'string') {
|
|
131
151
|
out[env] = parseDatabaseUrl(config[env]);
|
|
@@ -183,7 +203,8 @@ exports.loadObject = function (_config, currentEnv) {
|
|
|
183
203
|
out.setCurrent(config.default);
|
|
184
204
|
} else if (config.defaultEnv) {
|
|
185
205
|
if (config.defaultEnv.ENV) {
|
|
186
|
-
|
|
206
|
+
// without the variable and a default, the default environments
|
|
207
|
+
out.setCurrent(fromEnv(config.defaultEnv) || ['dev', 'development']);
|
|
187
208
|
} else {
|
|
188
209
|
out.setCurrent(config.defaultEnv);
|
|
189
210
|
}
|
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/file.js
CHANGED
|
@@ -221,10 +221,25 @@ File.loadFromDatabase = function (dir, prefix, driver, internals) {
|
|
|
221
221
|
);
|
|
222
222
|
})
|
|
223
223
|
.map(function (result) {
|
|
224
|
-
|
|
224
|
+
// the names are recorded with their scope, dir is the directory of the
|
|
225
|
+
// scope already
|
|
226
|
+
return new File(path.join(rootOf(dir, internals.matching), result.name), internals);
|
|
225
227
|
});
|
|
226
228
|
};
|
|
227
229
|
|
|
230
|
+
/**
|
|
231
|
+
* The migrations directory of a scope directory.
|
|
232
|
+
*/
|
|
233
|
+
function rootOf (dir, scope) {
|
|
234
|
+
const resolved = path.resolve(dir);
|
|
235
|
+
|
|
236
|
+
if (scope && resolved.endsWith(path.sep + path.normalize(scope))) {
|
|
237
|
+
return resolved.slice(0, -(path.normalize(scope).length + 1));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return dir;
|
|
241
|
+
}
|
|
242
|
+
|
|
228
243
|
Promise.promisifyAll(fs);
|
|
229
244
|
|
|
230
245
|
module.exports = File;
|
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
|
|
|
@@ -221,13 +222,18 @@ const purges = async (pdriver, internals) => {
|
|
|
221
222
|
return { row, value: row ? JSON.parse(row.value) : {} };
|
|
222
223
|
};
|
|
223
224
|
|
|
224
|
-
|
|
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) => {
|
|
225
230
|
if (internals.dryRun) {
|
|
226
231
|
return;
|
|
227
232
|
}
|
|
228
233
|
|
|
229
234
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
230
|
-
const
|
|
235
|
+
const row = await pdriver._getKV(internals.migrationState, key);
|
|
236
|
+
const value = row ? JSON.parse(row.value) : {};
|
|
231
237
|
if (mutate(value) === false) {
|
|
232
238
|
return;
|
|
233
239
|
}
|
|
@@ -235,23 +241,26 @@ const changePurges = async (pdriver, internals, mutate) => {
|
|
|
235
241
|
const next = JSON.stringify(value);
|
|
236
242
|
if (!row) {
|
|
237
243
|
try {
|
|
238
|
-
await pdriver._insertKV(internals.migrationState,
|
|
244
|
+
await pdriver._insertKV(internals.migrationState, key, next);
|
|
239
245
|
} catch (err) {
|
|
240
|
-
log.verbose(
|
|
246
|
+
log.verbose(`[release] ${key} inserted concurrently`, err.message);
|
|
241
247
|
}
|
|
242
248
|
} else {
|
|
243
|
-
await pdriver._updateKVC(internals.migrationState,
|
|
249
|
+
await pdriver._updateKVC(internals.migrationState, key, next, 'value', row.value);
|
|
244
250
|
}
|
|
245
251
|
|
|
246
|
-
const after = await pdriver._getKV(internals.migrationState,
|
|
252
|
+
const after = await pdriver._getKV(internals.migrationState, key);
|
|
247
253
|
if (after && after.value === next) {
|
|
248
254
|
return;
|
|
249
255
|
}
|
|
250
256
|
}
|
|
251
257
|
|
|
252
|
-
throw new Error(
|
|
258
|
+
throw new Error(`[release] could not change ${key}, too much contention`);
|
|
253
259
|
};
|
|
254
260
|
|
|
261
|
+
const changePurges = (pdriver, internals, mutate) =>
|
|
262
|
+
changeState(pdriver, internals, PURGES, mutate);
|
|
263
|
+
|
|
255
264
|
/**
|
|
256
265
|
* Schedules purging the rows of a soft delete, { purge: true } takes the
|
|
257
266
|
* options of the project.
|
|
@@ -285,6 +294,77 @@ const forgetPurges = (pdriver, internals, t, prefix) =>
|
|
|
285
294
|
marks.forEach(mark => delete value[mark]);
|
|
286
295
|
});
|
|
287
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
|
+
|
|
288
368
|
const duePurges = async (pdriver, internals, index, releaseIndex) => {
|
|
289
369
|
const { value } = await purges(pdriver, internals);
|
|
290
370
|
|
|
@@ -326,6 +406,14 @@ const start = async (walker, label, index, releaseIndex, mode = 'up') => {
|
|
|
326
406
|
if (purged.length) {
|
|
327
407
|
await purge(walker, label, purged);
|
|
328
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
|
+
}
|
|
329
417
|
}
|
|
330
418
|
|
|
331
419
|
if (!renames.length && !drops.length) {
|
|
@@ -463,6 +551,19 @@ const assertRevertible = async (walker, label) => {
|
|
|
463
551
|
/**
|
|
464
552
|
* Warns about what is due for dropping with drop 'manual'.
|
|
465
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
|
+
|
|
466
567
|
const warnDuePurges = async (pdriver, internals, index, releaseIndex) => {
|
|
467
568
|
(await duePurges(pdriver, internals, index, releaseIndex))
|
|
468
569
|
.filter(e => e.drop === 'manual')
|
|
@@ -495,6 +596,12 @@ const warnDue = (internals, index, releaseIndex) => {
|
|
|
495
596
|
module.exports = {
|
|
496
597
|
PREFIX,
|
|
497
598
|
PURGES,
|
|
599
|
+
BACKUPS,
|
|
600
|
+
registerBackup,
|
|
601
|
+
forgetBackups,
|
|
602
|
+
backupsOf,
|
|
603
|
+
dropBackups,
|
|
604
|
+
warnDueBackups,
|
|
498
605
|
schedulePurge,
|
|
499
606
|
forgetPurges,
|
|
500
607
|
warnDuePurges,
|
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/template.js
CHANGED
|
@@ -105,8 +105,6 @@ Template.prototype.sqlFileLoaderTemplate = function () {
|
|
|
105
105
|
' return new Promise( function( resolve, reject ) {',
|
|
106
106
|
" fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){",
|
|
107
107
|
' if (err) return reject(err);',
|
|
108
|
-
" console.log('received data: ' + data);",
|
|
109
|
-
'',
|
|
110
108
|
' resolve(data);',
|
|
111
109
|
' });',
|
|
112
110
|
' })',
|
|
@@ -122,8 +120,6 @@ Template.prototype.sqlFileLoaderTemplate = function () {
|
|
|
122
120
|
' return new Promise( function( resolve, reject ) {',
|
|
123
121
|
" fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){",
|
|
124
122
|
' if (err) return reject(err);',
|
|
125
|
-
" console.log('received data: ' + data);",
|
|
126
|
-
'',
|
|
127
123
|
' resolve(data);',
|
|
128
124
|
' });',
|
|
129
125
|
' })',
|
|
@@ -171,8 +167,6 @@ Template.prototype.sqlFileLoaderIgnoreOnInitTemplate = function () {
|
|
|
171
167
|
' return new Promise( function( resolve, reject ) {',
|
|
172
168
|
" fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){",
|
|
173
169
|
' if (err) return reject(err);',
|
|
174
|
-
" console.log('received data: ' + data);",
|
|
175
|
-
'',
|
|
176
170
|
' resolve(data);',
|
|
177
171
|
' });',
|
|
178
172
|
' })',
|
|
@@ -181,7 +175,6 @@ Template.prototype.sqlFileLoaderIgnoreOnInitTemplate = function () {
|
|
|
181
175
|
' });',
|
|
182
176
|
' }',
|
|
183
177
|
' else {',
|
|
184
|
-
" console.log('ignoring on init: ' + filePath)",
|
|
185
178
|
' return null;',
|
|
186
179
|
' }',
|
|
187
180
|
'};',
|
|
@@ -193,8 +186,6 @@ Template.prototype.sqlFileLoaderIgnoreOnInitTemplate = function () {
|
|
|
193
186
|
' return new Promise( function( resolve, reject ) {',
|
|
194
187
|
" fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){",
|
|
195
188
|
' if (err) return reject(err);',
|
|
196
|
-
" console.log('received data: ' + data);",
|
|
197
|
-
'',
|
|
198
189
|
' resolve(data);',
|
|
199
190
|
' });',
|
|
200
191
|
' })',
|
package/lib/walker.js
CHANGED
|
@@ -48,6 +48,12 @@ const Walker = function (driver, directory, mode, intern, prefix, opts = {}) {
|
|
|
48
48
|
* c: schema
|
|
49
49
|
*/
|
|
50
50
|
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
51
|
+
// the state of an earlier run of the same instance of the API, e.g. a
|
|
52
|
+
// down leaves unlearn set, which would keep up from recording anything
|
|
53
|
+
this.internals.unlearn = false;
|
|
54
|
+
this.internals.rollback = false;
|
|
55
|
+
this.internals.rollbackContinue = false;
|
|
56
|
+
this.internals.learnFromScratch = false;
|
|
51
57
|
this.mode = mode;
|
|
52
58
|
|
|
53
59
|
if (!this.mode) this.prefix = `static-${prefix}`;
|
|
@@ -425,6 +431,12 @@ Walker.prototype = {
|
|
|
425
431
|
this._applied,
|
|
426
432
|
this._release.index
|
|
427
433
|
);
|
|
434
|
+
await Release.warnDueBackups(
|
|
435
|
+
this._pdriver,
|
|
436
|
+
this.internals,
|
|
437
|
+
this._applied,
|
|
438
|
+
this._release.index
|
|
439
|
+
);
|
|
428
440
|
}
|
|
429
441
|
},
|
|
430
442
|
|
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 => {
|