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 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'),
@@ -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
- if (d && d[t] !== undefined) {
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 = Math.floor(Date.now() / 1000);
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
- const changePurges = async (pdriver, internals, mutate) => {
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 { row, value } = await purges(pdriver, internals);
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, PURGES, next);
244
+ await pdriver._insertKV(internals.migrationState, key, next);
238
245
  } catch (err) {
239
- log.verbose('[release] purges inserted concurrently', err.message);
246
+ log.verbose(`[release] ${key} inserted concurrently`, err.message);
240
247
  }
241
248
  } else {
242
- await pdriver._updateKVC(internals.migrationState, PURGES, next, 'value', row.value);
249
+ await pdriver._updateKVC(internals.migrationState, key, next, 'value', row.value);
243
250
  }
244
251
 
245
- const after = await pdriver._getKV(internals.migrationState, PURGES);
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('[release] could not change the purges, too much contention');
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 (purged.length) {
323
- await purge(walker, label, purged);
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').up(unrecorded(walker), walker.driver, file);
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
- if (mig && mig.value !== '{}') {
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
- dbmUtil.filterDown(completedFiles, partialName, count).sort(sortFn)
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
- internals.releaseLabel = (Release.releases(files).map[name] || {}).label || null;
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 => {
package/package.json CHANGED
@@ -31,7 +31,7 @@
31
31
  "programmable",
32
32
  "api"
33
33
  ],
34
- "version": "1.5.0",
34
+ "version": "1.7.0",
35
35
  "bugs": {
36
36
  "url": "https://github.com/db-migrate/node-db-migrate/issues"
37
37
  },