db-migrate 1.3.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/api.js +9 -0
- package/lib/commands/generated.js +2 -1
- package/lib/commands/work.js +33 -0
- package/lib/dml.js +458 -101
- package/lib/driver/index.js +2 -0
- package/lib/jobs.js +177 -0
- package/lib/walker.js +51 -3
- package/lib/work.js +260 -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,78 @@ 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
|
+
if (this.work.pause) {
|
|
286
|
+
await delay(this.work.pause);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
batchSize (options) {
|
|
291
|
+
return Number(options.batch) || Number(this.work.batch) || BATCH;
|
|
292
|
+
}
|
|
293
|
+
|
|
168
294
|
/**
|
|
169
295
|
* The key of a table, to change and restore its rows by. Taken from the
|
|
170
296
|
* schema, for other tables it is passed with { key }.
|
|
@@ -179,10 +305,12 @@ class Dml {
|
|
|
179
305
|
|
|
180
306
|
if (!key.length) {
|
|
181
307
|
throw new Error(
|
|
182
|
-
`${action}("${table}") needs the primary key of "${table}"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
308
|
+
`${action}("${table}") needs the primary key of "${table}", which is ` +
|
|
309
|
+
"unknown to the schema of db-migrate. Pass it with { key: 'id' }" +
|
|
310
|
+
(action === 'purge'
|
|
311
|
+
? '.'
|
|
312
|
+
: ', or pass { irreversible: true } to change the rows without ' +
|
|
313
|
+
'being able to revert it.')
|
|
186
314
|
);
|
|
187
315
|
}
|
|
188
316
|
|
|
@@ -251,6 +379,31 @@ class Dml {
|
|
|
251
379
|
return { sql, params };
|
|
252
380
|
}
|
|
253
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Changes the rows matching condition in batches by their key, until none
|
|
384
|
+
* matches anymore. Changing a row has to make it stop matching, so this
|
|
385
|
+
* continues by itself after an interruption.
|
|
386
|
+
*/
|
|
387
|
+
async untilDone (table, key, condition, options, change) {
|
|
388
|
+
const size = this.batchSize(options);
|
|
389
|
+
const order = key.map(k => this.ddl(k)).join(', ');
|
|
390
|
+
|
|
391
|
+
for (;;) {
|
|
392
|
+
const rows = await this.select(
|
|
393
|
+
`SELECT ${order} FROM ${this.ddl(table)} WHERE ${condition.sql} ` +
|
|
394
|
+
`ORDER BY ${order} LIMIT ${size}`,
|
|
395
|
+
condition.params
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
if (!rows.length) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
await this.tx(() => change(this.byKey(rows, key)));
|
|
403
|
+
await this.between();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
254
407
|
/**
|
|
255
408
|
* Runs one instruction as a step: records what reverts it, executes it and
|
|
256
409
|
* records it as done. The step of an interrupted run is continued, with
|
|
@@ -272,7 +425,7 @@ class Dml {
|
|
|
272
425
|
return;
|
|
273
426
|
}
|
|
274
427
|
|
|
275
|
-
await
|
|
428
|
+
await this.progress.step(op);
|
|
276
429
|
|
|
277
430
|
let entry = recovery && op <= recovery.learned
|
|
278
431
|
? mod.s.find(e => e.n === op)
|
|
@@ -288,22 +441,29 @@ class Dml {
|
|
|
288
441
|
entry = record(op);
|
|
289
442
|
entry.n = op;
|
|
290
443
|
mod.s.push(entry);
|
|
291
|
-
await
|
|
292
|
-
await
|
|
444
|
+
await this.save();
|
|
445
|
+
await this.progress.learned(op);
|
|
293
446
|
}
|
|
294
447
|
|
|
295
448
|
await execute(entry, op);
|
|
296
449
|
|
|
297
450
|
internals.migrationDone = op;
|
|
298
|
-
await
|
|
451
|
+
await this.progress.done(op);
|
|
452
|
+
await this.between();
|
|
299
453
|
}
|
|
300
454
|
|
|
455
|
+
/**
|
|
456
|
+
* The records of the migration, its schema stays as it is.
|
|
457
|
+
*/
|
|
301
458
|
save () {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
459
|
+
if (this.internals.dryRun) {
|
|
460
|
+
return Promise.resolve();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return this.pdriver._updateKV(
|
|
464
|
+
this.internals.migrationState,
|
|
465
|
+
this.file.name,
|
|
466
|
+
JSON.stringify(this.internals.modSchema)
|
|
307
467
|
);
|
|
308
468
|
}
|
|
309
469
|
|
|
@@ -342,26 +502,27 @@ class Dml {
|
|
|
342
502
|
irreversible
|
|
343
503
|
? { t: 3, a: 'insert', c: [table] }
|
|
344
504
|
: { t: 4, a: 'insert', c: [table, `${this.file.name}#${op}`] },
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
505
|
+
entry =>
|
|
506
|
+
this.tx(async () => {
|
|
507
|
+
if (!rows.length) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
349
510
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
511
|
+
if (irreversible) {
|
|
512
|
+
return this.driver.insert(table, rows);
|
|
513
|
+
}
|
|
353
514
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
515
|
+
const flag = entry.c[1];
|
|
516
|
+
// rows of an interrupted run of this step
|
|
517
|
+
await this.driver.runSql(
|
|
518
|
+
`DELETE FROM ${this.ddl(table)} WHERE ${this.ddl(FLAG)} = ?`,
|
|
519
|
+
[flag]
|
|
520
|
+
);
|
|
521
|
+
await this.driver.insert(
|
|
522
|
+
table,
|
|
523
|
+
rows.map(row => Object.assign({}, row, { [FLAG]: flag }))
|
|
524
|
+
);
|
|
525
|
+
})
|
|
365
526
|
);
|
|
366
527
|
}
|
|
367
528
|
|
|
@@ -404,28 +565,46 @@ class Dml {
|
|
|
404
565
|
},
|
|
405
566
|
async entry => {
|
|
406
567
|
if (irreversible || this.internals.dryRun) {
|
|
407
|
-
return this.
|
|
408
|
-
|
|
409
|
-
|
|
568
|
+
return this.tx(() =>
|
|
569
|
+
this.driver.runSql(
|
|
570
|
+
`UPDATE ${this.ddl(table)} SET ${assign} WHERE ${condition.sql}`,
|
|
571
|
+
values.concat(condition.params)
|
|
572
|
+
)
|
|
410
573
|
);
|
|
411
574
|
}
|
|
412
575
|
|
|
413
576
|
await this.backup(entry, table, key.concat(columns), condition);
|
|
414
|
-
await this.batches(entry, options,
|
|
415
|
-
|
|
416
|
-
return this.driver.runSql(
|
|
577
|
+
await this.batches(entry, options, match =>
|
|
578
|
+
this.driver.runSql(
|
|
417
579
|
`UPDATE ${this.ddl(table)} SET ${assign} WHERE ${match.sql}`,
|
|
418
580
|
values.concat(match.params)
|
|
419
|
-
)
|
|
420
|
-
|
|
581
|
+
)
|
|
582
|
+
);
|
|
421
583
|
}
|
|
422
584
|
);
|
|
423
585
|
}
|
|
424
586
|
|
|
425
587
|
/**
|
|
426
588
|
* delete(table, where, [options]), delete the rows matching where.
|
|
589
|
+
*
|
|
590
|
+
* mode 'copy' (default) copies the rows to a backup table first. mode
|
|
591
|
+
* 'soft' keeps the rows, sets column to the current time, or value, and
|
|
592
|
+
* marks them in their flag. The application filters them by column, until
|
|
593
|
+
* purge deletes them.
|
|
427
594
|
*/
|
|
428
595
|
delete (table, where, options = {}) {
|
|
596
|
+
const mode = options.mode || 'copy';
|
|
597
|
+
|
|
598
|
+
if (mode === 'soft') {
|
|
599
|
+
return this.softDelete(table, where, options);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (mode !== 'copy') {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`delete("${table}") has no mode "${mode}", use 'copy' or 'soft'`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
429
608
|
const condition = this.where(where);
|
|
430
609
|
const irreversible = options.irreversible === true;
|
|
431
610
|
const key = irreversible ? null : this.key(table, options, 'delete');
|
|
@@ -439,20 +618,117 @@ class Dml {
|
|
|
439
618
|
: { t: 4, a: 'delete', c: [table, backupName(this.file, op), key] },
|
|
440
619
|
async entry => {
|
|
441
620
|
if (irreversible || this.internals.dryRun) {
|
|
621
|
+
return this.tx(() =>
|
|
622
|
+
this.driver.runSql(
|
|
623
|
+
`DELETE FROM ${this.ddl(table)} WHERE ${condition.sql}`,
|
|
624
|
+
condition.params
|
|
625
|
+
)
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
await this.backup(entry, table, null, condition);
|
|
630
|
+
await this.batches(entry, options, match =>
|
|
631
|
+
this.driver.runSql(
|
|
632
|
+
`DELETE FROM ${this.ddl(table)} WHERE ${match.sql}`,
|
|
633
|
+
match.params
|
|
634
|
+
)
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
softDelete (table, where, options) {
|
|
641
|
+
const column = options.column;
|
|
642
|
+
const columns = this.columns(table);
|
|
643
|
+
|
|
644
|
+
if (typeof column !== 'string') {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`delete("${table}") in soft mode needs the column marking the rows ` +
|
|
647
|
+
"as deleted, { mode: 'soft', column: 'deleted_at' }"
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!columns || !columns[FLAG]) {
|
|
652
|
+
throw new Error(
|
|
653
|
+
`delete("${table}") in soft mode marks the rows in their ${FLAG} ` +
|
|
654
|
+
'column, which only tables created by v2 migrations have'
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const condition = this.where(where);
|
|
659
|
+
const key = this.key(table, options, 'delete');
|
|
660
|
+
const time = options.value === undefined;
|
|
661
|
+
|
|
662
|
+
return this.step(
|
|
663
|
+
'delete',
|
|
664
|
+
[table],
|
|
665
|
+
op => ({ t: 4, a: 'soft', c: [table, column, key, deleted(this.file, op)] }),
|
|
666
|
+
entry => {
|
|
667
|
+
const mark = entry.c[3];
|
|
668
|
+
const set =
|
|
669
|
+
`${this.ddl(column)} = ${time ? 'CURRENT_TIMESTAMP' : '?'}, ` +
|
|
670
|
+
`${this.ddl(FLAG)} = ${this.concat(`COALESCE(${this.ddl(FLAG)}, '')`)}`;
|
|
671
|
+
const value = time ? [mark] : [toValue(options.value), mark];
|
|
672
|
+
// rows deleted already, by the application or before, stay as they are
|
|
673
|
+
const active = {
|
|
674
|
+
sql: `(${condition.sql}) AND ${this.ddl(column)} IS NULL`,
|
|
675
|
+
params: condition.params
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
if (this.internals.dryRun) {
|
|
679
|
+
return this.driver.runSql(
|
|
680
|
+
`UPDATE ${this.ddl(table)} SET ${set} WHERE ${active.sql}`,
|
|
681
|
+
value.concat(active.params)
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return this.untilDone(table, key, active, options, match =>
|
|
686
|
+
this.driver.runSql(
|
|
687
|
+
`UPDATE ${this.ddl(table)} SET ${set} WHERE ${match.sql}`,
|
|
688
|
+
value.concat(match.params)
|
|
689
|
+
)
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* purge(table, [migration], [options]), deletes the rows deleted in soft
|
|
697
|
+
* mode for good, of all migrations or of the one named. It can not be
|
|
698
|
+
* reverted.
|
|
699
|
+
*/
|
|
700
|
+
purge (table, migration, options = {}) {
|
|
701
|
+
if (isObject(migration)) {
|
|
702
|
+
options = migration;
|
|
703
|
+
migration = undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const key = this.key(table, options, 'purge');
|
|
707
|
+
const mark = migration ? `|del:${migration}#` : '|del:';
|
|
708
|
+
|
|
709
|
+
return this.step(
|
|
710
|
+
'purge',
|
|
711
|
+
[table].concat(migration ? [migration] : []),
|
|
712
|
+
() => ({ t: 3, a: 'purge', c: [table] }),
|
|
713
|
+
() => {
|
|
714
|
+
const condition = {
|
|
715
|
+
sql: `${this.ddl(FLAG)} LIKE ? ESCAPE '!'`,
|
|
716
|
+
params: [`%${like(mark)}%`]
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
if (this.internals.dryRun) {
|
|
442
720
|
return this.driver.runSql(
|
|
443
721
|
`DELETE FROM ${this.ddl(table)} WHERE ${condition.sql}`,
|
|
444
722
|
condition.params
|
|
445
723
|
);
|
|
446
724
|
}
|
|
447
725
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const match = this.byKey(rows, key);
|
|
451
|
-
return this.driver.runSql(
|
|
726
|
+
return this.untilDone(table, key, condition, options, match =>
|
|
727
|
+
this.driver.runSql(
|
|
452
728
|
`DELETE FROM ${this.ddl(table)} WHERE ${match.sql}`,
|
|
453
729
|
match.params
|
|
454
|
-
)
|
|
455
|
-
|
|
730
|
+
)
|
|
731
|
+
);
|
|
456
732
|
}
|
|
457
733
|
);
|
|
458
734
|
}
|
|
@@ -486,7 +762,7 @@ class Dml {
|
|
|
486
762
|
irreversible
|
|
487
763
|
? { t: 3, a: 'runSql', c: [sql] }
|
|
488
764
|
: { t: 4, a: 'runSql', c: [revert[0], revert[1] || []] },
|
|
489
|
-
() => this.driver.runSql(sql, params)
|
|
765
|
+
() => this.tx(() => this.driver.runSql(sql, params))
|
|
490
766
|
);
|
|
491
767
|
}
|
|
492
768
|
|
|
@@ -510,15 +786,18 @@ class Dml {
|
|
|
510
786
|
const backup = this.ddl(entry.c[1]);
|
|
511
787
|
const select = columns ? columns.map(c => this.ddl(c)).join(', ') : '*';
|
|
512
788
|
|
|
789
|
+
// outside of transactions, creating tables commits them in MySQL
|
|
513
790
|
await this.driver.runSql(`DROP TABLE IF EXISTS ${backup}`);
|
|
514
791
|
await this.driver.runSql(
|
|
515
792
|
`CREATE TABLE ${backup} AS SELECT ${select} FROM ${this.ddl(table)} ` +
|
|
516
793
|
'WHERE 1 = 0'
|
|
517
794
|
);
|
|
518
|
-
await this.
|
|
519
|
-
|
|
520
|
-
`
|
|
521
|
-
|
|
795
|
+
await this.tx(() =>
|
|
796
|
+
this.driver.runSql(
|
|
797
|
+
`INSERT INTO ${backup} SELECT ${select} FROM ${this.ddl(table)} ` +
|
|
798
|
+
`WHERE ${condition.sql}`,
|
|
799
|
+
condition.params
|
|
800
|
+
)
|
|
522
801
|
);
|
|
523
802
|
|
|
524
803
|
entry.b = 1;
|
|
@@ -532,13 +811,14 @@ class Dml {
|
|
|
532
811
|
* interruption sets the same values.
|
|
533
812
|
*/
|
|
534
813
|
async batches (entry, options, change) {
|
|
535
|
-
const size =
|
|
814
|
+
const size = this.batchSize(options);
|
|
536
815
|
const backup = this.ddl(entry.c[1]);
|
|
537
|
-
const key = entry.c[2]
|
|
816
|
+
const key = entry.c[2];
|
|
817
|
+
const order = key.map(k => this.ddl(k)).join(', ');
|
|
538
818
|
|
|
539
819
|
for (;;) {
|
|
540
820
|
const rows = await this.driver.all(
|
|
541
|
-
`SELECT ${
|
|
821
|
+
`SELECT ${order} FROM ${backup} ORDER BY ${order} ` +
|
|
542
822
|
`LIMIT ${size} OFFSET ${entry.o}`
|
|
543
823
|
);
|
|
544
824
|
|
|
@@ -546,10 +826,11 @@ class Dml {
|
|
|
546
826
|
return;
|
|
547
827
|
}
|
|
548
828
|
|
|
549
|
-
await change(rows);
|
|
829
|
+
await this.tx(() => change(this.byKey(rows, key)));
|
|
550
830
|
entry.o += rows.length;
|
|
551
831
|
log.verbose(`[dml] ${this.file.name}: ${entry.o} rows ${entry.a}d`);
|
|
552
832
|
await this.save();
|
|
833
|
+
await this.between();
|
|
553
834
|
}
|
|
554
835
|
}
|
|
555
836
|
}
|
|
@@ -560,12 +841,14 @@ class Dml {
|
|
|
560
841
|
*/
|
|
561
842
|
const reverts = {
|
|
562
843
|
insert: (d, [table, flag]) =>
|
|
563
|
-
d.
|
|
564
|
-
|
|
565
|
-
|
|
844
|
+
d.tx(() =>
|
|
845
|
+
d.driver.runSql(
|
|
846
|
+
`DELETE FROM ${d.ddl(table)} WHERE ${d.ddl(FLAG)} = ?`,
|
|
847
|
+
[flag]
|
|
848
|
+
)
|
|
566
849
|
),
|
|
567
850
|
|
|
568
|
-
runSql: (d, [sql, params]) => d.driver.runSql(sql, params),
|
|
851
|
+
runSql: (d, [sql, params]) => d.tx(() => d.driver.runSql(sql, params)),
|
|
569
852
|
|
|
570
853
|
update: async (d, [table, backup, key, columns]) => {
|
|
571
854
|
let offset = 0;
|
|
@@ -583,10 +866,12 @@ const reverts = {
|
|
|
583
866
|
return;
|
|
584
867
|
}
|
|
585
868
|
|
|
586
|
-
await
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
869
|
+
await d.tx(() =>
|
|
870
|
+
Promise.each(rows, row =>
|
|
871
|
+
d.driver.runSql(
|
|
872
|
+
`UPDATE ${d.ddl(table)} SET ${assign} WHERE ${match}`,
|
|
873
|
+
columns.map(c => row[c]).concat(key.map(k => row[k]))
|
|
874
|
+
)
|
|
590
875
|
)
|
|
591
876
|
);
|
|
592
877
|
offset += rows.length;
|
|
@@ -608,12 +893,32 @@ const reverts = {
|
|
|
608
893
|
.join(' AND ');
|
|
609
894
|
|
|
610
895
|
// rows deleted by the step only, those not deleted yet stay as they are
|
|
611
|
-
await d.
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
896
|
+
await d.tx(() =>
|
|
897
|
+
d.driver.runSql(
|
|
898
|
+
`INSERT INTO ${d.ddl(table)} (${columns}) SELECT ${columns} FROM ` +
|
|
899
|
+
`${d.ddl(backup)} ${d.ddl('__dbm_b')} WHERE NOT EXISTS (SELECT 1 ` +
|
|
900
|
+
`FROM ${d.ddl(table)} ${d.ddl('__dbm_t')} WHERE ${match})`
|
|
901
|
+
)
|
|
615
902
|
);
|
|
616
|
-
}
|
|
903
|
+
},
|
|
904
|
+
|
|
905
|
+
soft: (d, [table, column, key, mark]) =>
|
|
906
|
+
d.untilDone(
|
|
907
|
+
table,
|
|
908
|
+
key,
|
|
909
|
+
{
|
|
910
|
+
sql: `${d.ddl(FLAG)} LIKE ? ESCAPE '!'`,
|
|
911
|
+
params: [`%${like(mark)}%`]
|
|
912
|
+
},
|
|
913
|
+
{},
|
|
914
|
+
match =>
|
|
915
|
+
d.driver.runSql(
|
|
916
|
+
`UPDATE ${d.ddl(table)} SET ${d.ddl(column)} = NULL, ` +
|
|
917
|
+
`${d.ddl(FLAG)} = NULLIF(REPLACE(${d.ddl(FLAG)}, ?, ''), '') ` +
|
|
918
|
+
`WHERE ${match.sql}`,
|
|
919
|
+
[mark].concat(match.params)
|
|
920
|
+
)
|
|
921
|
+
)
|
|
617
922
|
};
|
|
618
923
|
|
|
619
924
|
const irreversibleSteps = internals =>
|
|
@@ -625,8 +930,8 @@ const irreversibleSteps = internals =>
|
|
|
625
930
|
* Reverts the recorded steps, the last first. Steps with a backup table
|
|
626
931
|
* restore the rows from it and drop it afterwards.
|
|
627
932
|
*/
|
|
628
|
-
const revert = async (context, file, internals, pdriver) => {
|
|
629
|
-
|
|
933
|
+
const revert = async (context, file, internals, pdriver, d) => {
|
|
934
|
+
d = d || new Dml(context, file, internals, pdriver, null);
|
|
630
935
|
const mod = internals.modSchema;
|
|
631
936
|
|
|
632
937
|
while (mod.s.length) {
|
|
@@ -637,10 +942,10 @@ const revert = async (context, file, internals, pdriver) => {
|
|
|
637
942
|
throw new Error(`Invalid state record of a dml migration, ${entry.a}`);
|
|
638
943
|
}
|
|
639
944
|
|
|
640
|
-
if (internals.dryRun && backup) {
|
|
945
|
+
if (internals.dryRun && (backup || entry.a === 'soft')) {
|
|
641
946
|
log.info(
|
|
642
|
-
`[dml] would
|
|
643
|
-
entry.c[1]
|
|
947
|
+
`[dml] would revert ${entry.a}("${entry.c[0]}")` +
|
|
948
|
+
(backup ? ` from ${entry.c[1]}` : '')
|
|
644
949
|
);
|
|
645
950
|
} else {
|
|
646
951
|
// b: 1 the backup is complete, 2 the rows are restored from it
|
|
@@ -662,6 +967,21 @@ const revert = async (context, file, internals, pdriver) => {
|
|
|
662
967
|
await State.deleteState(pdriver, file, internals);
|
|
663
968
|
};
|
|
664
969
|
|
|
970
|
+
const irreversibleError = (file, internals) => {
|
|
971
|
+
const irreversible = irreversibleSteps(internals);
|
|
972
|
+
|
|
973
|
+
if (!irreversible.length) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const err = new Error(
|
|
978
|
+
`Migration "${file.name}" can not be reverted, it ran ` +
|
|
979
|
+
`${irreversible.join(', ')}, which can not be reverted.`
|
|
980
|
+
);
|
|
981
|
+
err.irreversible = true;
|
|
982
|
+
return err;
|
|
983
|
+
};
|
|
984
|
+
|
|
665
985
|
const opts = context => ({
|
|
666
986
|
options: context.internals.safeOptions,
|
|
667
987
|
dbm: context.internals.safeOptions.dbmigrate
|
|
@@ -675,14 +995,8 @@ const down = async (context, driver, file, { abort } = {}) => {
|
|
|
675
995
|
await State.startMigration(pdriver, file, internals, { op: 'down' });
|
|
676
996
|
}
|
|
677
997
|
|
|
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
|
-
|
|
998
|
+
const err = irreversibleError(file, internals);
|
|
999
|
+
if (err) {
|
|
686
1000
|
if (abort !== true) {
|
|
687
1001
|
await State.endMigration(pdriver, file, internals);
|
|
688
1002
|
}
|
|
@@ -695,15 +1009,16 @@ const down = async (context, driver, file, { abort } = {}) => {
|
|
|
695
1009
|
return Promise.promisify(context.deleteMigrationRecord.bind(context))(file);
|
|
696
1010
|
};
|
|
697
1011
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1012
|
+
/**
|
|
1013
|
+
* Runs the migration, after recovering an interrupted run, and rolls it
|
|
1014
|
+
* back if it fails. Shared by the foreground and the background workers.
|
|
1015
|
+
*
|
|
1016
|
+
* hooks.rollingBack: records that the rollback started
|
|
1017
|
+
* hooks.restart: records a fresh start after recovering by rollback
|
|
1018
|
+
*/
|
|
1019
|
+
const execute = async (context, file, internals, pdriver, interrupted, progress, work, hooks) => {
|
|
701
1020
|
let recovery = null;
|
|
702
1021
|
|
|
703
|
-
const interrupted = await State.startMigration(pdriver, file, internals, {
|
|
704
|
-
recover: true
|
|
705
|
-
});
|
|
706
|
-
|
|
707
1022
|
if (interrupted) {
|
|
708
1023
|
const mode = recoveryMode(file, interrupted);
|
|
709
1024
|
log.warn(
|
|
@@ -713,29 +1028,32 @@ const up = async (context, driver, file) => {
|
|
|
713
1028
|
);
|
|
714
1029
|
|
|
715
1030
|
if (mode === 'rollback') {
|
|
716
|
-
await
|
|
1031
|
+
await hooks.rollingBack();
|
|
717
1032
|
await revert(context, file, internals, pdriver);
|
|
718
1033
|
internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
|
|
719
|
-
await
|
|
720
|
-
await State.startMigration(pdriver, file, internals);
|
|
1034
|
+
await hooks.restart();
|
|
721
1035
|
} else {
|
|
722
1036
|
recovery = interrupted;
|
|
723
1037
|
}
|
|
724
1038
|
}
|
|
725
1039
|
|
|
726
1040
|
internals.migrationDone = recovery ? recovery.done : 0;
|
|
727
|
-
const db = new Dml(context, file, internals, pdriver, recovery);
|
|
1041
|
+
const db = new Dml(context, file, internals, pdriver, recovery, progress, work);
|
|
728
1042
|
|
|
729
1043
|
try {
|
|
730
1044
|
await file.get().migrate(db, opts(context));
|
|
731
1045
|
} catch (err) {
|
|
1046
|
+
if (err && err.stopped) {
|
|
1047
|
+
throw err;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
732
1050
|
describeFailure(err, db, internals);
|
|
733
1051
|
|
|
734
1052
|
if (irreversibleSteps(internals).length) {
|
|
735
1053
|
log.error(
|
|
736
1054
|
`Migration "${file.name}" failed and can not be rolled back, it ` +
|
|
737
|
-
'ran a step
|
|
738
|
-
'
|
|
1055
|
+
'ran a step which can not be reverted. The steps executed stay, ' +
|
|
1056
|
+
'the next run continues after them.'
|
|
739
1057
|
);
|
|
740
1058
|
throw err;
|
|
741
1059
|
}
|
|
@@ -746,8 +1064,34 @@ const up = async (context, driver, file) => {
|
|
|
746
1064
|
(err && err.message) || err
|
|
747
1065
|
}`
|
|
748
1066
|
);
|
|
749
|
-
await
|
|
750
|
-
await
|
|
1067
|
+
await hooks.rollingBack();
|
|
1068
|
+
await revert(context, file, internals, pdriver, db);
|
|
1069
|
+
err.rolledBack = true;
|
|
1070
|
+
throw err;
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
const up = async (context, driver, file) => {
|
|
1075
|
+
const internals = context.internals;
|
|
1076
|
+
const pdriver = context._pdriver;
|
|
1077
|
+
|
|
1078
|
+
const interrupted = await State.startMigration(pdriver, file, internals, {
|
|
1079
|
+
recover: true
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
try {
|
|
1083
|
+
await execute(context, file, internals, pdriver, interrupted, null, {}, {
|
|
1084
|
+
rollingBack: () => State.rollingBack(pdriver, internals),
|
|
1085
|
+
restart: async () => {
|
|
1086
|
+
await State.endMigration(pdriver, file, internals);
|
|
1087
|
+
await State.startMigration(pdriver, file, internals);
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
if (err && err.rolledBack) {
|
|
1092
|
+
await State.endMigration(pdriver, file, internals);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
751
1095
|
throw err;
|
|
752
1096
|
}
|
|
753
1097
|
|
|
@@ -755,4 +1099,17 @@ const up = async (context, driver, file) => {
|
|
|
755
1099
|
return State.endMigration(pdriver, file, internals);
|
|
756
1100
|
};
|
|
757
1101
|
|
|
758
|
-
module.exports = {
|
|
1102
|
+
module.exports = {
|
|
1103
|
+
isDml,
|
|
1104
|
+
isBackground,
|
|
1105
|
+
checkType,
|
|
1106
|
+
up,
|
|
1107
|
+
down,
|
|
1108
|
+
execute,
|
|
1109
|
+
revert,
|
|
1110
|
+
irreversibleError,
|
|
1111
|
+
Dml,
|
|
1112
|
+
Stopped,
|
|
1113
|
+
FLAG,
|
|
1114
|
+
insertRows
|
|
1115
|
+
};
|