db-migrate 1.8.0 → 1.8.1

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.
@@ -127,6 +127,10 @@ function run (internals, config) {
127
127
  }
128
128
  });
129
129
  options.watch = argv.watch === true;
130
+ // work:<scope>, the jobs of a scope with a database of its own
131
+ if (folder[1]) {
132
+ options.scope = folder[1];
133
+ }
130
134
 
131
135
  const worker = load('work')(internals, config, options);
132
136
  const stop = () => {
@@ -179,7 +179,23 @@ module.exports = function (internals, isModule) {
179
179
  }
180
180
  });
181
181
 
182
- internals.argv = deepExtend(internals.argv, rcConfig);
182
+ // the options given on the command line or to the API win over the rc
183
+ // files, those of the command line with all their aliases
184
+ var given = {};
185
+ if (isModule) {
186
+ given = Object.assign({}, internals.cmdOptions);
187
+ } else {
188
+ var defaulted = yargs.parsed.defaulted || {};
189
+ var aliases = yargs.parsed.aliases || {};
190
+ Object.keys(internals.argv).forEach(function (key) {
191
+ var names = [key].concat(aliases[key] || []);
192
+ if (key !== '_' && key !== '$0' && !names.some(n => defaulted[n])) {
193
+ given[key] = internals.argv[key];
194
+ }
195
+ });
196
+ }
197
+
198
+ internals.argv = deepExtend(internals.argv, rcConfig, given);
183
199
 
184
200
  // positional arguments, the API adds its own ones in module mode
