db-migrate 1.6.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
@@ -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/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
 
@@ -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/walker.js CHANGED
@@ -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
 
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.7.0",
35
35
  "bugs": {
36
36
  "url": "https://github.com/db-migrate/node-db-migrate/issues"
37
37
  },