db-migrate 1.0.0-beta.35 → 1.0.0-beta.37
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/connect.js +2 -1
- package/lib/chain.js +51 -0
- package/lib/commands/set-default-argv.js +14 -1
- package/lib/executors/versioned/v2.js +97 -3
- package/lib/learn.js +4 -1
- package/lib/methods/v2/statedone.js +16 -0
- package/lib/methods/v2/statetravel.js +17 -3
- package/lib/methods/v2/translatestate.js +4 -2
- package/lib/state.js +414 -115
- package/lib/walker.js +154 -111
- package/package.json +1 -1
- package/tttt.js +0 -2
package/connect.js
CHANGED
package/lib/chain.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const log = require('db-migrate-shared').log;
|
|
2
|
+
|
|
1
3
|
const DEFAULT = [
|
|
2
4
|
'renameCollection',
|
|
3
5
|
'dropCollection',
|
|
@@ -15,6 +17,21 @@ const DEFAULT = [
|
|
|
15
17
|
'removeForeignKey'
|
|
16
18
|
];
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Human readable form of an instruction for the log, like
|
|
22
|
+
* addColumn("users", "email")
|
|
23
|
+
*/
|
|
24
|
+
const describe = (method, args) =>
|
|
25
|
+
`${method}(${args
|
|
26
|
+
.filter(
|
|
27
|
+
a =>
|
|
28
|
+
typeof a === 'string' ||
|
|
29
|
+
typeof a === 'number' ||
|
|
30
|
+
(Array.isArray(a) && a.every(x => typeof x === 'string'))
|
|
31
|
+
)
|
|
32
|
+
.map(a => JSON.stringify(a))
|
|
33
|
+
.join(', ')})`;
|
|
34
|
+
|
|
18
35
|
let rollbackCounter = 0;
|
|
19
36
|
let rollbackSignal = null;
|
|
20
37
|
let signal = null;
|
|
@@ -82,6 +99,14 @@ class Chain {
|
|
|
82
99
|
this._step = null;
|
|
83
100
|
this._interface = null;
|
|
84
101
|
|
|
102
|
+
/**
|
|
103
|
+
* op: number of the current step, the instructions of a migration
|
|
104
|
+
* recovery: progress of an interrupted previous run to resume from,
|
|
105
|
+
* see State.startMigration
|
|
106
|
+
*/
|
|
107
|
+
this.op = 0;
|
|
108
|
+
this.recovery = null;
|
|
109
|
+
|
|
85
110
|
DEFAULT.concat(Object.keys(this.context.learnable || [])).forEach(
|
|
86
111
|
method => {
|
|
87
112
|
this[method] = function (...args) {
|
|
@@ -130,7 +155,33 @@ class Chain {
|
|
|
130
155
|
async exec (m, ...args) {
|
|
131
156
|
let ret;
|
|
132
157
|
const stat = [];
|
|
158
|
+
const op = ++this.op;
|
|
159
|
+
const recovery = this.recovery;
|
|
160
|
+
this.internals.migrationOp = op;
|
|
161
|
+
|
|
162
|
+
if (recovery && op <= recovery.done) {
|
|
163
|
+
log.info(
|
|
164
|
+
`[recovery] ${this.file.name}: skipping already executed step ` +
|
|
165
|
+
`${op}/${recovery.done} ${describe(m, args)}`
|
|
166
|
+
);
|
|
167
|
+
return ret;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// the step was interrupted after its reverse operation was recorded,
|
|
171
|
+
// but before it finished on the database
|
|
172
|
+
const learned = recovery !== null && op <= recovery.learned;
|
|
173
|
+
if (learned) {
|
|
174
|
+
log.info(
|
|
175
|
+
`[recovery] ${this.file.name}: executing interrupted step ${op} ` +
|
|
176
|
+
`${describe(m, args)} without learning it again`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
133
180
|
while (this.step()) {
|
|
181
|
+
if (learned && this._step.learns) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
134
185
|
ret = await this._interface[m].apply(this._interface, args);
|
|
135
186
|
stat.push(ret);
|
|
136
187
|
|
|
@@ -17,7 +17,9 @@ module.exports = function (internals, isModule) {
|
|
|
17
17
|
'vcseeder-dir': internals.cwd + '/VCSeeder',
|
|
18
18
|
'staticseeder-dir': internals.cwd + '/Seeder',
|
|
19
19
|
'ignore-completed-migrations': false,
|
|
20
|
-
'backup-state': false
|
|
20
|
+
'backup-state': false,
|
|
21
|
+
'lock-timeout': 60000,
|
|
22
|
+
'lock-interval': 1000
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
if (!isModule) {
|
|
@@ -116,6 +118,17 @@ module.exports = function (internals, isModule) {
|
|
|
116
118
|
.string('log-level')
|
|
117
119
|
.describe('backup-state', 'For use with fix, to take a backup of the current state before continuing.')
|
|
118
120
|
.boolean('backup-state')
|
|
121
|
+
.describe(
|
|
122
|
+
'lock-timeout',
|
|
123
|
+
'Milliseconds without any sign of life after which a migration lock' +
|
|
124
|
+
' held by another process is considered stale and taken over.'
|
|
125
|
+
)
|
|
126
|
+
.number('lock-timeout')
|
|
127
|
+
.describe(
|
|
128
|
+
'lock-interval',
|
|
129
|
+
'Milliseconds between checks while waiting for the migration lock.'
|
|
130
|
+
)
|
|
131
|
+
.number('lock-interval')
|
|
119
132
|
.parse();
|
|
120
133
|
} else {
|
|
121
134
|
internals.argv = Object.assign(defaultConfig, internals.cmdOptions);
|
|
@@ -8,10 +8,76 @@ const Chain = require('../../chain');
|
|
|
8
8
|
const StateTravel = require('../../methods/v2/statetravel')();
|
|
9
9
|
const StateNoStepTravel = require('../../methods/v2/statetravel')(false);
|
|
10
10
|
const Migrate = require('../../methods/v2/migrate');
|
|
11
|
+
const StateDone = require('../../methods/v2/statedone');
|
|
11
12
|
const TranslateState = require('../../methods/v2/translatestate');
|
|
12
13
|
const AddConventions = require('../../methods/v2/conventions');
|
|
13
14
|
const util = require('util');
|
|
14
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
|
+
const recoverByRollback = async (context, driver, file, interrupted) => {
|
|
65
|
+
const internals = context.internals;
|
|
66
|
+
const pdriver = context._pdriver;
|
|
67
|
+
|
|
68
|
+
await State.rollingBack(pdriver, internals);
|
|
69
|
+
internals.modSchema.s = internals.modSchema.s.filter(
|
|
70
|
+
entry => !(entry.n > interrupted.done)
|
|
71
|
+
);
|
|
72
|
+
await TranslateState(context._driver, file, driver, internals, pdriver);
|
|
73
|
+
|
|
74
|
+
internals.unlearn = false;
|
|
75
|
+
internals.rollback = false;
|
|
76
|
+
internals.rollbackContinue = false;
|
|
77
|
+
internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
78
|
+
await State.endMigration(pdriver, file, internals);
|
|
79
|
+
};
|
|
80
|
+
|
|
15
81
|
const execUnit = {
|
|
16
82
|
_extend: (context, type) => {
|
|
17
83
|
return {
|
|
@@ -38,8 +104,31 @@ const execUnit = {
|
|
|
38
104
|
chain.addChain(Learn);
|
|
39
105
|
chain.addChain(StateNoStepTravel);
|
|
40
106
|
chain.addChain(Migrate);
|
|
107
|
+
chain.addChain(StateDone);
|
|
108
|
+
|
|
109
|
+
const interrupted = await State.startMigration(
|
|
110
|
+
context._pdriver,
|
|
111
|
+
file,
|
|
112
|
+
context.internals,
|
|
113
|
+
{ recover: true }
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (interrupted) {
|
|
117
|
+
const mode = recoveryMode(file, interrupted);
|
|
118
|
+
log.warn(
|
|
119
|
+
`[recovery] ${file.name}: the previous run was interrupted at step ` +
|
|
120
|
+
`${interrupted.step}, ${interrupted.done} steps were executed, ` +
|
|
121
|
+
`recovering by ${mode}`
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (mode === 'rollback') {
|
|
125
|
+
await recoverByRollback(context, driver, file, interrupted);
|
|
126
|
+
await State.startMigration(context._pdriver, file, context.internals);
|
|
127
|
+
} else {
|
|
128
|
+
chain.recovery = interrupted;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
41
131
|
|
|
42
|
-
await State.startMigration(context._pdriver, file, context.internals);
|
|
43
132
|
// startMigration - needs secondary instance since we can not afford to
|
|
44
133
|
// loose state and the transaction start will include these for roll back
|
|
45
134
|
// we will disable them probably at all from DDL when the driver does not
|
|
@@ -60,6 +149,7 @@ const execUnit = {
|
|
|
60
149
|
'An error occured. No alternative failure strategy defined. Rolling back!',
|
|
61
150
|
err
|
|
62
151
|
);
|
|
152
|
+
await State.rollingBack(context._pdriver, context.internals);
|
|
63
153
|
await execUnit.down(context, driver, file, { abort: true });
|
|
64
154
|
throw err;
|
|
65
155
|
}
|
|
@@ -77,7 +167,9 @@ const execUnit = {
|
|
|
77
167
|
chain.addChain(Learn);
|
|
78
168
|
chain.addChain(StateTravel);
|
|
79
169
|
|
|
80
|
-
await State.startMigration(context._pdriver, file, context.internals
|
|
170
|
+
await State.startMigration(context._pdriver, file, context.internals, {
|
|
171
|
+
op: 'fix'
|
|
172
|
+
});
|
|
81
173
|
// startMigration - needs secondary instance since we can not afford to
|
|
82
174
|
// loose state and the transaction start will include these for roll back
|
|
83
175
|
// we will disable them probably at all from DDL when the driver does not
|
|
@@ -110,7 +202,9 @@ const execUnit = {
|
|
|
110
202
|
// if we get the abort signal this means we are in a rollback routine
|
|
111
203
|
// which means in turn we want to avoid reloading the state
|
|
112
204
|
if (abort !== true) {
|
|
113
|
-
await State.startMigration(context._pdriver, file, context.internals
|
|
205
|
+
await State.startMigration(context._pdriver, file, context.internals, {
|
|
206
|
+
op: 'down'
|
|
207
|
+
});
|
|
114
208
|
}
|
|
115
209
|
// start migration, see up comments
|
|
116
210
|
await TranslateState(context._driver, file, driver, context.internals, context._pdriver);
|
package/lib/learn.js
CHANGED
|
@@ -260,7 +260,7 @@ class STD {
|
|
|
260
260
|
|
|
261
261
|
addIndex (t, i, c, u) {
|
|
262
262
|
if (Array.isArray(c)) {
|
|
263
|
-
c.forEach(x => this.checkColumn(t, typeof x
|
|
263
|
+
c.forEach(x => this.checkColumn(t, typeof x === 'object' ? x.name : x));
|
|
264
264
|
} else {
|
|
265
265
|
this.checkColumn(t, c);
|
|
266
266
|
}
|
|
@@ -371,6 +371,9 @@ const noLearnError = prop => {
|
|
|
371
371
|
};
|
|
372
372
|
|
|
373
373
|
module.exports = {
|
|
374
|
+
// learns the schema and records the reverse operations
|
|
375
|
+
learns: true,
|
|
376
|
+
|
|
374
377
|
getInterface: (context, file, driver, internals) => {
|
|
375
378
|
if (context.learnable) {
|
|
376
379
|
const _std = new STD(internals, context);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const Shadow = require('../../driver/shadow');
|
|
2
|
+
const State = require('../../state');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Records a step as done, after it was executed on the database.
|
|
6
|
+
*/
|
|
7
|
+
module.exports = {
|
|
8
|
+
getInterface: (context, file, driver, internals, pdriver) => {
|
|
9
|
+
const done = () =>
|
|
10
|
+
function () {
|
|
11
|
+
return State.done(pdriver, internals.migrationOp, internals);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return Shadow.overshadow(driver, {}, done);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -17,24 +17,38 @@ const DEFAULT = {
|
|
|
17
17
|
removeForeignKey: 'addForeignKey'
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Persists the state around learning a step. Before learning (step) it
|
|
22
|
+
* records the step as started, afterwards it tags the recorded reverse
|
|
23
|
+
* operations with their step and records the step as learned.
|
|
24
|
+
*/
|
|
20
25
|
class StateTravel {
|
|
21
26
|
constructor (internals, file, driver, pdriver, step = true) {
|
|
22
27
|
this.file = file;
|
|
23
28
|
this.internals = internals;
|
|
24
29
|
this.driver = driver;
|
|
25
30
|
this.pdriver = pdriver;
|
|
26
|
-
this._counter = 0;
|
|
27
31
|
this._default = async function _default () {
|
|
32
|
+
const op = this.internals.migrationOp;
|
|
33
|
+
|
|
28
34
|
if (step) {
|
|
29
|
-
await State.step(this.pdriver,
|
|
35
|
+
await State.step(this.pdriver, op, this.internals);
|
|
36
|
+
} else {
|
|
37
|
+
this.internals.modSchema.s.forEach(entry => {
|
|
38
|
+
if (entry.n === undefined) entry.n = op;
|
|
39
|
+
});
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
await State.update(
|
|
33
43
|
this.pdriver,
|
|
34
44
|
this.file,
|
|
35
45
|
this.internals.modSchema,
|
|
36
46
|
this.internals
|
|
37
47
|
);
|
|
48
|
+
|
|
49
|
+
if (!step) {
|
|
50
|
+
await State.learned(this.pdriver, op, this.internals);
|
|
51
|
+
}
|
|
38
52
|
};
|
|
39
53
|
}
|
|
40
54
|
}
|
|
@@ -101,7 +101,7 @@ async function processEntry (
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
internals.modSchema.s.
|
|
104
|
+
internals.modSchema.s.pop();
|
|
105
105
|
await State.update(pdriver, file, internals.modSchema, internals);
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -113,7 +113,9 @@ module.exports = async (context, file, driver, internals, pdriver) => {
|
|
|
113
113
|
|
|
114
114
|
chain.addChain(Learn);
|
|
115
115
|
chain.addChain(Migrate);
|
|
116
|
-
|
|
116
|
+
// the stored records keep their order, so an interrupted rollback can
|
|
117
|
+
// be continued
|
|
118
|
+
await Promise.resolve(mod.s.slice().reverse()).each(args =>
|
|
117
119
|
processEntry(context, file, chain, internals, pdriver, args)
|
|
118
120
|
);
|
|
119
121
|
|
package/lib/state.js
CHANGED
|
@@ -1,68 +1,301 @@
|
|
|
1
1
|
const MSTATE = '__dbmigrate_state__';
|
|
2
2
|
const SSTATE = '__dbmigrate_schema__';
|
|
3
|
-
const RUNON = 'run_on';
|
|
4
3
|
|
|
5
4
|
const log = require('db-migrate-shared').log;
|
|
6
5
|
const fs = require('fs').promises;
|
|
7
6
|
const path = require('path');
|
|
8
7
|
const crypto = require('crypto');
|
|
8
|
+
const { performance } = require('perf_hooks');
|
|
9
9
|
|
|
10
10
|
const ID = crypto.randomBytes(32).toString('base64');
|
|
11
|
+
|
|
12
|
+
const LOCK_TIMEOUT = 60000;
|
|
13
|
+
const LOCK_INTERVAL = 1000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* active: a lock session is running, from acquire until release, even if
|
|
17
|
+
* the lock got lost in between
|
|
18
|
+
* owner: we currently hold the lock
|
|
19
|
+
* current: the last MSTATE row we wrote and verified, the expected
|
|
20
|
+
* value for our next compare and swap
|
|
21
|
+
* queue: serializes our own writes, so heartbeat and state updates
|
|
22
|
+
* never race each other on the same expected row
|
|
23
|
+
*/
|
|
24
|
+
let active = false;
|
|
11
25
|
let owner = false;
|
|
26
|
+
let current = null;
|
|
27
|
+
let heartbeat = null;
|
|
28
|
+
let queue = Promise.resolve();
|
|
29
|
+
|
|
30
|
+
const nonce = () => crypto.randomBytes(8).toString('hex');
|
|
31
|
+
|
|
32
|
+
const hashFile = async file => {
|
|
33
|
+
if (!file.path) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return crypto
|
|
39
|
+
.createHash('sha256')
|
|
40
|
+
.update(await fs.readFile(file.path))
|
|
41
|
+
.digest('hex');
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
48
|
+
|
|
49
|
+
const lockOptions = internals => {
|
|
50
|
+
const argv = internals.argv || {};
|
|
51
|
+
return {
|
|
52
|
+
timeout: Number(argv['lock-timeout']) || LOCK_TIMEOUT,
|
|
53
|
+
interval: Number(argv['lock-interval']) || LOCK_INTERVAL
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const read = (driver, internals) =>
|
|
58
|
+
driver._getKV(internals.migrationState, MSTATE);
|
|
59
|
+
|
|
60
|
+
const holder = row => JSON.parse(row.value).s.ID;
|
|
61
|
+
|
|
62
|
+
const heldByOther = row => {
|
|
63
|
+
const id = holder(row);
|
|
64
|
+
return !!id && id !== ID;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Atomic update of the lock row. The where clause contains the value we
|
|
69
|
+
* have read before, which carries the date and a nonce renewed on every
|
|
70
|
+
* write. If anybody else wrote in between, the update matches no row.
|
|
71
|
+
* Whoever wins, sees his own value on re-read.
|
|
72
|
+
*/
|
|
73
|
+
const cas = (driver, internals, expected, value) =>
|
|
74
|
+
driver._updateKVC(
|
|
75
|
+
internals.migrationState,
|
|
76
|
+
MSTATE,
|
|
77
|
+
value,
|
|
78
|
+
'value',
|
|
79
|
+
expected.value
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Concurrent CREATE TABLE IF NOT EXISTS can still collide (e.g. on pg_type
|
|
84
|
+
* in postgres) when several processes start on an empty database. The
|
|
85
|
+
* second attempt finds the table and is a noop.
|
|
86
|
+
*/
|
|
87
|
+
const createConcurrently = async create => {
|
|
88
|
+
try {
|
|
89
|
+
await create();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
log.verbose('[state] create failed, retrying once', err.message);
|
|
92
|
+
await create();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The stored schema starts as '{}', keep the structure the walker expects.
|
|
98
|
+
*/
|
|
99
|
+
const parseSchema = value =>
|
|
100
|
+
Object.assign({ i: {}, c: {}, f: {}, e: {} }, JSON.parse(value));
|
|
101
|
+
|
|
102
|
+
const stopHeartbeat = () => {
|
|
103
|
+
if (heartbeat) {
|
|
104
|
+
clearInterval(heartbeat);
|
|
105
|
+
heartbeat = null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const write = (driver, internals, mutate) => {
|
|
110
|
+
const run = queue.then(async () => {
|
|
111
|
+
if (!owner) {
|
|
112
|
+
throw new Error('[state] the migration lock is not held by this process');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const state = JSON.parse(current.value);
|
|
116
|
+
Object.assign(state.s, { ID, date: new Date(), n: nonce() });
|
|
117
|
+
mutate(state.s);
|
|
118
|
+
|
|
119
|
+
const value = JSON.stringify(state);
|
|
120
|
+
await cas(driver, internals, current, value);
|
|
121
|
+
const row = await read(driver, internals);
|
|
122
|
+
|
|
123
|
+
if (!row || row.value !== value) {
|
|
124
|
+
owner = false;
|
|
125
|
+
current = null;
|
|
126
|
+
stopHeartbeat();
|
|
127
|
+
throw new Error('[state] lost the migration lock to another process');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
current = row;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
queue = run.catch(() => {});
|
|
134
|
+
return run;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const claim = (row, driver, internals) => {
|
|
138
|
+
if (!row || holder(row) !== ID) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
active = true;
|
|
143
|
+
owner = true;
|
|
144
|
+
current = row;
|
|
145
|
+
|
|
146
|
+
const { timeout } = lockOptions(internals);
|
|
147
|
+
heartbeat = setInterval(() => {
|
|
148
|
+
write(driver, internals, () => {}).catch(err => log.error(err.message));
|
|
149
|
+
}, Math.max(Math.floor(timeout / 3), 1));
|
|
150
|
+
heartbeat.unref();
|
|
151
|
+
|
|
152
|
+
return true;
|
|
153
|
+
};
|
|
12
154
|
|
|
13
155
|
module.exports = {
|
|
156
|
+
createConcurrently,
|
|
157
|
+
|
|
158
|
+
// for tests, simulates a process which died while holding the lock
|
|
159
|
+
__stopHeartbeat: stopHeartbeat,
|
|
160
|
+
|
|
14
161
|
/**
|
|
15
|
-
*
|
|
16
|
-
* and
|
|
17
|
-
* It is important that all systems are in sync. We will use the
|
|
18
|
-
* database clock instead if possible.
|
|
162
|
+
* Drivers declare that their state methods were verified for locking,
|
|
163
|
+
* with an atomic _updateKVC and run_on set by the database clock.
|
|
19
164
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
165
|
+
supportsLock: function (driver) {
|
|
166
|
+
return !!(
|
|
167
|
+
driver &&
|
|
168
|
+
driver._meta &&
|
|
169
|
+
driver._meta.supports &&
|
|
170
|
+
driver._meta.supports.locking === true
|
|
171
|
+
);
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Try to acquire the migration lock once. Resolves to true if this process
|
|
176
|
+
* is the owner afterwards. A lock held by another process is only taken
|
|
177
|
+
* over, if it is the exact row waitForRelease reported as stale.
|
|
178
|
+
*/
|
|
179
|
+
acquire: async function (driver, internals, stale = null, retries = 3) {
|
|
180
|
+
if (owner) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const row = await read(driver, internals);
|
|
185
|
+
|
|
186
|
+
if (!row) {
|
|
187
|
+
try {
|
|
188
|
+
await driver._insertKV(
|
|
189
|
+
internals.migrationState,
|
|
190
|
+
MSTATE,
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
s: { step: 0, fin: 1, ID, date: new Date(), n: nonce() }
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// somebody else inserted the row first, the re-read decides
|
|
197
|
+
log.verbose('[state] lock insert failed', err.message);
|
|
43
198
|
}
|
|
44
199
|
|
|
45
|
-
|
|
200
|
+
return claim(await read(driver, internals), driver, internals);
|
|
201
|
+
}
|
|
46
202
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
203
|
+
if (heldByOther(row)) {
|
|
204
|
+
if (!stale || stale.value !== row.value) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { s } = JSON.parse(row.value);
|
|
209
|
+
log.warn(
|
|
210
|
+
`[state] taking over a stale migration lock from ${s.date}` +
|
|
211
|
+
(s.fin === 0
|
|
212
|
+
? `, the previous run was interrupted during ${
|
|
213
|
+
s.f ? `migration "${s.f}"` : 'a migration'
|
|
214
|
+
} at step ${s.step}`
|
|
215
|
+
: '')
|
|
54
216
|
);
|
|
217
|
+
}
|
|
55
218
|
|
|
56
|
-
|
|
219
|
+
const state = JSON.parse(row.value);
|
|
220
|
+
Object.assign(state.s, { ID, date: new Date(), n: nonce() });
|
|
221
|
+
await cas(driver, internals, row, JSON.stringify(state));
|
|
57
222
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
223
|
+
const after = await read(driver, internals);
|
|
224
|
+
if (claim(after, driver, internals)) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
61
227
|
|
|
62
|
-
|
|
63
|
-
|
|
228
|
+
// Nobody holds the lock, still our update did not apply. Either another
|
|
229
|
+
// process acquired and released in between, or the compare and swap does
|
|
230
|
+
// not work with this driver at all, which must not end in a busy loop.
|
|
231
|
+
if (after && !heldByOther(after)) {
|
|
232
|
+
if (retries > 0) {
|
|
233
|
+
return module.exports.acquire(driver, internals, null, retries - 1);
|
|
64
234
|
}
|
|
235
|
+
|
|
236
|
+
throw new Error(
|
|
237
|
+
'[state] could not acquire the migration lock although it is free, ' +
|
|
238
|
+
'the driver does not seem to support compare and swap on the state'
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return false;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Wait until the lock is released. A lock whose row did not change for
|
|
247
|
+
* lock-timeout ms is considered stale, its holder is gone. This is
|
|
248
|
+
* measured on our own monotonic clock, so no clocks have to be in sync.
|
|
249
|
+
*
|
|
250
|
+
* Resolves to the stale row, or null if the lock was released properly.
|
|
251
|
+
*/
|
|
252
|
+
waitForRelease: async function (driver, internals) {
|
|
253
|
+
const { timeout, interval } = lockOptions(internals);
|
|
254
|
+
let seen = null;
|
|
255
|
+
let since = 0;
|
|
256
|
+
|
|
257
|
+
for (;;) {
|
|
258
|
+
const row = await read(driver, internals);
|
|
259
|
+
|
|
260
|
+
if (!row || !heldByOther(row)) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const now = performance.now();
|
|
265
|
+
if (
|
|
266
|
+
!seen ||
|
|
267
|
+
seen.value !== row.value ||
|
|
268
|
+
String(seen.run_on) !== String(row.run_on)
|
|
269
|
+
) {
|
|
270
|
+
seen = row;
|
|
271
|
+
since = now;
|
|
272
|
+
} else if (now - since >= timeout) {
|
|
273
|
+
return row;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await delay(interval);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
release: async function (driver, internals) {
|
|
281
|
+
stopHeartbeat();
|
|
282
|
+
|
|
283
|
+
if (!owner) {
|
|
284
|
+
active = false;
|
|
285
|
+
return;
|
|
65
286
|
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await write(driver, internals, s => {
|
|
290
|
+
s.ID = 0;
|
|
291
|
+
});
|
|
292
|
+
} catch (err) {
|
|
293
|
+
log.warn(err.message);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
active = false;
|
|
297
|
+
owner = false;
|
|
298
|
+
current = null;
|
|
66
299
|
},
|
|
67
300
|
|
|
68
301
|
isOwner: function () {
|
|
@@ -70,7 +303,7 @@ module.exports = {
|
|
|
70
303
|
},
|
|
71
304
|
|
|
72
305
|
init: async function (driver, internals, { emptyState, backupState }) {
|
|
73
|
-
await driver._createKV(internals.migrationState);
|
|
306
|
+
await createConcurrently(() => driver._createKV(internals.migrationState));
|
|
74
307
|
const _schema = await driver._getKV(internals.migrationState, SSTATE);
|
|
75
308
|
|
|
76
309
|
if (_schema && backupState) {
|
|
@@ -91,42 +324,139 @@ module.exports = {
|
|
|
91
324
|
|
|
92
325
|
const schema = emptyState !== true ? _schema : null;
|
|
93
326
|
if (schema) {
|
|
94
|
-
internals.schema =
|
|
327
|
+
internals.schema = parseSchema(schema.value);
|
|
95
328
|
} else if (!emptyState) {
|
|
96
|
-
|
|
329
|
+
try {
|
|
330
|
+
await driver._insertKV(internals.migrationState, SSTATE, '{}');
|
|
331
|
+
} catch (err) {
|
|
332
|
+
// another process initialized the state at the same time
|
|
333
|
+
const row = await driver._getKV(internals.migrationState, SSTATE);
|
|
334
|
+
if (!row) {
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
internals.schema = parseSchema(row.value);
|
|
339
|
+
}
|
|
97
340
|
}
|
|
341
|
+
},
|
|
98
342
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Another process may have migrated while we were waiting for the lock,
|
|
345
|
+
* so the schema loaded during init is outdated.
|
|
346
|
+
*/
|
|
347
|
+
reloadSchema: async function (driver, internals) {
|
|
348
|
+
const schema = await driver._getKV(internals.migrationState, SSTATE);
|
|
349
|
+
if (schema) {
|
|
350
|
+
internals.schema = parseSchema(schema.value);
|
|
102
351
|
}
|
|
103
|
-
|
|
104
|
-
return Promise.resolve();
|
|
105
352
|
},
|
|
106
353
|
|
|
107
|
-
|
|
108
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Start a migration in the state. With recover, an interrupted previous
|
|
356
|
+
* run of the same migration is not reset but returned, so the caller can
|
|
357
|
+
* decide how to recover:
|
|
358
|
+
*
|
|
359
|
+
* step: the last step started
|
|
360
|
+
* learned: the last step whose reverse operation was recorded
|
|
361
|
+
* done: the last step executed on the database
|
|
362
|
+
* rollback: the run was interrupted while rolling back
|
|
363
|
+
* changed: the migration file changed since
|
|
364
|
+
*/
|
|
365
|
+
startMigration: async function (
|
|
366
|
+
driver,
|
|
367
|
+
file,
|
|
368
|
+
internals,
|
|
369
|
+
{ op = 'up', recover = false } = {}
|
|
370
|
+
) {
|
|
109
371
|
const mig = await driver._getKV(internals.migrationState, file.name);
|
|
110
372
|
|
|
111
373
|
if (mig && mig.value !== '{}') {
|
|
112
374
|
internals.modSchema = JSON.parse(mig.value);
|
|
375
|
+
// records of older runs, they belong to no step of this run
|
|
376
|
+
(internals.modSchema.s || []).forEach(entry => {
|
|
377
|
+
if (entry.n === undefined) entry.n = 0;
|
|
378
|
+
});
|
|
113
379
|
}
|
|
114
380
|
|
|
115
381
|
if (internals.dryRun) {
|
|
116
|
-
return
|
|
382
|
+
return null;
|
|
117
383
|
}
|
|
118
384
|
|
|
119
|
-
await
|
|
385
|
+
const h = await hashFile(file);
|
|
386
|
+
const row = await read(driver, internals);
|
|
387
|
+
const prev = row ? JSON.parse(row.value).s : null;
|
|
388
|
+
|
|
389
|
+
if (prev && prev.fin === 0 && prev.f) {
|
|
390
|
+
if (recover && prev.f === file.name && prev.o === 'up' && op === 'up') {
|
|
391
|
+
return {
|
|
392
|
+
step: prev.step || 0,
|
|
393
|
+
learned: prev.learned || 0,
|
|
394
|
+
done: prev.done || 0,
|
|
395
|
+
rollback: prev.rb === 1,
|
|
396
|
+
changed: !!prev.h && !!h && prev.h !== h
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
log.warn(
|
|
401
|
+
`[state] ignoring the interrupted ${prev.o} of migration ` +
|
|
402
|
+
`"${prev.f}" at step ${prev.step}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
await module.exports.progress(driver, internals, s => {
|
|
407
|
+
Object.assign(s, {
|
|
408
|
+
step: 0,
|
|
409
|
+
fin: 0,
|
|
410
|
+
f: file.name,
|
|
411
|
+
o: op,
|
|
412
|
+
learned: 0,
|
|
413
|
+
done: 0,
|
|
414
|
+
rb: 0,
|
|
415
|
+
h
|
|
416
|
+
});
|
|
417
|
+
});
|
|
120
418
|
|
|
121
419
|
if (!mig) {
|
|
122
|
-
|
|
420
|
+
await driver._insertKV(
|
|
123
421
|
internals.migrationState,
|
|
124
422
|
file.name,
|
|
125
423
|
JSON.stringify({})
|
|
126
424
|
);
|
|
127
|
-
} else {
|
|
128
|
-
return Promise.resolve();
|
|
129
425
|
}
|
|
426
|
+
|
|
427
|
+
return null;
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Write migration progress into the lock row. While holding the lock this
|
|
432
|
+
* is a compare and swap, which fails if we lost the lock. Without a lock
|
|
433
|
+
* (driver does not support it) the row is written unconditionally.
|
|
434
|
+
*/
|
|
435
|
+
progress: async function (driver, internals, mutate) {
|
|
436
|
+
if (active) {
|
|
437
|
+
return write(driver, internals, mutate);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const row = await read(driver, internals);
|
|
441
|
+
const state = row
|
|
442
|
+
? JSON.parse(row.value)
|
|
443
|
+
: { s: { step: 0, fin: 0, ID: 0 } };
|
|
444
|
+
state.s.date = new Date();
|
|
445
|
+
mutate(state.s);
|
|
446
|
+
|
|
447
|
+
if (!row) {
|
|
448
|
+
return driver._insertKV(
|
|
449
|
+
internals.migrationState,
|
|
450
|
+
MSTATE,
|
|
451
|
+
JSON.stringify(state)
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return driver._updateKV(
|
|
456
|
+
internals.migrationState,
|
|
457
|
+
MSTATE,
|
|
458
|
+
JSON.stringify(state)
|
|
459
|
+
);
|
|
130
460
|
},
|
|
131
461
|
|
|
132
462
|
update: async function (driver, file, state, internals) {
|
|
@@ -148,45 +478,34 @@ module.exports = {
|
|
|
148
478
|
return driver._getKV(internals.migrationState, file.name);
|
|
149
479
|
},
|
|
150
480
|
|
|
151
|
-
|
|
481
|
+
learned: async function (driver, step, internals) {
|
|
152
482
|
if (internals.dryRun) {
|
|
153
483
|
return Promise.resolve();
|
|
154
484
|
}
|
|
155
485
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
let runOn = state.run_on;
|
|
162
|
-
state = state.value;
|
|
163
|
-
state = JSON.parse(state);
|
|
486
|
+
return module.exports.progress(driver, internals, s => {
|
|
487
|
+
s.learned = step;
|
|
488
|
+
});
|
|
489
|
+
},
|
|
164
490
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
491
|
+
done: async function (driver, step, internals) {
|
|
492
|
+
if (internals.dryRun) {
|
|
493
|
+
return Promise.resolve();
|
|
168
494
|
}
|
|
169
495
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
internals.migrationState,
|
|
175
|
-
MSTATE,
|
|
176
|
-
JSON.stringify(state),
|
|
177
|
-
RUNON,
|
|
178
|
-
runOn
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
state = await driver._getKV(internals.migrationState, MSTATE);
|
|
182
|
-
|
|
183
|
-
runOn = state.run_on;
|
|
184
|
-
state = state.value;
|
|
185
|
-
state = JSON.parse(state);
|
|
496
|
+
return module.exports.progress(driver, internals, s => {
|
|
497
|
+
s.done = step;
|
|
498
|
+
});
|
|
499
|
+
},
|
|
186
500
|
|
|
187
|
-
|
|
188
|
-
|
|
501
|
+
rollingBack: async function (driver, internals) {
|
|
502
|
+
if (internals.dryRun) {
|
|
503
|
+
return Promise.resolve();
|
|
189
504
|
}
|
|
505
|
+
|
|
506
|
+
return module.exports.progress(driver, internals, s => {
|
|
507
|
+
s.rb = 1;
|
|
508
|
+
});
|
|
190
509
|
},
|
|
191
510
|
|
|
192
511
|
step: async function (driver, step, internals) {
|
|
@@ -195,18 +514,9 @@ module.exports = {
|
|
|
195
514
|
return Promise.resolve();
|
|
196
515
|
}
|
|
197
516
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
202
|
-
state = JSON.parse(state);
|
|
203
|
-
state.s.date = new Date();
|
|
204
|
-
state.s.step = step;
|
|
205
|
-
return driver._updateKV(
|
|
206
|
-
internals.migrationState,
|
|
207
|
-
MSTATE,
|
|
208
|
-
JSON.stringify(state)
|
|
209
|
-
);
|
|
517
|
+
return module.exports.progress(driver, internals, s => {
|
|
518
|
+
s.step = step;
|
|
519
|
+
});
|
|
210
520
|
},
|
|
211
521
|
|
|
212
522
|
deleteState: function (driver, file, internals) {
|
|
@@ -218,21 +528,10 @@ module.exports = {
|
|
|
218
528
|
return Promise.resolve();
|
|
219
529
|
}
|
|
220
530
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
state.s.date = new Date();
|
|
227
|
-
state.s.fin = 1;
|
|
228
|
-
|
|
229
|
-
// unlock from current process
|
|
230
|
-
state.s.ID = 0;
|
|
231
|
-
|
|
232
|
-
return driver._updateKV(
|
|
233
|
-
internals.migrationState,
|
|
234
|
-
MSTATE,
|
|
235
|
-
JSON.stringify(state)
|
|
236
|
-
);
|
|
531
|
+
// the lock itself stays until the walker releases it
|
|
532
|
+
return module.exports.progress(driver, internals, s => {
|
|
533
|
+
s.fin = 1;
|
|
534
|
+
s.rb = 0;
|
|
535
|
+
});
|
|
237
536
|
}
|
|
238
537
|
};
|
package/lib/walker.js
CHANGED
|
@@ -63,7 +63,9 @@ const Walker = function (driver, directory, mode, intern, prefix, opts = {}) {
|
|
|
63
63
|
Walker.prototype = {
|
|
64
64
|
createTables: async function (options) {
|
|
65
65
|
await State.init(this._pdriver, this.internals, options);
|
|
66
|
-
return
|
|
66
|
+
return State.createConcurrently(() =>
|
|
67
|
+
this._driver._createList(this.internals.migrationTable)
|
|
68
|
+
);
|
|
67
69
|
},
|
|
68
70
|
|
|
69
71
|
createMigrationsTable: function (options = {}) {
|
|
@@ -90,6 +92,52 @@ Walker.prototype = {
|
|
|
90
92
|
return this.createTables(options);
|
|
91
93
|
},
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Determine what to run first, only lock when there is actually something
|
|
97
|
+
* to run. If another process holds the lock, wait for its release and
|
|
98
|
+
* determine again, the other process might have done the work already.
|
|
99
|
+
* Once we hold the lock, state and pending migrations are read again.
|
|
100
|
+
*/
|
|
101
|
+
_withLock: async function (determine, run) {
|
|
102
|
+
if (this.prefix !== 'migration' || this.internals.dryRun) {
|
|
103
|
+
return run(await determine());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!State.supportsLock(this._pdriver)) {
|
|
107
|
+
log.warn(
|
|
108
|
+
this.title +
|
|
109
|
+
'Your database driver does not support migration locking, ' +
|
|
110
|
+
'concurrent processes are not prevented from migrating at the ' +
|
|
111
|
+
'same time. Update your driver to a version supporting it.'
|
|
112
|
+
);
|
|
113
|
+
return run(await determine());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let stale = null;
|
|
117
|
+
let toRun = await determine();
|
|
118
|
+
|
|
119
|
+
while (toRun.length > 0) {
|
|
120
|
+
if (await State.acquire(this._pdriver, this.internals, stale)) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
log.info(this.title + 'Waiting for the migration lock of another process');
|
|
125
|
+
stale = await State.waitForRelease(this._pdriver, this.internals);
|
|
126
|
+
toRun = await determine();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (toRun.length === 0) {
|
|
130
|
+
return run(toRun);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await State.reloadSchema(this._pdriver, this.internals);
|
|
135
|
+
return await run(await determine());
|
|
136
|
+
} finally {
|
|
137
|
+
await State.release(this._pdriver, this.internals);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
93
141
|
writeMigrationRecord: function (migration, callback) {
|
|
94
142
|
const onComplete = err => {
|
|
95
143
|
if (err) {
|
|
@@ -149,96 +197,84 @@ Walker.prototype = {
|
|
|
149
197
|
up: function (options, callback) {
|
|
150
198
|
const partialName = options.destination;
|
|
151
199
|
const count = options.count;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
allFiles,
|
|
163
|
-
completedFiles,
|
|
164
|
-
partialName,
|
|
165
|
-
count
|
|
200
|
+
const determine = () =>
|
|
201
|
+
Promise.all([
|
|
202
|
+
File.loadFromFileystem(this.directory, this.prefix, this.internals),
|
|
203
|
+
File.loadFromDatabase(
|
|
204
|
+
this.directory,
|
|
205
|
+
this.prefix,
|
|
206
|
+
this._driver,
|
|
207
|
+
this.internals
|
|
208
|
+
)
|
|
209
|
+
]).spread((allFiles, completedFiles) =>
|
|
210
|
+
dbmUtil.filterUp(allFiles, completedFiles, partialName, count)
|
|
166
211
|
);
|
|
167
212
|
|
|
168
|
-
if (toRun.length === 0) {
|
|
169
|
-
log.info(this.title + 'Nothing to run');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (this.internals.check) {
|
|
173
|
-
const toRunNames = toRun.map(migration => migration.name);
|
|
174
|
-
log.info(this.title + 'run:', toRunNames);
|
|
175
|
-
return toRunNames;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return toRun;
|
|
179
|
-
});
|
|
180
|
-
|
|
181
213
|
if (this.internals.check) {
|
|
182
|
-
return
|
|
214
|
+
return determine()
|
|
215
|
+
.then(toRun => this._checkResult(toRun))
|
|
216
|
+
.nodeify(callback);
|
|
183
217
|
}
|
|
184
218
|
|
|
185
|
-
return
|
|
186
|
-
.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return
|
|
192
|
-
this,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
219
|
+
return Promise.resolve(
|
|
220
|
+
this._withLock(determine, toRun => {
|
|
221
|
+
if (toRun.length === 0) {
|
|
222
|
+
log.info(this.title + 'Nothing to run');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return Promise.each(toRun, file => {
|
|
226
|
+
log.verbose(this.title + 'preparing to run up:', file.name);
|
|
227
|
+
const _meta = file.get()._meta || {};
|
|
228
|
+
const version = _meta.version || 1;
|
|
229
|
+
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
230
|
+
return require(`./executors/versioned/v${version}`).up(
|
|
231
|
+
this,
|
|
232
|
+
this.driver,
|
|
233
|
+
file
|
|
234
|
+
);
|
|
235
|
+
});
|
|
196
236
|
})
|
|
197
|
-
|
|
237
|
+
).nodeify(callback);
|
|
198
238
|
},
|
|
199
239
|
|
|
200
240
|
down: function (options, callback) {
|
|
201
241
|
const partialName = options.destination;
|
|
202
242
|
const count = options.count;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (toRun.length === 0) {
|
|
213
|
-
log.info(this.title + 'Nothing to run');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (this.internals.check) {
|
|
217
|
-
const toRunNames = toRun.map(migration => migration.name);
|
|
218
|
-
log.info(this.title + 'run:', toRunNames);
|
|
219
|
-
return toRunNames;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return toRun;
|
|
223
|
-
});
|
|
243
|
+
const determine = () =>
|
|
244
|
+
File.loadFromDatabase(
|
|
245
|
+
this.directory,
|
|
246
|
+
this.prefix,
|
|
247
|
+
this._driver,
|
|
248
|
+
this.internals
|
|
249
|
+
).then(completedFiles =>
|
|
250
|
+
dbmUtil.filterDown(completedFiles, partialName, count)
|
|
251
|
+
);
|
|
224
252
|
|
|
225
253
|
if (this.internals.check) {
|
|
226
|
-
return
|
|
254
|
+
return determine()
|
|
255
|
+
.then(toRun => this._checkResult(toRun))
|
|
256
|
+
.nodeify(callback);
|
|
227
257
|
}
|
|
228
258
|
|
|
229
|
-
return
|
|
230
|
-
.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return
|
|
236
|
-
this,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
259
|
+
return Promise.resolve(
|
|
260
|
+
this._withLock(determine, toRun => {
|
|
261
|
+
if (toRun.length === 0) {
|
|
262
|
+
log.info(this.title + 'Nothing to run');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return Promise.each(toRun, file => {
|
|
266
|
+
log.verbose(this.title + 'preparing to run down:', file.name);
|
|
267
|
+
const _meta = file.get()._meta || {};
|
|
268
|
+
const version = _meta.version || 1;
|
|
269
|
+
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
270
|
+
return require(`./executors/versioned/v${version}`).down(
|
|
271
|
+
this,
|
|
272
|
+
this.driver,
|
|
273
|
+
file
|
|
274
|
+
);
|
|
275
|
+
});
|
|
240
276
|
})
|
|
241
|
-
|
|
277
|
+
).nodeify(callback);
|
|
242
278
|
},
|
|
243
279
|
|
|
244
280
|
fix: function (options, callback) {
|
|
@@ -255,49 +291,56 @@ Walker.prototype = {
|
|
|
255
291
|
return a.localeCompare(b);
|
|
256
292
|
};
|
|
257
293
|
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
log.info(this.title + 'Nothing to run');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (this.internals.check) {
|
|
271
|
-
const toRunNames = toRun.map(migration => migration.name);
|
|
272
|
-
log.info(this.title + 'run:', toRunNames);
|
|
273
|
-
return toRunNames;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return toRun;
|
|
277
|
-
});
|
|
294
|
+
const determine = () =>
|
|
295
|
+
File.loadFromDatabase(
|
|
296
|
+
this.directory,
|
|
297
|
+
this.prefix,
|
|
298
|
+
this._driver,
|
|
299
|
+
this.internals
|
|
300
|
+
).then(completedFiles =>
|
|
301
|
+
dbmUtil.filterDown(completedFiles, partialName, count).sort(sortFn)
|
|
302
|
+
);
|
|
278
303
|
|
|
279
304
|
if (this.internals.check) {
|
|
280
|
-
return
|
|
305
|
+
return determine()
|
|
306
|
+
.then(toRun => this._checkResult(toRun))
|
|
307
|
+
.nodeify(callback);
|
|
281
308
|
}
|
|
282
309
|
|
|
283
|
-
return
|
|
284
|
-
.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const version = _meta.version || 1;
|
|
288
|
-
if (version < 2) {
|
|
289
|
-
log.warn(`${this.title} skipping "${file.name}" as v1 migrations do not maintain a schema`);
|
|
290
|
-
return Promise.resolve();
|
|
310
|
+
return Promise.resolve(
|
|
311
|
+
this._withLock(determine, toRun => {
|
|
312
|
+
if (toRun.length === 0) {
|
|
313
|
+
log.info(this.title + 'Nothing to run');
|
|
291
314
|
}
|
|
292
315
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
316
|
+
return Promise.each(toRun, file => {
|
|
317
|
+
log.verbose(this.title + 'preparing to run fix:', file.name);
|
|
318
|
+
const _meta = file.get()._meta || {};
|
|
319
|
+
const version = _meta.version || 1;
|
|
320
|
+
if (version < 2) {
|
|
321
|
+
log.warn(`${this.title} skipping "${file.name}" as v1 migrations do not maintain a schema`);
|
|
322
|
+
return Promise.resolve();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
326
|
+
return require(`./executors/versioned/v${version}`).fix(
|
|
327
|
+
this,
|
|
328
|
+
this.driver,
|
|
329
|
+
file
|
|
330
|
+
);
|
|
331
|
+
});
|
|
299
332
|
})
|
|
300
|
-
|
|
333
|
+
).nodeify(callback);
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
_checkResult: function (toRun) {
|
|
337
|
+
if (toRun.length === 0) {
|
|
338
|
+
log.info(this.title + 'Nothing to run');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const toRunNames = toRun.map(migration => migration.name);
|
|
342
|
+
log.info(this.title + 'run:', toRunNames);
|
|
343
|
+
return toRunNames;
|
|
301
344
|
},
|
|
302
345
|
|
|
303
346
|
check: function (options, callback) {
|
package/package.json
CHANGED
package/tttt.js
DELETED