db-migrate 1.3.0 → 1.4.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/api.js +9 -0
- package/lib/commands/generated.js +2 -1
- package/lib/commands/work.js +33 -0
- package/lib/dml.js +463 -101
- package/lib/driver/index.js +2 -0
- package/lib/jobs.js +302 -0
- package/lib/state.js +6 -0
- package/lib/walker.js +118 -5
- package/lib/work.js +274 -0
- package/package.json +2 -2
package/lib/dml.js
CHANGED
|
@@ -16,15 +16,22 @@ const { recoveryMode, describeFailure } = require('./recovery');
|
|
|
16
16
|
* first, reverting restores them from there. The rows are then
|
|
17
17
|
* changed in batches by their key, the progress is kept in the
|
|
18
18
|
* state, so an interrupted run continues where it stopped.
|
|
19
|
+
* delete in soft mode: the rows are not deleted, but get a timestamp in a
|
|
20
|
+
* column of the user and a mark in their flag, reverting removes
|
|
21
|
+
* both again. purge deletes such rows for good.
|
|
19
22
|
* runSql: reverted by the SQL given with { revert }
|
|
20
23
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
24
|
+
* Each step, and each batch of a step, runs inside a transaction, unless
|
|
25
|
+
* the migration opts out with _meta.transactions = false.
|
|
26
|
+
*
|
|
27
|
+
* The records are { t: 4, a: action, c: args }, or { t: 3 } for a step which
|
|
28
|
+
* can not be reverted, run with { irreversible: true } or a purge.
|
|
23
29
|
*/
|
|
24
30
|
|
|
25
31
|
const FLAG = '__dbmigrate__flag';
|
|
26
32
|
const BATCH = 1000;
|
|
27
33
|
const TYPES = ['dml'];
|
|
34
|
+
const PG = ['pg', 'cockroachdb', 'postgres', 'postgresql'];
|
|
28
35
|
|
|
29
36
|
const SCHEMA_INSTRUCTIONS = [
|
|
30
37
|
'createTable',
|
|
@@ -43,10 +50,14 @@ const SCHEMA_INSTRUCTIONS = [
|
|
|
43
50
|
'renameCollection'
|
|
44
51
|
];
|
|
45
52
|
|
|
46
|
-
const
|
|
53
|
+
const meta = file => file.get()._meta || {};
|
|
54
|
+
|
|
55
|
+
const isDml = file => meta(file).type === 'dml';
|
|
56
|
+
|
|
57
|
+
const isBackground = file => isDml(file) && meta(file).background === true;
|
|
47
58
|
|
|
48
59
|
const checkType = file => {
|
|
49
|
-
const type = (file
|
|
60
|
+
const { type, background } = meta(file);
|
|
50
61
|
|
|
51
62
|
if (type !== undefined && TYPES.indexOf(type) === -1) {
|
|
52
63
|
throw new Error(
|
|
@@ -55,8 +66,27 @@ const checkType = file => {
|
|
|
55
66
|
' or leave it out for a schema migration'
|
|
56
67
|
);
|
|
57
68
|
}
|
|
69
|
+
|
|
70
|
+
if (background === true && type !== 'dml') {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Migration "${file.name}" runs in the background, which only dml ` +
|
|
73
|
+
"migrations can, set _meta.type to 'dml'"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
58
76
|
};
|
|
59
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Stops a worker between two batches, the job is continued later.
|
|
80
|
+
*/
|
|
81
|
+
class Stopped extends Error {
|
|
82
|
+
constructor () {
|
|
83
|
+
super('stopped');
|
|
84
|
+
this.stopped = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
89
|
+
|
|
60
90
|
const backupName = (file, op) =>
|
|
61
91
|
'__dbm_backup_' +
|
|
62
92
|
crypto
|
|
@@ -65,6 +95,12 @@ const backupName = (file, op) =>
|
|
|
65
95
|
.digest('hex')
|
|
66
96
|
.slice(0, 16);
|
|
67
97
|
|
|
98
|
+
// the mark of a soft delete in the flag of a row
|
|
99
|
+
const deleted = (file, op) => `|del:${file.name}#${op}`;
|
|
100
|
+
|
|
101
|
+
// a LIKE pattern matching text literally, with ! as escape character
|
|
102
|
+
const like = text => text.replace(/[!%_]/g, c => '!' + c);
|
|
103
|
+
|
|
68
104
|
const isObject = x =>
|
|
69
105
|
x !== null &&
|
|
70
106
|
typeof x === 'object' &&
|
|
@@ -136,13 +172,31 @@ const insertRows = args => {
|
|
|
136
172
|
throw new Error('insert needs the rows to insert');
|
|
137
173
|
};
|
|
138
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The progress of a migration run in the foreground is kept in the row of
|
|
177
|
+
* the migration lock, see State.
|
|
178
|
+
*/
|
|
179
|
+
const foreground = (pdriver, internals) => ({
|
|
180
|
+
step: op => State.step(pdriver, op, internals),
|
|
181
|
+
learned: op => State.learned(pdriver, op, internals),
|
|
182
|
+
done: op => State.done(pdriver, op, internals)
|
|
183
|
+
});
|
|
184
|
+
|
|
139
185
|
class Dml {
|
|
140
|
-
|
|
186
|
+
/**
|
|
187
|
+
* progress: records the steps started, learned and done
|
|
188
|
+
* work: options of a background worker, batch and pause between batches,
|
|
189
|
+
* stopping() tells to stop after the current batch
|
|
190
|
+
*/
|
|
191
|
+
constructor (context, file, internals, pdriver, recovery, progress, work = {}) {
|
|
141
192
|
this.driver = context._driver;
|
|
142
193
|
this.file = file;
|
|
143
194
|
this.internals = internals;
|
|
144
195
|
this.pdriver = pdriver;
|
|
145
196
|
this.recovery = recovery;
|
|
197
|
+
this.progress = progress || foreground(pdriver, internals);
|
|
198
|
+
this.work = work;
|
|
199
|
+
this.transactional = meta(file).transactions !== false;
|
|
146
200
|
this.op = 0;
|
|
147
201
|
this.current = null;
|
|
148
202
|
|
|
@@ -165,6 +219,83 @@ class Dml {
|
|
|
165
219
|
return t ? t.columns || t : null;
|
|
166
220
|
}
|
|
167
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Reads rows with parameters written as ?, all of pg takes $1, $2, ...
|
|
224
|
+
*/
|
|
225
|
+
select (sql, params = []) {
|
|
226
|
+
if (!params.length) {
|
|
227
|
+
return this.driver.all(sql);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (PG.indexOf(this.driver._dbmDriver) !== -1) {
|
|
231
|
+
let i = 0;
|
|
232
|
+
sql = sql.replace(/\?/g, () => `$${++i}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// with a callback, all of db-migrate-sqlite3 before 1.2.1 drops the
|
|
236
|
+
// parameters without one
|
|
237
|
+
return Promise.fromCallback(cb => this.driver.all(sql, params, cb));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* sql followed by a text parameter. CONCAT of PostgreSQL can not tell the
|
|
242
|
+
* type of a parameter, || of MySQL is OR.
|
|
243
|
+
*/
|
|
244
|
+
concat (sql) {
|
|
245
|
+
return this.driver._dbmDriver === 'mysql'
|
|
246
|
+
? `CONCAT(${sql}, ?)`
|
|
247
|
+
: `(${sql} || CAST(? AS TEXT))`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Runs fn inside a transaction, rolled back if it fails.
|
|
252
|
+
*/
|
|
253
|
+
async tx (fn) {
|
|
254
|
+
if (!this.transactional || this.internals.dryRun) {
|
|
255
|
+
return fn();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await this.driver.runSql('BEGIN');
|
|
259
|
+
|
|
260
|
+
let ret;
|
|
261
|
+
try {
|
|
262
|
+
ret = await fn();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
try {
|
|
265
|
+
await this.driver.runSql('ROLLBACK');
|
|
266
|
+
} catch (rollbackErr) {
|
|
267
|
+
log.verbose('[dml] rollback failed', rollbackErr.message);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await this.driver.runSql('COMMIT');
|
|
274
|
+
return ret;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Between batches, a worker in the background pauses and stops if asked.
|
|
279
|
+
*/
|
|
280
|
+
async between () {
|
|
281
|
+
if (this.work.stopping && this.work.stopping()) {
|
|
282
|
+
throw new Stopped();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// migrations paused the jobs
|
|
286
|
+
if (this.work.yielding && (await this.work.yielding())) {
|
|
287
|
+
throw new Stopped();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (this.work.pause) {
|
|
291
|
+
await delay(this.work.pause);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
batchSize (options) {
|
|
296
|
+
return Number(options.batch) || Number(this.work.batch) || BATCH;
|
|
297
|
+
}
|
|
298
|
+
|
|
168
299
|
/**
|
|
169
300
|
* The key of a table, to change and restore its rows by. Taken from the
|
|
170
301
|
* schema, for other tables it is passed with { key }.
|
|
@@ -179,10 +310,12 @@ class Dml {
|
|
|
179
310
|
|
|
180
311
|
if (!key.length) {
|
|
181
312
|
throw new Error(
|
|
182
|
-
`${action}("${table}") needs the primary key of "${table}"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
313
|
+
`${action}("${table}") needs the primary key of "${table}", which is ` +
|
|
314
|
+
"unknown to the schema of db-migrate. Pass it with { key: 'id' }" +
|
|
315
|
+
(action === 'purge'
|
|
316
|
+
? '.'
|
|
317
|
+
: ', or pass { irreversible: true } to change the rows without ' +
|
|
318
|
+
'being able to revert it.')
|
|
186
319
|
);
|
|
187
320
|
}
|
|
188
321
|
|
|
@@ -251,6 +384,31 @@ class Dml {
|
|
|
251
384
|
return { sql, params };
|
|
252
385
|
}
|
|
253
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Changes the rows matching condition in batches by their key, until none
|
|
389
|
+
* matches anymore. Changing a row has to make it stop matching, so this
|
|
390
|
+
* continues by itself after an interruption.
|
|
391
|
+
*/
|
|
392
|
+
async untilDone (table, key, condition, options, change) {
|
|
393
|
+
const size = this.batchSize(options);
|
|
394
|
+
const order = key.map(k => this.ddl(k)).join(', ');
|
|
395
|
+
|
|
396
|
+
for (;;) {
|
|
397
|
+
const rows = await this.select(
|
|
398
|
+
`SELECT ${order} FROM ${this.ddl(table)} WHERE ${condition.sql} ` +
|
|
399
|
+
`ORDER BY ${order} LIMIT ${size}`,
|
|
400
|
+
condition.params
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
if (!rows.length) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await this.tx(() => change(this.byKey(rows, key)));
|
|
408
|
+
await this.between();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
254
412
|
/**
|
|
255
413
|
* Runs one instruction as a step: records what reverts it, executes it and
|
|
256
414
|
* records it as done. The step of an interrupted run is continued, with
|
|
@@ -272,7 +430,7 @@ class Dml {
|
|
|
272
430
|
return;
|
|
273
431
|
}
|
|
274
432
|
|
|
275
|
-
await
|
|
433
|
+
await this.progress.step(op);
|
|
276
434
|
|
|
277
435
|
let entry = recovery && op <= recovery.learned
|
|
278
436
|
? mod.s.find(e => e.n === op)
|
|
@@ -288,22 +446,29 @@ class Dml {
|
|
|
288
446
|
entry = record(op);
|
|
289
447
|
entry.n = op;
|
|
290
448
|
mod.s.push(entry);
|
|
291
|
-
await
|
|
292
|
-
await
|
|
449
|
+
await this.save();
|
|
450
|
+
await this.progress.learned(op);
|
|
293
451
|
}
|
|
294
452
|
|
|
295
453
|
await execute(entry, op);
|
|
296
454
|
|
|
297
455
|
internals.migrationDone = op;
|
|
298
|
-
await
|
|
456
|
+
await this.progress.done(op);
|
|
457
|
+
await this.between();
|
|
299
458
|
}
|
|
300
459
|
|
|
460
|
+
/**
|
|
461
|
+
* The records of the migration, its schema stays as it is.
|
|
462
|
+
*/
|
|
301
463
|
save () {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
464
|
+
if (this.internals.dryRun) {
|
|
465
|
+
return Promise.resolve();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return this.pdriver._updateKV(
|
|
469
|
+
this.internals.migrationState,
|
|
470
|
+
this.file.name,
|
|
471
|
+
JSON.stringify(this.internals.modSchema)
|
|
307
472
|
);
|
|
308
473
|
}
|
|
309
474
|
|
|
@@ -342,26 +507,27 @@ class Dml {
|
|
|
342
507
|
irreversible
|
|
343
508
|
? { t: 3, a: 'insert', c: [table] }
|
|
344
509
|
: { t: 4, a: 'insert', c: [table, `${this.file.name}#${op}`] },
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
510
|
+
entry =>
|
|
511
|
+
this.tx(async () => {
|
|
512
|
+
if (!rows.length) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
349
515
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
516
|
+
if (irreversible) {
|
|
517
|
+
return this.driver.insert(table, rows);
|
|
518
|
+
}
|
|
353
519
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
520
|
+
const flag = entry.c[1];
|
|
521
|
+
// rows of an interrupted run of this step
|
|
522
|
+
await this.driver.runSql(
|
|
523
|
+
`DELETE FROM ${this.ddl(table)} WHERE ${this.ddl(FLAG)} = ?`,
|
|
524
|
+
[flag]
|
|
525
|
+
);
|
|
526
|
+
await this.driver.insert(
|
|
527
|
+
table,
|
|
528
|
+
rows.map(row => Object.assign({}, row, { [FLAG]: flag }))
|
|
529
|
+
);
|
|
530
|
+
})
|
|
365
531
|
);
|
|
366
532
|
}
|
|
367
533
|
|
|
@@ -404,28 +570,46 @@ class Dml {
|
|
|
404
570
|
},
|
|
405
571
|
async entry => {
|
|
406
572
|
if (irreversible || this.internals.dryRun) {
|
|
407
|
-
return this.
|
|
408
|
-
|
|
409
|
-
|
|
573
|
+
return this.tx(() =>
|
|
574
|
+
this.driver.runSql(
|
|
575
|
+
`UPDATE ${this.ddl(table)} SET ${assign} WHERE ${condition.sql}`,
|
|
576
|
+
values.concat(condition.params)
|
|
577
|
+
)
|
|
410
578
|
);
|
|
411
579
|
}
|
|
412
580
|
|
|
413
581
|
await this.backup(entry, table, key.concat(columns), condition);
|
|
414
|
-
await this.batches(entry, options,
|
|
415
|
-
|
|
416
|
-
return this.driver.runSql(
|
|
582
|
+
await this.batches(entry, options, match =>
|
|
583
|
+
this.driver.runSql(
|
|
417
584
|
`UPDATE ${this.ddl(table)} SET ${assign} WHERE ${match.sql}`,
|
|
418
585
|
values.concat(match.params)
|
|
419
|
-
)
|
|
420
|
-
|
|
586
|
+
)
|
|
587
|
+
);
|
|
421
588
|
}
|
|
422
589
|
);
|
|
423
590
|
}
|
|
424
591
|
|
|
425
592
|
/**
|
|
426
593
|
* delete(table, where, [options]), delete the rows matching where.
|
|
594
|
+
*
|
|
595
|
+
* mode 'copy' (default) copies the rows to a backup table first. mode
|
|
596
|
+
* 'soft' keeps the rows, sets column to the current time, or value, and
|
|
597
|
+
* marks them in their flag. The application filters them by column, until
|
|
598
|
+
* purge deletes them.
|
|
427
599
|
*/
|
|
428
600
|
delete (table, where, options = {}) {
|
|
601
|
+
const mode = options.mode || 'copy';
|
|
602
|
+
|
|
603
|
+
if (mode === 'soft') {
|
|
604
|
+
return this.softDelete(table, where, options);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (mode !== 'copy') {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`delete("${table}") has no mode "${mode}", use 'copy' or 'soft'`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
429
613
|
const condition = this.where(where);
|
|
430
614
|
const irreversible = options.irreversible === true;
|
|
431
615
|
const key = irreversible ? null : this.key(table, options, 'delete');
|
|
@@ -439,20 +623,117 @@ class Dml {
|
|
|
439
623
|
: { t: 4, a: 'delete', c: [table, backupName(this.file, op), key] },
|
|
440
624
|
async entry => {
|
|
441
625
|
if (irreversible || this.internals.dryRun) {
|
|
626
|
+
return this.tx(() =>
|
|
627
|
+
this.driver.runSql(
|
|
628
|
+
`DELETE FROM ${this.ddl(table)} WHERE ${condition.sql}`,
|
|
629
|
+
condition.params
|
|
630
|
+
)
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
await this.backup(entry, table, null, condition);
|
|
635
|
+
await this.batches(entry, options, match =>
|
|
636
|
+
this.driver.runSql(
|
|
637
|
+
`DELETE FROM ${this.ddl(table)} WHERE ${match.sql}`,
|
|
638
|
+
match.params
|
|
639
|
+
)
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
softDelete (table, where, options) {
|
|
646
|
+
const column = options.column;
|
|
647
|
+
const columns = this.columns(table);
|
|
648
|
+
|
|
649
|
+
if (typeof column !== 'string') {
|
|
650
|
+
throw new Error(
|
|
651
|
+
`delete("${table}") in soft mode needs the column marking the rows ` +
|
|
652
|
+
"as deleted, { mode: 'soft', column: 'deleted_at' }"
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!columns || !columns[FLAG]) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
`delete("${table}") in soft mode marks the rows in their ${FLAG} ` +
|
|
659
|
+
'column, which only tables created by v2 migrations have'
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const condition = this.where(where);
|
|
664
|
+
const key = this.key(table, options, 'delete');
|
|
665
|
+
const time = options.value === undefined;
|
|
666
|
+
|
|
667
|
+
return this.step(
|
|
668
|
+
'delete',
|
|
669
|
+
[table],
|
|
670
|
+
op => ({ t: 4, a: 'soft', c: [table, column, key, deleted(this.file, op)] }),
|
|
671
|
+
entry => {
|
|
672
|
+
const mark = entry.c[3];
|
|
673
|
+
const set =
|
|
674
|
+
`${this.ddl(column)} = ${time ? 'CURRENT_TIMESTAMP' : '?'}, ` +
|
|
675
|
+
`${this.ddl(FLAG)} = ${this.concat(`COALESCE(${this.ddl(FLAG)}, '')`)}`;
|
|
676
|
+
const value = time ? [mark] : [toValue(options.value), mark];
|
|
677
|
+
// rows deleted already, by the application or before, stay as they are
|
|
678
|
+
const active = {
|
|
679
|
+
sql: `(${condition.sql}) AND ${this.ddl(column)} IS NULL`,
|
|
680
|
+
params: condition.params
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
if (this.internals.dryRun) {
|
|
684
|
+
return this.driver.runSql(
|
|
685
|
+
`UPDATE ${this.ddl(table)} SET ${set} WHERE ${active.sql}`,
|
|
686
|
+
value.concat(active.params)
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return this.untilDone(table, key, active, options, match =>
|
|
691
|
+
this.driver.runSql(
|
|
692
|
+
`UPDATE ${this.ddl(table)} SET ${set} WHERE ${match.sql}`,
|
|
693
|
+
value.concat(match.params)
|
|
694
|
+
)
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* purge(table, [migration], [options]), deletes the rows deleted in soft
|
|
702
|
+
* mode for good, of all migrations or of the one named. It can not be
|
|
703
|
+
* reverted.
|
|
704
|
+
*/
|
|
705
|
+
purge (table, migration, options = {}) {
|
|
706
|
+
if (isObject(migration)) {
|
|
707
|
+
options = migration;
|
|
708
|
+
migration = undefined;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const key = this.key(table, options, 'purge');
|
|
712
|
+
const mark = migration ? `|del:${migration}#` : '|del:';
|
|
713
|
+
|
|
714
|
+
return this.step(
|
|
715
|
+
'purge',
|
|
716
|
+
[table].concat(migration ? [migration] : []),
|
|
717
|
+
() => ({ t: 3, a: 'purge', c: [table] }),
|
|
718
|
+
() => {
|
|
719
|
+
const condition = {
|
|
720
|
+
sql: `${this.ddl(FLAG)} LIKE ? ESCAPE '!'`,
|
|
721
|
+
params: [`%${like(mark)}%`]
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
if (this.internals.dryRun) {
|
|
442
725
|
return this.driver.runSql(
|
|
443
726
|
`DELETE FROM ${this.ddl(table)} WHERE ${condition.sql}`,
|
|
444
727
|
condition.params
|
|
445
728
|
);
|
|
446
729
|
}
|
|
447
730
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const match = this.byKey(rows, key);
|
|
451
|
-
return this.driver.runSql(
|
|
731
|
+
return this.untilDone(table, key, condition, options, match =>
|
|
732
|
+
this.driver.runSql(
|
|
452
733
|
`DELETE FROM ${this.ddl(table)} WHERE ${match.sql}`,
|
|
453
734
|
match.params
|
|
454
|
-
)
|
|
455
|
-
|
|
735
|
+
)
|
|
736
|
+
);
|
|
456
737
|
}
|
|
457
738
|
);
|
|
458
739
|
}
|
|
@@ -486,7 +767,7 @@ class Dml {
|
|
|
486
767
|
irreversible
|
|
487
768
|
? { t: 3, a: 'runSql', c: [sql] }
|
|
488
769
|
: { t: 4, a: 'runSql', c: [revert[0], revert[1] || []] },
|
|
489
|
-
() => this.driver.runSql(sql, params)
|
|
770
|
+
() => this.tx(() => this.driver.runSql(sql, params))
|
|
490
771
|
);
|
|
491
772
|
}
|
|
492
773
|
|
|
@@ -510,15 +791,18 @@ class Dml {
|
|
|
510
791
|
const backup = this.ddl(entry.c[1]);
|
|
511
792
|
const select = columns ? columns.map(c => this.ddl(c)).join(', ') : '*';
|
|
512
793
|
|
|
794
|
+
// outside of transactions, creating tables commits them in MySQL
|
|
513
795
|
await this.driver.runSql(`DROP TABLE IF EXISTS ${backup}`);
|
|
514
796
|
await this.driver.runSql(
|
|
515
797
|
`CREATE TABLE ${backup} AS SELECT ${select} FROM ${this.ddl(table)} ` +
|
|
516
798
|
'WHERE 1 = 0'
|
|
517
799
|
);
|
|
518
|
-
await this.
|
|
519
|
-
|
|
520
|
-
`
|
|
521
|
-
|
|
800
|
+
await this.tx(() =>
|
|
801
|
+
this.driver.runSql(
|
|
802
|
+
`INSERT INTO ${backup} SELECT ${select} FROM ${this.ddl(table)} ` +
|
|
803
|
+
`WHERE ${condition.sql}`,
|
|
804
|
+
condition.params
|
|
805
|
+
)
|
|
522
806
|
);
|
|
523
807
|
|
|
524
808
|
entry.b = 1;
|
|
@@ -532,13 +816,14 @@ class Dml {
|
|
|
532
816
|
* interruption sets the same values.
|
|
533
817
|
*/
|
|
534
818
|
async batches (entry, options, change) {
|
|
535
|
-
const size =
|
|
819
|
+
const size = this.batchSize(options);
|
|
536
820
|
const backup = this.ddl(entry.c[1]);
|
|
537
|
-
const key = entry.c[2]
|
|
821
|
+
const key = entry.c[2];
|
|
822
|
+
const order = key.map(k => this.ddl(k)).join(', ');
|
|
538
823
|
|
|
539
824
|
for (;;) {
|
|
540
825
|
const rows = await this.driver.all(
|
|
541
|
-
`SELECT ${
|
|
826
|
+
`SELECT ${order} FROM ${backup} ORDER BY ${order} ` +
|
|
542
827
|
`LIMIT ${size} OFFSET ${entry.o}`
|
|
543
828
|
);
|
|
544
829
|
|
|
@@ -546,10 +831,11 @@ class Dml {
|
|
|
546
831
|
return;
|
|
547
832
|
}
|
|
548
833
|
|
|
549
|
-
await change(rows);
|
|
834
|
+
await this.tx(() => change(this.byKey(rows, key)));
|
|
550
835
|
entry.o += rows.length;
|
|
551
836
|
log.verbose(`[dml] ${this.file.name}: ${entry.o} rows ${entry.a}d`);
|
|
552
837
|
await this.save();
|
|
838
|
+
await this.between();
|
|
553
839
|
}
|
|
554
840
|
}
|
|
555
841
|
}
|
|
@@ -560,12 +846,14 @@ class Dml {
|
|
|
560
846
|
*/
|
|
561
847
|
const reverts = {
|
|
562
848
|
insert: (d, [table, flag]) =>
|
|
563
|
-
d.
|
|
564
|
-
|
|
565
|
-
|
|
849
|
+
d.tx(() =>
|
|
850
|
+
d.driver.runSql(
|
|
851
|
+
`DELETE FROM ${d.ddl(table)} WHERE ${d.ddl(FLAG)} = ?`,
|
|
852
|
+
[flag]
|
|
853
|
+
)
|
|
566
854
|
),
|
|
567
855
|
|
|
568
|
-
runSql: (d, [sql, params]) => d.driver.runSql(sql, params),
|
|
856
|
+
runSql: (d, [sql, params]) => d.tx(() => d.driver.runSql(sql, params)),
|
|
569
857
|
|
|
570
858
|
update: async (d, [table, backup, key, columns]) => {
|
|
571
859
|
let offset = 0;
|
|
@@ -583,10 +871,12 @@ const reverts = {
|
|
|
583
871
|
return;
|
|
584
872
|
}
|
|
585
873
|
|
|
586
|
-
await
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
874
|
+
await d.tx(() =>
|
|
875
|
+
Promise.each(rows, row =>
|
|
876
|
+
d.driver.runSql(
|
|
877
|
+
`UPDATE ${d.ddl(table)} SET ${assign} WHERE ${match}`,
|
|
878
|
+
columns.map(c => row[c]).concat(key.map(k => row[k]))
|
|
879
|
+
)
|
|
590
880
|
)
|
|
591
881
|
);
|
|
592
882
|
offset += rows.length;
|
|
@@ -608,12 +898,32 @@ const reverts = {
|
|
|
608
898
|
.join(' AND ');
|
|
609
899
|
|
|
610
900
|
// rows deleted by the step only, those not deleted yet stay as they are
|
|
611
|
-
await d.
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
901
|
+
await d.tx(() =>
|
|
902
|
+
d.driver.runSql(
|
|
903
|
+
`INSERT INTO ${d.ddl(table)} (${columns}) SELECT ${columns} FROM ` +
|
|
904
|
+
`${d.ddl(backup)} ${d.ddl('__dbm_b')} WHERE NOT EXISTS (SELECT 1 ` +
|
|
905
|
+
`FROM ${d.ddl(table)} ${d.ddl('__dbm_t')} WHERE ${match})`
|
|
906
|
+
)
|
|
615
907
|
);
|
|
616
|
-
}
|
|
908
|
+
},
|
|
909
|
+
|
|
910
|
+
soft: (d, [table, column, key, mark]) =>
|
|
911
|
+
d.untilDone(
|
|
912
|
+
table,
|
|
913
|
+
key,
|
|
914
|
+
{
|
|
915
|
+
sql: `${d.ddl(FLAG)} LIKE ? ESCAPE '!'`,
|
|
916
|
+
params: [`%${like(mark)}%`]
|
|
917
|
+
},
|
|
918
|
+
{},
|
|
919
|
+
match =>
|
|
920
|
+
d.driver.runSql(
|
|
921
|
+
`UPDATE ${d.ddl(table)} SET ${d.ddl(column)} = NULL, ` +
|
|
922
|
+
`${d.ddl(FLAG)} = NULLIF(REPLACE(${d.ddl(FLAG)}, ?, ''), '') ` +
|
|
923
|
+
`WHERE ${match.sql}`,
|
|
924
|
+
[mark].concat(match.params)
|
|
925
|
+
)
|
|
926
|
+
)
|
|
617
927
|
};
|
|
618
928
|
|
|
619
929
|
const irreversibleSteps = internals =>
|
|
@@ -625,8 +935,8 @@ const irreversibleSteps = internals =>
|
|
|
625
935
|
* Reverts the recorded steps, the last first. Steps with a backup table
|
|
626
936
|
* restore the rows from it and drop it afterwards.
|
|
627
937
|
*/
|
|
628
|
-
const revert = async (context, file, internals, pdriver) => {
|
|
629
|
-
|
|
938
|
+
const revert = async (context, file, internals, pdriver, d) => {
|
|
939
|
+
d = d || new Dml(context, file, internals, pdriver, null);
|
|
630
940
|
const mod = internals.modSchema;
|
|
631
941
|
|
|
632
942
|
while (mod.s.length) {
|
|
@@ -637,10 +947,10 @@ const revert = async (context, file, internals, pdriver) => {
|
|
|
637
947
|
throw new Error(`Invalid state record of a dml migration, ${entry.a}`);
|
|
638
948
|
}
|
|
639
949
|
|
|
640
|
-
if (internals.dryRun && backup) {
|
|
950
|
+
if (internals.dryRun && (backup || entry.a === 'soft')) {
|
|
641
951
|
log.info(
|
|
642
|
-
`[dml] would
|
|
643
|
-
entry.c[1]
|
|
952
|
+
`[dml] would revert ${entry.a}("${entry.c[0]}")` +
|
|
953
|
+
(backup ? ` from ${entry.c[1]}` : '')
|
|
644
954
|
);
|
|
645
955
|
} else {
|
|
646
956
|
// b: 1 the backup is complete, 2 the rows are restored from it
|
|
@@ -662,6 +972,21 @@ const revert = async (context, file, internals, pdriver) => {
|
|
|
662
972
|
await State.deleteState(pdriver, file, internals);
|
|
663
973
|
};
|
|
664
974
|
|
|
975
|
+
const irreversibleError = (file, internals) => {
|
|
976
|
+
const irreversible = irreversibleSteps(internals);
|
|
977
|
+
|
|
978
|
+
if (!irreversible.length) {
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const err = new Error(
|
|
983
|
+
`Migration "${file.name}" can not be reverted, it ran ` +
|
|
984
|
+
`${irreversible.join(', ')}, which can not be reverted.`
|
|
985
|
+
);
|
|
986
|
+
err.irreversible = true;
|
|
987
|
+
return err;
|
|
988
|
+
};
|
|
989
|
+
|
|
665
990
|
const opts = context => ({
|
|
666
991
|
options: context.internals.safeOptions,
|
|
667
992
|
dbm: context.internals.safeOptions.dbmigrate
|
|
@@ -675,14 +1000,8 @@ const down = async (context, driver, file, { abort } = {}) => {
|
|
|
675
1000
|
await State.startMigration(pdriver, file, internals, { op: 'down' });
|
|
676
1001
|
}
|
|
677
1002
|
|
|
678
|
-
const
|
|
679
|
-
if (
|
|
680
|
-
const err = new Error(
|
|
681
|
-
`Migration "${file.name}" can not be reverted, it ran ` +
|
|
682
|
-
`${irreversible.join(', ')} with { irreversible: true }.`
|
|
683
|
-
);
|
|
684
|
-
err.irreversible = true;
|
|
685
|
-
|
|
1003
|
+
const err = irreversibleError(file, internals);
|
|
1004
|
+
if (err) {
|
|
686
1005
|
if (abort !== true) {
|
|
687
1006
|
await State.endMigration(pdriver, file, internals);
|
|
688
1007
|
}
|
|
@@ -695,15 +1014,16 @@ const down = async (context, driver, file, { abort } = {}) => {
|
|
|
695
1014
|
return Promise.promisify(context.deleteMigrationRecord.bind(context))(file);
|
|
696
1015
|
};
|
|
697
1016
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1017
|
+
/**
|
|
1018
|
+
* Runs the migration, after recovering an interrupted run, and rolls it
|
|
1019
|
+
* back if it fails. Shared by the foreground and the background workers.
|
|
1020
|
+
*
|
|
1021
|
+
* hooks.rollingBack: records that the rollback started
|
|
1022
|
+
* hooks.restart: records a fresh start after recovering by rollback
|
|
1023
|
+
*/
|
|
1024
|
+
const execute = async (context, file, internals, pdriver, interrupted, progress, work, hooks) => {
|
|
701
1025
|
let recovery = null;
|
|
702
1026
|
|
|
703
|
-
const interrupted = await State.startMigration(pdriver, file, internals, {
|
|
704
|
-
recover: true
|
|
705
|
-
});
|
|
706
|
-
|
|
707
1027
|
if (interrupted) {
|
|
708
1028
|
const mode = recoveryMode(file, interrupted);
|
|
709
1029
|
log.warn(
|
|
@@ -713,29 +1033,32 @@ const up = async (context, driver, file) => {
|
|
|
713
1033
|
);
|
|
714
1034
|
|
|
715
1035
|
if (mode === 'rollback') {
|
|
716
|
-
await
|
|
1036
|
+
await hooks.rollingBack();
|
|
717
1037
|
await revert(context, file, internals, pdriver);
|
|
718
1038
|
internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
719
|
-
await
|
|
720
|
-
await State.startMigration(pdriver, file, internals);
|
|
1039
|
+
await hooks.restart();
|
|
721
1040
|
} else {
|
|
722
1041
|
recovery = interrupted;
|
|
723
1042
|
}
|
|
724
1043
|
}
|
|
725
1044
|
|
|
726
1045
|
internals.migrationDone = recovery ? recovery.done : 0;
|
|
727
|
-
const db = new Dml(context, file, internals, pdriver, recovery);
|
|
1046
|
+
const db = new Dml(context, file, internals, pdriver, recovery, progress, work);
|
|
728
1047
|
|
|
729
1048
|
try {
|
|
730
1049
|
await file.get().migrate(db, opts(context));
|
|
731
1050
|
} catch (err) {
|
|
1051
|
+
if (err && err.stopped) {
|
|
1052
|
+
throw err;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
732
1055
|
describeFailure(err, db, internals);
|
|
733
1056
|
|
|
734
1057
|
if (irreversibleSteps(internals).length) {
|
|
735
1058
|
log.error(
|
|
736
1059
|
`Migration "${file.name}" failed and can not be rolled back, it ` +
|
|
737
|
-
'ran a step
|
|
738
|
-
'
|
|
1060
|
+
'ran a step which can not be reverted. The steps executed stay, ' +
|
|
1061
|
+
'the next run continues after them.'
|
|
739
1062
|
);
|
|
740
1063
|
throw err;
|
|
741
1064
|
}
|
|
@@ -746,8 +1069,34 @@ const up = async (context, driver, file) => {
|
|
|
746
1069
|
(err && err.message) || err
|
|
747
1070
|
}`
|
|
748
1071
|
);
|
|
749
|
-
await
|
|
750
|
-
await
|
|
1072
|
+
await hooks.rollingBack();
|
|
1073
|
+
await revert(context, file, internals, pdriver, db);
|
|
1074
|
+
err.rolledBack = true;
|
|
1075
|
+
throw err;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
const up = async (context, driver, file) => {
|
|
1080
|
+
const internals = context.internals;
|
|
1081
|
+
const pdriver = context._pdriver;
|
|
1082
|
+
|
|
1083
|
+
const interrupted = await State.startMigration(pdriver, file, internals, {
|
|
1084
|
+
recover: true
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
try {
|
|
1088
|
+
await execute(context, file, internals, pdriver, interrupted, null, {}, {
|
|
1089
|
+
rollingBack: () => State.rollingBack(pdriver, internals),
|
|
1090
|
+
restart: async () => {
|
|
1091
|
+
await State.endMigration(pdriver, file, internals);
|
|
1092
|
+
await State.startMigration(pdriver, file, internals);
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
if (err && err.rolledBack) {
|
|
1097
|
+
await State.endMigration(pdriver, file, internals);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
751
1100
|
throw err;
|
|
752
1101
|
}
|
|
753
1102
|
|
|
@@ -755,4 +1104,17 @@ const up = async (context, driver, file) => {
|
|
|
755
1104
|
return State.endMigration(pdriver, file, internals);
|
|
756
1105
|
};
|
|
757
1106
|
|
|
758
|
-
module.exports = {
|
|
1107
|
+
module.exports = {
|
|
1108
|
+
isDml,
|
|
1109
|
+
isBackground,
|
|
1110
|
+
checkType,
|
|
1111
|
+
up,
|
|
1112
|
+
down,
|
|
1113
|
+
execute,
|
|
1114
|
+
revert,
|
|
1115
|
+
irreversibleError,
|
|
1116
|
+
Dml,
|
|
1117
|
+
Stopped,
|
|
1118
|
+
FLAG,
|
|
1119
|
+
insertRows
|
|
1120
|
+
};
|