185
201
  if (!Array.isArray(internals.argv._)) {
package/lib/dml.js CHANGED
@@ -101,6 +101,15 @@ const deleted = (file, op) => `|del:${file.name}#${op}`;
101
101
  // a LIKE pattern matching text literally, with ! as escape character
102
102
  const like = text => text.replace(/[!%_]/g, c => '!' + c);
103
103
 
104
+ /**
105
+ * The rows carrying the mark of a soft delete as a whole, followed by the
106
+ * end of the flag or by the next mark: |del:m#1 is no part of |del:m#10.
107
+ */
108
+ const markedBy = (ddl, mark) => ({
109
+ sql: `(${ddl(FLAG)} LIKE ? ESCAPE '!' OR ${ddl(FLAG)} LIKE ? ESCAPE '!')`,
110
+ params: [`%${like(mark)}`, `%${like(mark)}|%`]
111
+ });
112
+
104
113
  const isObject = x =>
105
114
  x !== null &&
106
115
  typeof x === 'object' &&
@@ -196,7 +205,9 @@ class Dml {
196
205
  this.recovery = recovery;
197
206
  this.progress = progress || foreground(pdriver, internals);
198
207
  this.work = work;
199
- this.transactional = meta(file).transactions !== false;
208
+ // --non-transactional turns them off for all migrations
209
+ this.transactional =
210
+ meta(file).transactions !== false && !internals.notransactions;
200
211
  this.op = 0;
201
212
  this.current = null;
202
213
 
@@ -247,6 +258,19 @@ class Dml {
247
258
  : `(${sql} || CAST(? AS TEXT))`;
248
259
  }
249
260
 
261
+ /**
262
+ * The flag without a mark, followed by the parameters '|' and the mark
263
+ * with a '|' after it, so only the mark as a whole is taken out. NULL
264
+ * once nothing is left.
265
+ */
266
+ unmarked () {
267
+ const replaced = `REPLACE(${this.concat(this.ddl(FLAG))}, ?, '|')`;
268
+
269
+ return this.driver._dbmDriver === 'mysql'
270
+ ? `NULLIF(TRIM(TRAILING '|' FROM ${replaced}), '')`
271
+ : `NULLIF(RTRIM(${replaced}, '|'), '')`;
272
+ }
273
+
250
274
  /**
251
275
  * Runs fn inside a transaction, rolled back if it fails.
252
276
  */
@@ -695,6 +719,24 @@ class Dml {
695
719
  );
696
720
  }
697
721
 
722
+ // MySQL keeps 255 characters of a flag, a mark cut off is never
723
+ // found again
724
+ if (this.driver._dbmDriver === 'mysql') {
725
+ const [{ n }] = await this.select(
726
+ `SELECT COUNT(*) AS n FROM ${this.ddl(table)} WHERE ${active.sql} ` +
727
+ `AND CHAR_LENGTH(COALESCE(${this.ddl(FLAG)}, '')) + ? > 255`,
728
+ active.params.concat([mark.length])
729
+ );
730
+
731
+ if (Number(n) > 0) {
732
+ throw new Error(
733
+ `delete("${table}") in soft mode can not mark ${n} row(s), ` +
734
+ `their ${FLAG} would be longer than the 255 characters ` +
735
+ 'MySQL keeps of it'
736
+ );
737
+ }
738
+ }
739
+
698
740
  if (purge) {
699
741
  await require('./release').schedulePurge(
700
742
  this.pdriver,
@@ -972,21 +1014,17 @@ const reverts = {
972
1014
  },
973
1015
 
974
1016
  soft: async (d, [table, column, key, mark]) => {
975
- await require('./release').forgetPurges(d.pdriver, d.internals, table, mark);
1017
+ await require('./release').forgetPurges(d.pdriver, d.internals, table, mark, true);
976
1018
  return d.untilDone(
977
1019
  table,
978
1020
  key,
979
- {
980
- sql: `${d.ddl(FLAG)} LIKE ? ESCAPE '!'`,
981
- params: [`%${like(mark)}%`]
982
- },
1021
+ markedBy(d.ddl.bind(d), mark),
983
1022
  {},
984
1023
  match =>
985
1024
  d.driver.runSql(
986
1025
  `UPDATE ${d.ddl(table)} SET ${d.ddl(column)} = NULL, ` +
987
- `${d.ddl(FLAG)} = NULLIF(REPLACE(${d.ddl(FLAG)}, ?, ''), '') ` +
988
- `WHERE ${match.sql}`,
989
- [mark].concat(match.params)
1026
+ `${d.ddl(FLAG)} = ${d.unmarked()} WHERE ${match.sql}`,
1027
+ ['|', `${mark}|`].concat(match.params)
990
1028
  )
991
1029
  );
992
1030
  }
@@ -1183,5 +1221,6 @@ module.exports = {
1183
1221
  Dml,
1184
1222
  Stopped,
1185
1223
  FLAG,
1224
+ markedBy,
1186
1225
  insertRows
1187
1226
  };
package/lib/jobs.js CHANGED
@@ -21,6 +21,7 @@ const State = require('./state');
21
21
  * in the foreground, see State
22
22
  * h: hash of the migration file when it started
23
23
  * err: why it failed
24
+ * sc: the scope the migration belongs to, missing for the top level
24
25
  *
25
26
  * Migrations take precedence over jobs. Whoever holds the migration lock
26
27
  * pauses the jobs before migrating: no job is taken anymore, the running
@@ -107,7 +108,8 @@ const register = async (pdriver, internals, file) => {
107
108
  jobs[file.name] = Object.assign(
108
109
  { step: 0, learned: 0, done: 0, rb: 0 },
109
110
  job,
110
- { s: 'queued', blocking, ID: 0, n: nonce(), err: undefined }
111
+ { s: 'queued', blocking, ID: 0, n: nonce(), err: undefined },
112
+ internals.matching ? { sc: internals.matching } : {}
111
113
  );
112
114
  });
113
115
 
@@ -119,6 +121,13 @@ const register = async (pdriver, internals, file) => {
119
121
  log.info(`[jobs] ${file.name} runs in the background`);
120
122
  };
121
123
 
124
+ /**
125
+ * The scope of a job, the one of internals for jobs registered before the
126
+ * scope was kept.
127
+ */
128
+ const scopeOf = (job, internals) =>
129
+ job && typeof job.sc === 'string' ? job.sc : internals.matching || '';
130
+
122
131
  /**
123
132
  * Whether the jobs are paused by a migration. A pause holds as long as its
124
133
  * holder holds the migration lock and keeps it alive, the lock row did not
@@ -299,4 +308,4 @@ const update = (pdriver, internals, name, id, mutate, lost = () => {}) =>
299
308
  job.n = nonce();
300
309
  });
301
310
 
302
- module.exports = { JOBS, WAIT, list, register, claim, update, paused, pause, resume, remove };
311
+ module.exports = { JOBS, WAIT, list, scopeOf, register, claim, update, paused, pause, resume, remove };
package/lib/release.js CHANGED
@@ -279,12 +279,14 @@ const schedulePurge = (pdriver, internals, mark, t, key, purge) => {
279
279
 
280
280
  /**
281
281
  * Forgets the purges done, of a soft delete or by db.purge, matching
282
- * the table and the prefix of the mark.
282
+ * the table and the prefix of the mark, or the mark itself when exact.
283
283
  */
284
- const forgetPurges = (pdriver, internals, t, prefix) =>
284
+ const forgetPurges = (pdriver, internals, t, prefix, exact = false) =>
285
285
  changePurges(pdriver, internals, value => {
286
286
  const marks = Object.keys(value).filter(
287
- mark => value[mark].t === t && mark.indexOf(prefix) === 0
287
+ mark =>
288
+ value[mark].t === t &&
289
+ (exact ? mark === prefix : mark.indexOf(prefix) === 0)
288
290
  );
289
291
 
290
292
  if (!marks.length) {
@@ -461,7 +463,7 @@ const purge = async (walker, label, due) => {
461
463
  const internals = walker.internals;
462
464
  const pdriver = walker._pdriver;
463
465
  const name = PREFIX + label;
464
- const { Dml, FLAG } = require('./dml');
466
+ const { Dml, markedBy } = require('./dml');
465
467
  const file = releaseFile(label, async () => {});
466
468
  const d = new Dml(walker, file, internals, pdriver, null, {
467
469
  step: async () => {},
@@ -475,7 +477,7 @@ const purge = async (walker, label, due) => {
475
477
  await d.untilDone(
476
478
  e.t,
477
479
  e.key,
478
- { sql: `${d.ddl(FLAG)} LIKE ? ESCAPE '!'`, params: [`%${e.mark.replace(/[!%_]/g, c => '!' + c)}%`] },
480
+ markedBy(d.ddl.bind(d), e.mark),
479
481
  {},
480
482
  match => walker._driver.runSql(`DELETE FROM ${d.ddl(e.t)} WHERE ${match.sql}`, match.params)
481
483
  );
package/lib/state.js CHANGED
@@ -324,6 +324,16 @@ module.exports = {
324
324
 
325
325
  log.info(`[state] Created a backup of ${internals.migrationState} by renaming table to ${newName}`);
326
326
  await driver._createKV(internals.migrationState);
327
+ // all but the schema, as without a backup: fix learns the schema and
328
+ // the records of v2 migrations, not the jobs, seeds, purges, backups
329
+ // and the records of dml migrations
330
+ const key = driver.escapeDDL('key');
331
+ const columns = `${key}, ${driver.escapeDDL('value')}, ${driver.escapeDDL('run_on')}`;
332
+ await driver.runSql(
333
+ `INSERT INTO ${driver.escapeDDL(internals.migrationState)} (${columns}) ` +
334
+ `SELECT ${columns} FROM ${driver.escapeDDL(newName)} WHERE ${key} <> ?`,
335
+ [SSTATE]
336
+ );
327
337
  await driver._insertKV(internals.migrationState, SSTATE, '{}');
328
338
  }
329
339
  }
package/lib/walker.js CHANGED
@@ -184,7 +184,12 @@ Walker.prototype = {
184
184
  }
185
185
 
186
186
  const jobs = await Jobs.list(this._pdriver, this.internals);
187
- const names = Object.keys(jobs);
187
+ // the jobs of this scope, scopes without a database of their own share
188
+ // the jobs with the top level
189
+ const scope = this.internals.matching || '';
190
+ const names = Object.keys(jobs).filter(
191
+ name => Jobs.scopeOf(jobs[name], this.internals) === scope
192
+ );
188
193
  if (!names.length) {
189
194
  return [];
190
195
  }
package/lib/work.js CHANGED
@@ -20,6 +20,8 @@ const Release = require('./release');
20
20
  * timeout: ms after which a job of a worker which stopped responding is
21
21
  * taken over, the heartbeat renews it every third of it
22
22
  * watch: keep looking for new jobs, instead of ending once none is left
23
+ * scope: the scope whose database the jobs are kept in, for a scope with a
24
+ * config.json of its own; the jobs of other scopes run in their scope
23
25
  *
24
26
  * Returns { done, stop }. done resolves with the jobs done and failed, once
25
27
  * no job is left, or after stop() was called. stop() lets the running jobs
@@ -50,7 +52,10 @@ const connect = async (internals, config) => {
50
52
  Migrator
51
53
  );
52
54
 
53
- walker.directory = path.resolve(internals.argv['migrations-dir']);
55
+ walker.directory = path.resolve(
56
+ internals.argv['migrations-dir'],
57
+ internals.locTitle || ''
58
+ );
54
59
  await walker.createMigrationsTable();
55
60
 
56
61
  if (!State.supportsLock(walker._pdriver)) {
@@ -86,6 +91,19 @@ const hashFile = file =>
86
91
  .update(require('fs').readFileSync(file.path))
87
92
  .digest('hex');
88
93
 
94
+ /**
95
+ * The record of the migration, reverting it deletes it.
96
+ */
97
+ const keepRecord = async (pdriver, internals, name) => {
98
+ const mig = await pdriver._getKV(internals.migrationState, name);
99
+
100
+ if (!mig) {
101
+ await pdriver._insertKV(internals.migrationState, name, '{}');
102
+ }
103
+
104
+ return mig;
105
+ };
106
+
89
107
  const runJob = async (walker, name, id, options, stopping, result, seen) => {
90
108
  const internals = walker.internals;
91
109
  const pdriver = walker._pdriver;
@@ -95,6 +113,12 @@ const runJob = async (walker, name, id, options, stopping, result, seen) => {
95
113
  lost = true;
96
114
  });
97
115
 
116
+ // the scope the job was registered in, the one of the worker for jobs
117
+ // registered before the scope was kept
118
+ const scope = Jobs.scopeOf((await Jobs.list(pdriver, internals))[name], internals);
119
+ internals.matching = scope;
120
+ walker.directory = path.resolve(internals.argv['migrations-dir'], scope);
121
+
98
122
  const files = await File.loadFromFileystem(
99
123
  walker.directory,
100
124
  walker.prefix,
@@ -116,6 +140,16 @@ const runJob = async (walker, name, id, options, stopping, result, seen) => {
116
140
  throw new Error(`The migration file of the job ${name} is missing`);
117
141
  }
118
142
 
143
+ // its worker died between recording the migration and forgetting the job
144
+ const recorded = await walker._driver.allLoadedMigrationsAsync();
145
+ if (recorded.some(r => r.name === `${scope}/${name}`)) {
146
+ await update((job, jobs) => {
147
+ delete jobs[name];
148
+ });
149
+ log.info(`[jobs] ${name} is done`);
150
+ return;
151
+ }
152
+
119
153
  const heartbeat = setInterval(() => {
120
154
  update(() => {}).catch(err => log.error(err.message));
121
155
  }, Math.max(Math.floor(options.timeout / 3), 1));
@@ -127,7 +161,7 @@ const runJob = async (walker, name, id, options, stopping, result, seen) => {
127
161
  const interrupted = interruption(jobs[name], hash);
128
162
 
129
163
  await State.reloadSchema(pdriver, internals);
130
- const mig = await pdriver._getKV(internals.migrationState, name);
164
+ const mig = await keepRecord(pdriver, internals, name);
131
165
  internals.modSchema = Object.assign(
132
166
  { i: {}, c: {}, f: {}, s: [] },
133
167
  mig ? JSON.parse(mig.value) : {}
@@ -171,10 +205,13 @@ const runJob = async (walker, name, id, options, stopping, result, seen) => {
171
205
  },
172
206
  {
173
207
  rollingBack: () => update(job => { job.rb = 1; }),
174
- restart: () =>
175
- update(job => {
208
+ restart: async () => {
209
+ // reverting deleted the record, the run starting over needs one
210
+ await keepRecord(pdriver, internals, name);
211
+ await update(job => {
176
212
  Object.assign(job, { step: 0, learned: 0, done: 0, rb: 0, h: hash });
177
- })
213
+ });
214
+ }
178
215
  }
179
216
  );
180
217
 
@@ -216,8 +253,51 @@ const runJob = async (walker, name, id, options, stopping, result, seen) => {
216
253
  }
217
254
  };
218
255
 
256
+ /**
257
+ * A dry run, which jobs would run.
258
+ */
259
+ const dryRun = async (internals, config) => {
260
+ const walker = await connect(internals, config);
261
+
262
+ try {
263
+ const jobs = await Jobs.list(walker._pdriver, internals);
264
+ const names = Object.keys(jobs)
265
+ .sort()
266
+ .filter(name => jobs[name].s !== 'failed');
267
+
268
+ names.forEach(name => log.info(`[jobs] ${name} would run`));
269
+ } finally {
270
+ await close(walker);
271
+ }
272
+
273
+ return { done: [], failed: [] };
274
+ };
275
+
219
276
  module.exports = function executeWork (internals, config, opts = {}) {
220
277
  const options = Object.assign({}, DEFAULTS, opts);
278
+
279
+ // the jobs of all scopes are kept with the top level, but those of the
280
+ // scopes with a database of their own
281
+ if (options.scope === 'all') {
282
+ const done = Promise.reject(
283
+ new Error(
284
+ 'work runs the jobs of one database, the top level with the scopes ' +
285
+ 'sharing it, or work:<scope> for a scope with a database of its own'
286
+ )
287
+ );
288
+ return { done, stop: () => done };
289
+ }
290
+
291
+ if (options.scope) {
292
+ internals.migrationMode = options.scope;
293
+ internals.matching = options.scope;
294
+ }
295
+
296
+ if (internals.dryRun) {
297
+ const done = dryRun(internals, config);
298
+ return { done, stop: () => done };
299
+ }
300
+
221
301
  const id = crypto.randomBytes(16).toString('base64');
222
302
  const seen = {};
223
303
  const result = { done: [], failed: [] };
package/package.json CHANGED
@@ -31,7 +31,7 @@
31
31
  "programmable",
32
32
  "api"
33
33
  ],
34
- "version": "1.8.0",
34
+ "version": "1.8.1",
35
35
  "bugs": {
36
36
  "url": "https://github.com/db-migrate/node-db-migrate/issues"
37
37
  },