db-migrate 1.2.0 → 1.4.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/README.md +9 -8
- package/api.js +27 -46
- package/lib/commands/generated.js +3 -1
- package/lib/commands/helper/seeder.js +29 -0
- package/lib/commands/run.js +11 -13
- package/lib/commands/seed.js +5 -9
- package/lib/commands/set-default-argv.js +4 -1
- package/lib/commands/undo-seed.js +5 -9
- package/lib/commands/work.js +33 -0
- package/lib/dml.js +1115 -0
- package/lib/driver/index.js +2 -0
- package/lib/executors/versioned/v2.js +18 -78
- package/lib/jobs.js +177 -0
- package/lib/recovery.js +83 -0
- package/lib/seed.js +199 -0
- package/lib/walker.js +51 -3
- package/lib/work.js +260 -0
- package/package.json +4 -4
package/lib/driver/index.js
CHANGED
|
@@ -166,6 +166,8 @@ exports.connect = function (config, intern, callback) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
trackSql(db);
|
|
169
|
+
// the dialect of parameters in all, $1 instead of ? for pg
|
|
170
|
+
db._dbmDriver = typeof config.driver === 'string' ? config.driver : null;
|
|
169
171
|
callback(null, db);
|
|
170
172
|
}
|
|
171
173
|
);
|
|
@@ -12,84 +12,8 @@ const StateDone = require('../../methods/v2/statedone');
|
|
|
12
12
|
const TranslateState = require('../../methods/v2/translatestate');
|
|
13
13
|
const AddConventions = require('../../methods/v2/conventions');
|
|
14
14
|
const util = require('util');
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* How to recover from an interrupted run of a migration, set per migration
|
|
18
|
-
* by _meta.recovery:
|
|
19
|
-
*
|
|
20
|
-
* skip: skip the steps already executed and continue with the rest
|
|
21
|
-
* rollback: revert the steps already executed and run the migration again
|
|
22
|
-
*/
|
|
23
|
-
const RECOVERY_MODES = ['skip', 'rollback'];
|
|
24
|
-
const RECOVERY_DEFAULT = 'skip';
|
|
25
|
-
|
|
26
|
-
const recoveryMode = (file, interrupted) => {
|
|
27
|
-
const name = file.name;
|
|
28
|
-
const mode = file.get()._meta.recovery || RECOVERY_DEFAULT;
|
|
29
|
-
|
|
30
|
-
if (RECOVERY_MODES.indexOf(mode) === -1) {
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Invalid recovery mode "${mode}" in migration "${name}", use one of ` +
|
|
33
|
-
RECOVERY_MODES.join(', ')
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (interrupted.rollback) {
|
|
38
|
-
if (mode !== 'rollback') {
|
|
39
|
-
log.warn(
|
|
40
|
-
`[recovery] ${name}: the previous run was interrupted while rolling ` +
|
|
41
|
-
'back, continuing the rollback'
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return 'rollback';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (mode === 'skip' && interrupted.changed) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
`Migration "${name}" was interrupted at step ${interrupted.step} and ` +
|
|
51
|
-
'changed since, so the executed steps can not be skipped safely. ' +
|
|
52
|
-
"Set _meta.recovery to 'rollback' to revert them, or repair the " +
|
|
53
|
-
'state manually.'
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return mode;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Revert the steps executed by an interrupted run. A step started but not
|
|
62
|
-
* done did not change the database, so its reverse operation is dropped.
|
|
63
|
-
*/
|
|
64
|
-
/**
|
|
65
|
-
* Keep the reverse operations of the steps executed on the database. The
|
|
66
|
-
* failed step only changed the database, if its main statement went through
|
|
67
|
-
* before failing, signaled by the driver (e.g. a foreign key of createTable
|
|
68
|
-
* failed after the table was created).
|
|
69
|
-
*/
|
|
70
|
-
const executedSteps = (internals, failed, signaled) => {
|
|
71
|
-
const done = internals.migrationDone;
|
|
72
|
-
|
|
73
|
-
internals.modSchema.s = internals.modSchema.s.filter(
|
|
74
|
-
entry =>
|
|
75
|
-
entry.n <= done || (signaled && failed > done && entry.n === failed)
|
|
76
|
-
);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Name the instruction a migration failed at, or after if the instruction
|
|
81
|
-
* itself finished and the migration code failed afterwards.
|
|
82
|
-
*/
|
|
83
|
-
const describeFailure = (err, chain, internals) => {
|
|
84
|
-
if (!err || typeof err !== 'object' || err.instruction || chain.op === 0) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const after = internals.migrationDone >= chain.op;
|
|
89
|
-
err.instruction = `${after ? 'after' : 'at'} step ${chain.op} ${
|
|
90
|
-
chain.current
|
|
91
|
-
}`;
|
|
92
|
-
};
|
|
15
|
+
const { recoveryMode, executedSteps, describeFailure } = require('../../recovery');
|
|
16
|
+
const Dml = require('../../dml');
|
|
93
17
|
|
|
94
18
|
const recoverByRollback = async (context, driver, file, interrupted) => {
|
|
95
19
|
const internals = context.internals;
|
|
@@ -125,6 +49,11 @@ const execUnit = {
|
|
|
125
49
|
},
|
|
126
50
|
|
|
127
51
|
up: async function (context, driver, file) {
|
|
52
|
+
Dml.checkType(file);
|
|
53
|
+
if (Dml.isDml(file)) {
|
|
54
|
+
return Dml.up(context, driver, file);
|
|
55
|
+
}
|
|
56
|
+
|
|
128
57
|
const _file = file.get();
|
|
129
58
|
const chain = new Chain(context._driver, file, driver, context.internals, context._pdriver);
|
|
130
59
|
if (!_file._meta.noDefaultColumn) {
|
|
@@ -205,6 +134,12 @@ const execUnit = {
|
|
|
205
134
|
},
|
|
206
135
|
|
|
207
136
|
fix: async function (context, driver, file) {
|
|
137
|
+
Dml.checkType(file);
|
|
138
|
+
if (Dml.isDml(file)) {
|
|
139
|
+
log.info(`[fix] skipping "${file.name}", dml migrations change no schema`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
208
143
|
const _file = file.get();
|
|
209
144
|
const chain = new Chain(context._driver, file, driver, context.internals, context._pdriver);
|
|
210
145
|
if (!_file._meta.noDefaultColumn) {
|
|
@@ -247,6 +182,11 @@ const execUnit = {
|
|
|
247
182
|
},
|
|
248
183
|
|
|
249
184
|
down: async function (context, driver, file, { abort } = {}) {
|
|
185
|
+
Dml.checkType(file);
|
|
186
|
+
if (Dml.isDml(file)) {
|
|
187
|
+
return Dml.down(context, driver, file, { abort });
|
|
188
|
+
}
|
|
189
|
+
|
|
250
190
|
// if we get the abort signal this means we are in a rollback routine
|
|
251
191
|
// which means in turn we want to avoid reloading the state
|
|
252
192
|
if (abort !== true) {
|
package/lib/jobs.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const log = require('db-migrate-shared').log;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Background migrations, dml migrations with _meta.background, are not run
|
|
8
|
+
* by up, but registered as jobs. Workers started by executeWork take the
|
|
9
|
+
* jobs one by one and run them. A migration counts as run once its job is
|
|
10
|
+
* done, until then it is running.
|
|
11
|
+
*
|
|
12
|
+
* All jobs live in one row of the state table, changed by compare and swap
|
|
13
|
+
* only, so any number of workers can work on them at once:
|
|
14
|
+
*
|
|
15
|
+
* s: queued, running or failed
|
|
16
|
+
* blocking: later jobs wait until this one is done
|
|
17
|
+
* ID: the worker running it, n: renewed by every write of the worker, a
|
|
18
|
+
* job whose n does not change for the timeout is taken over
|
|
19
|
+
* step, learned, done, rb: the progress of the run, like for migrations run
|
|
20
|
+
* in the foreground, see State
|
|
21
|
+
* h: hash of the migration file when it started
|
|
22
|
+
* err: why it failed
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const JOBS = '__dbmigrate_jobs__';
|
|
26
|
+
|
|
27
|
+
const nonce = () => crypto.randomBytes(8).toString('hex');
|
|
28
|
+
|
|
29
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
30
|
+
|
|
31
|
+
const read = async (pdriver, internals) => {
|
|
32
|
+
const row = await pdriver._getKV(internals.migrationState, JOBS);
|
|
33
|
+
return { row, jobs: row ? JSON.parse(row.value).jobs : {} };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const list = async (pdriver, internals) => (await read(pdriver, internals)).jobs;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Changes the jobs by mutate, retried until no other worker wrote in
|
|
40
|
+
* between. mutate returns false to leave them as they are.
|
|
41
|
+
*/
|
|
42
|
+
const change = async (pdriver, internals, mutate) => {
|
|
43
|
+
for (let attempt = 0; ; attempt++) {
|
|
44
|
+
const { row, jobs } = await read(pdriver, internals);
|
|
45
|
+
|
|
46
|
+
if (mutate(jobs) === false) {
|
|
47
|
+
return jobs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const value = JSON.stringify({ jobs, n: nonce() });
|
|
51
|
+
|
|
52
|
+
if (!row) {
|
|
53
|
+
try {
|
|
54
|
+
await pdriver._insertKV(internals.migrationState, JOBS, value);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// inserted by another worker first, try again with theirs
|
|
57
|
+
log.verbose('[jobs] insert failed', err.message);
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
await pdriver._updateKVC(
|
|
61
|
+
internals.migrationState,
|
|
62
|
+
JOBS,
|
|
63
|
+
value,
|
|
64
|
+
'value',
|
|
65
|
+
row.value
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const after = await pdriver._getKV(internals.migrationState, JOBS);
|
|
70
|
+
if (after && after.value === value) {
|
|
71
|
+
return jobs;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (attempt > 50) {
|
|
75
|
+
throw new Error('[jobs] could not change the jobs, too much contention');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await delay(Math.floor(Math.random() * 20 * Math.min(attempt + 1, 10)));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Registers a migration as job, or a failed one again. A job which failed
|
|
84
|
+
* with irreversible steps keeps its progress, so it continues after them.
|
|
85
|
+
*/
|
|
86
|
+
const register = async (pdriver, internals, file) => {
|
|
87
|
+
const blocking = (file.get()._meta || {}).blocking === true;
|
|
88
|
+
|
|
89
|
+
await change(pdriver, internals, jobs => {
|
|
90
|
+
const job = jobs[file.name];
|
|
91
|
+
|
|
92
|
+
if (job && job.s !== 'failed') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
jobs[file.name] = Object.assign(
|
|
97
|
+
{ step: 0, learned: 0, done: 0, rb: 0 },
|
|
98
|
+
job,
|
|
99
|
+
{ s: 'queued', blocking, ID: 0, n: nonce(), err: undefined }
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const mig = await pdriver._getKV(internals.migrationState, file.name);
|
|
104
|
+
if (!mig) {
|
|
105
|
+
await pdriver._insertKV(internals.migrationState, file.name, '{}');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
log.info(`[jobs] ${file.name} runs in the background`);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Takes the first job to run, in the order of their names. A running job is
|
|
113
|
+
* taken over if it did not change for timeout ms, measured on our clock in
|
|
114
|
+
* seen. Jobs after a blocking job wait until it is done.
|
|
115
|
+
*
|
|
116
|
+
* Resolves to the name of the job taken, or null.
|
|
117
|
+
*/
|
|
118
|
+
const claim = async (pdriver, internals, id, seen, timeout) => {
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
let taken = null;
|
|
121
|
+
|
|
122
|
+
await change(pdriver, internals, jobs => {
|
|
123
|
+
taken = null;
|
|
124
|
+
|
|
125
|
+
for (const name of Object.keys(jobs).sort()) {
|
|
126
|
+
const job = jobs[name];
|
|
127
|
+
let free = job.s === 'queued';
|
|
128
|
+
|
|
129
|
+
if (job.s === 'running' && job.ID !== id) {
|
|
130
|
+
const last = seen[name];
|
|
131
|
+
|
|
132
|
+
if (!last || last.n !== job.n) {
|
|
133
|
+
seen[name] = { n: job.n, since: now };
|
|
134
|
+
} else if (now - last.since >= timeout) {
|
|
135
|
+
log.warn(
|
|
136
|
+
`[jobs] taking over ${name}, its worker did not respond for ` +
|
|
137
|
+
`${timeout} ms`
|
|
138
|
+
);
|
|
139
|
+
free = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (free) {
|
|
144
|
+
Object.assign(job, { s: 'running', ID: id, n: nonce() });
|
|
145
|
+
taken = name;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (job.blocking) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return false;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return taken;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Changes a job we run. If another worker took it over in between, lost is
|
|
162
|
+
* called and nothing is changed.
|
|
163
|
+
*/
|
|
164
|
+
const update = (pdriver, internals, name, id, mutate, lost = () => {}) =>
|
|
165
|
+
change(pdriver, internals, jobs => {
|
|
166
|
+
const job = jobs[name];
|
|
167
|
+
|
|
168
|
+
if (!job || job.ID !== id) {
|
|
169
|
+
lost();
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
mutate(job, jobs);
|
|
174
|
+
job.n = nonce();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
module.exports = { JOBS, list, register, claim, update };
|
package/lib/recovery.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const log = require('db-migrate-shared').log;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How to recover from an interrupted run of a migration, set per migration
|
|
7
|
+
* by _meta.recovery:
|
|
8
|
+
*
|
|
9
|
+
* skip: skip the steps already executed and continue with the rest
|
|
10
|
+
* rollback: revert the steps already executed and run the migration again
|
|
11
|
+
*/
|
|
12
|
+
const RECOVERY_MODES = ['skip', 'rollback'];
|
|
13
|
+
const RECOVERY_DEFAULT = 'skip';
|
|
14
|
+
|
|
15
|
+
const recoveryMode = (file, interrupted) => {
|
|
16
|
+
const name = file.name;
|
|
17
|
+
const mode = file.get()._meta.recovery || RECOVERY_DEFAULT;
|
|
18
|
+
|
|
19
|
+
if (RECOVERY_MODES.indexOf(mode) === -1) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Invalid recovery mode "${mode}" in migration "${name}", use one of ` +
|
|
22
|
+
RECOVERY_MODES.join(', ')
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (interrupted.rollback) {
|
|
27
|
+
if (mode !== 'rollback') {
|
|
28
|
+
log.warn(
|
|
29
|
+
`[recovery] ${name}: the previous run was interrupted while rolling ` +
|
|
30
|
+
'back, continuing the rollback'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return 'rollback';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (mode === 'skip' && interrupted.changed) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Migration "${name}" was interrupted at step ${interrupted.step} and ` +
|
|
40
|
+
'changed since, so the executed steps can not be skipped safely. ' +
|
|
41
|
+
"Set _meta.recovery to 'rollback' to revert them, or repair the " +
|
|
42
|
+
'state manually.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return mode;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Revert the steps executed by an interrupted run. A step started but not
|
|
51
|
+
* done did not change the database, so its reverse operation is dropped.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Keep the reverse operations of the steps executed on the database. The
|
|
55
|
+
* failed step only changed the database, if its main statement went through
|
|
56
|
+
* before failing, signaled by the driver (e.g. a foreign key of createTable
|
|
57
|
+
* failed after the table was created).
|
|
58
|
+
*/
|
|
59
|
+
const executedSteps = (internals, failed, signaled) => {
|
|
60
|
+
const done = internals.migrationDone;
|
|
61
|
+
|
|
62
|
+
internals.modSchema.s = internals.modSchema.s.filter(
|
|
63
|
+
entry =>
|
|
64
|
+
entry.n <= done || (signaled && failed > done && entry.n === failed)
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Name the instruction a migration failed at, or after if the instruction
|
|
70
|
+
* itself finished and the migration code failed afterwards.
|
|
71
|
+
*/
|
|
72
|
+
const describeFailure = (err, chain, internals) => {
|
|
73
|
+
if (!err || typeof err !== 'object' || err.instruction || chain.op === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const after = internals.migrationDone >= chain.op;
|
|
78
|
+
err.instruction = `${after ? 'after' : 'at'} step ${chain.op} ${
|
|
79
|
+
chain.current
|
|
80
|
+
}`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
module.exports = { RECOVERY_MODES, recoveryMode, executedSteps, describeFailure };
|
package/lib/seed.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const Promise = require('bluebird');
|
|
6
|
+
const log = require('db-migrate-shared').log;
|
|
7
|
+
const { FLAG, insertRows } = require('./dml');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Static seeds insert data for development and tests, seeds/<name>.js:
|
|
11
|
+
*
|
|
12
|
+
* exports.seed = async db => {
|
|
13
|
+
* await db.insert('pets', [{ name: 'Rex' }, { name: 'Tom' }]);
|
|
14
|
+
* };
|
|
15
|
+
*
|
|
16
|
+
* The rows are flagged with seed:<name> in their __dbmigrate__flag column.
|
|
17
|
+
* Seeding again removes the rows of the previous run first, so seeds can be
|
|
18
|
+
* changed and run any time. The tables each seed inserted into are kept in
|
|
19
|
+
* the state, under SEEDS, in the order the seeds ran.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const SEEDS = '__dbmigrate_seeds__';
|
|
23
|
+
|
|
24
|
+
const flag = name => `seed:${name}`;
|
|
25
|
+
|
|
26
|
+
const loadState = async (pdriver, internals) => {
|
|
27
|
+
const row = await pdriver._getKV(internals.migrationState, SEEDS);
|
|
28
|
+
return { exists: !!row, seeds: row ? JSON.parse(row.value) : {} };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const saveState = (pdriver, internals, state) => {
|
|
32
|
+
if (internals.dryRun) {
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const value = JSON.stringify(state.seeds);
|
|
37
|
+
|
|
38
|
+
if (state.exists) {
|
|
39
|
+
return pdriver._updateKV(internals.migrationState, SEEDS, value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
state.exists = true;
|
|
43
|
+
return pdriver._insertKV(internals.migrationState, SEEDS, value);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const columns = (internals, table) => {
|
|
47
|
+
const t = internals.schema.c[table];
|
|
48
|
+
return t ? t.columns || t : null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
class SeedDb {
|
|
52
|
+
constructor (driver, internals, name, touch) {
|
|
53
|
+
this.driver = driver;
|
|
54
|
+
this.internals = internals;
|
|
55
|
+
this.name = name;
|
|
56
|
+
this.touch = touch;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async insert (table, ...args) {
|
|
60
|
+
const { rows } = insertRows(args);
|
|
61
|
+
const known = columns(this.internals, table);
|
|
62
|
+
|
|
63
|
+
if (!known || !known[FLAG]) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Seed "${this.name}" can not insert into "${table}", seeds insert ` +
|
|
66
|
+
`into tables created by v2 migrations only, by their ${FLAG} ` +
|
|
67
|
+
'column, so the rows can be removed again.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!rows.length) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await this.touch(table);
|
|
76
|
+
return this.driver.insert(
|
|
77
|
+
table,
|
|
78
|
+
rows.map(row => Object.assign({}, row, { [FLAG]: flag(this.name) }))
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
all (...args) {
|
|
83
|
+
return this.driver.all(...args);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Removes the rows of a seed, the tables in reverse order, for rows
|
|
89
|
+
* referencing rows of the same seed. Tables dropped since are skipped.
|
|
90
|
+
*/
|
|
91
|
+
const remove = async (driver, internals, state, name) => {
|
|
92
|
+
const tables = (state.seeds[name] || []).slice().reverse();
|
|
93
|
+
|
|
94
|
+
await Promise.each(tables, table => {
|
|
95
|
+
if (!columns(internals, table)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return driver.runSql(
|
|
100
|
+
`DELETE FROM ${driver.escapeDDL(table)} WHERE ${driver.escapeDDL(FLAG)} = ?`,
|
|
101
|
+
[flag(name)]
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
delete state.seeds[name];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const files = dir => {
|
|
109
|
+
if (!fs.existsSync(dir)) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return fs
|
|
114
|
+
.readdirSync(dir)
|
|
115
|
+
.filter(file => /\.js$/.test(file))
|
|
116
|
+
.sort()
|
|
117
|
+
.map(file => ({
|
|
118
|
+
name: file.replace(/\.js$/, ''),
|
|
119
|
+
path: path.join(dir, file)
|
|
120
|
+
}));
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const select = (list, name, what) => {
|
|
124
|
+
if (!name) {
|
|
125
|
+
return list;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const found = list.filter(seed => seed.name === name);
|
|
129
|
+
if (!found.length) {
|
|
130
|
+
throw new Error(`There is no ${what} "${name}"`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return found;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Seeds all seeds of the directory, or the one named. The rows of the
|
|
138
|
+
* seeds run before are removed first, of all seeds the last run first,
|
|
139
|
+
* including seeds whose files were removed since.
|
|
140
|
+
*/
|
|
141
|
+
const run = async (walker, name) => {
|
|
142
|
+
const internals = walker.internals;
|
|
143
|
+
const driver = walker._driver;
|
|
144
|
+
const pdriver = walker._pdriver;
|
|
145
|
+
const dir = path.resolve(internals.argv['seeds-dir']);
|
|
146
|
+
const seeds = select(files(dir), name, 'seed');
|
|
147
|
+
const state = await loadState(pdriver, internals);
|
|
148
|
+
|
|
149
|
+
const previous = name ? [name] : Object.keys(state.seeds).reverse();
|
|
150
|
+
await Promise.each(previous, seed => remove(driver, internals, state, seed));
|
|
151
|
+
await saveState(pdriver, internals, state);
|
|
152
|
+
|
|
153
|
+
await Promise.each(seeds, async seed => {
|
|
154
|
+
log.info(`[seed] ${seed.name}`);
|
|
155
|
+
delete require.cache[require.resolve(seed.path)];
|
|
156
|
+
const mod = require(seed.path);
|
|
157
|
+
|
|
158
|
+
if (typeof mod.seed !== 'function') {
|
|
159
|
+
throw new Error(`The seed "${seed.name}" exports no seed function`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
state.seeds[seed.name] = [];
|
|
163
|
+
const touch = async table => {
|
|
164
|
+
const tables = state.seeds[seed.name];
|
|
165
|
+
if (tables.indexOf(table) === -1) {
|
|
166
|
+
// recorded before inserting, so a failing seed is removed as well
|
|
167
|
+
tables.push(table);
|
|
168
|
+
await saveState(pdriver, internals, state);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
await mod.seed(new SeedDb(driver, internals, seed.name, touch), {
|
|
173
|
+
options: internals.safeOptions,
|
|
174
|
+
dbm: internals.safeOptions.dbmigrate
|
|
175
|
+
});
|
|
176
|
+
await saveState(pdriver, internals, state);
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Removes the rows of all seeds, or of the one named.
|
|
182
|
+
*/
|
|
183
|
+
const undo = async (walker, name) => {
|
|
184
|
+
const internals = walker.internals;
|
|
185
|
+
const state = await loadState(walker._pdriver, internals);
|
|
186
|
+
const seeds = name
|
|
187
|
+
? select(Object.keys(state.seeds).map(n => ({ name: n })), name, 'seeded seed')
|
|
188
|
+
: Object.keys(state.seeds)
|
|
189
|
+
.reverse()
|
|
190
|
+
.map(n => ({ name: n }));
|
|
191
|
+
|
|
192
|
+
await Promise.each(seeds, seed => {
|
|
193
|
+
log.info(`[seed] removing ${seed.name}`);
|
|
194
|
+
return remove(walker._driver, internals, state, seed.name);
|
|
195
|
+
});
|
|
196
|
+
await saveState(walker._pdriver, internals, state);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
module.exports = { run, undo, SEEDS };
|
package/lib/walker.js
CHANGED
|
@@ -5,6 +5,8 @@ const State = require('./state');
|
|
|
5
5
|
const log = require('db-migrate-shared').log;
|
|
6
6
|
const Promise = require('bluebird');
|
|
7
7
|
const File = require('./file.js');
|
|
8
|
+
const Dml = require('./dml');
|
|
9
|
+
const Jobs = require('./jobs');
|
|
8
10
|
|
|
9
11
|
const INTERFACES = {
|
|
10
12
|
migration: require('./interface/migratorInterface.js'),
|
|
@@ -133,6 +135,46 @@ Walker.prototype = {
|
|
|
133
135
|
}
|
|
134
136
|
},
|
|
135
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Migrations running in the background are left out, until their job is
|
|
140
|
+
* done. Failed ones are run, which registers them again.
|
|
141
|
+
*/
|
|
142
|
+
_withoutJobs: async function (toRun) {
|
|
143
|
+
if (this.prefix !== 'migration' || !this._pdriver || typeof this._pdriver._getKV !== 'function') {
|
|
144
|
+
return toRun;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const jobs = await Jobs.list(this._pdriver, this.internals);
|
|
148
|
+
|
|
149
|
+
return toRun.filter(file => {
|
|
150
|
+
const job = jobs[file.name];
|
|
151
|
+
|
|
152
|
+
if (job && job.s !== 'failed') {
|
|
153
|
+
log.info(`${this.title}${file.name} is running in the background`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
_registerJob: async function (file) {
|
|
162
|
+
Dml.checkType(file);
|
|
163
|
+
|
|
164
|
+
if (this.internals.dryRun) {
|
|
165
|
+
log.info(`${this.title}${file.name} would run in the background`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!State.supportsLock(this._pdriver)) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
'Background migrations need a driver supporting the migration lock'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Jobs.register(this._pdriver, this.internals, file);
|
|
176
|
+
},
|
|
177
|
+
|
|
136
178
|
writeMigrationRecord: function (migration, callback) {
|
|
137
179
|
const onComplete = err => {
|
|
138
180
|
if (err) {
|
|
@@ -201,9 +243,11 @@ Walker.prototype = {
|
|
|
201
243
|
this._driver,
|
|
202
244
|
this.internals
|
|
203
245
|
)
|
|
204
|
-
])
|
|
205
|
-
|
|
206
|
-
|
|
246
|
+
])
|
|
247
|
+
.spread((allFiles, completedFiles) =>
|
|
248
|
+
dbmUtil.filterUp(allFiles, completedFiles, partialName, count)
|
|
249
|
+
)
|
|
250
|
+
.then(toRun => this._withoutJobs(toRun));
|
|
207
251
|
|
|
208
252
|
if (this.internals.check) {
|
|
209
253
|
return determine()
|
|
@@ -219,6 +263,10 @@ Walker.prototype = {
|
|
|
219
263
|
|
|
220
264
|
return Promise.each(toRun, file => {
|
|
221
265
|
log.verbose(this.title + 'preparing to run up:', file.name);
|
|
266
|
+
if (Dml.isBackground(file)) {
|
|
267
|
+
return this._registerJob(file).catch(failedIn(file));
|
|
268
|
+
}
|
|
269
|
+
|
|
222
270
|
const _meta = file.get()._meta || {};
|
|
223
271
|
const version = _meta.version || 1;
|
|
224
272
|
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|