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.
- package/lib/commands/run.js +4 -0
- package/lib/commands/set-default-argv.js +17 -1
- package/lib/dml.js +48 -9
- package/lib/jobs.js +11 -2
- package/lib/release.js +7 -5
- package/lib/state.js +10 -0
- package/lib/walker.js +6 -1
- package/lib/work.js +85 -5
- package/package.json +1 -1
package/lib/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)} =
|
|
988
|
-
|
|
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 =>
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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: [] };
|