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 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
- if (scope) {
75
- internals.locTitle = scope;
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 = require(path.join(options.cwd, 'node_modules', plugins[i]));
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;
@@ -23,7 +23,10 @@ async function prepare (internals, config) {
23
23
  Migrator
24
24
  );
25
25
 
26
- migrator.directory = path.resolve(internals.argv['migrations-dir']);
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'),
@@ -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
- if (!process.env[level[configEntry].ENV]) {
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
- if (!process.env[config[env].ENV]) {
126
- log.verbose('Environment variable ' + config[env].ENV + ' is empty!');
127
- } else {
128
- out[env] = parseDatabaseUrl(process.env[config[env].ENV]);
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
- out.setCurrent(process.env[config.defaultEnv.ENV]);
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
- return new File(path.join(dir, result.name), internals);
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
- 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
 
@@ -221,13 +222,18 @@ const purges = async (pdriver, internals) => {
221
222
  return { row, value: row ? JSON.parse(row.value) : {} };
222
223
  };
223
224
 
224
- 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) => {
225
230
  if (internals.dryRun) {
226
231
  return;
227
232
  }
228
233
 
229
234
  for (let attempt = 0; attempt < 50; attempt++) {
230
- const { row, value } = await purges(pdriver, internals);
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, PURGES, next);
244
+ await pdriver._insertKV(internals.migrationState, key, next);
239
245
  } catch (err) {
240
- log.verbose('[release] purges inserted concurrently', err.message);
246
+ log.verbose(`[release] ${key} inserted concurrently`, err.message);
241
247
  }
242
248
  } else {
243
- await pdriver._updateKVC(internals.migrationState, PURGES, next, 'value', row.value);
249
+ await pdriver._updateKVC(internals.migrationState, key, next, 'value', row.value);
244
250
  }
245
251
 
246
- const after = await pdriver._getKV(internals.migrationState, PURGES);
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('[release] could not change the purges, too much contention');
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
- 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.6.0",
34
+ "version": "1.8.0",
35
35
  "bugs": {
36
36
  "url": "https://github.com/db-migrate/node-db-migrate/issues"
37
37
  },