db-migrate 1.1.0 → 1.3.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/lib/dml.js ADDED
@@ -0,0 +1,758 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const Promise = require('bluebird');
5
+ const log = require('db-migrate-shared').log;
6
+ const State = require('./state');
7
+ const { recoveryMode, describeFailure } = require('./recovery');
8
+
9
+ /**
10
+ * DML migrations, _meta: { version: 2, type: 'dml' }, change data instead of
11
+ * the schema. Every instruction is a step, recorded with what reverts it:
12
+ *
13
+ * insert: the rows get the step in their __dbmigrate__flag column, reverting
14
+ * deletes the rows flagged with it
15
+ * update, delete: the rows are copied to a backup table in the same database
16
+ * first, reverting restores them from there. The rows are then
17
+ * changed in batches by their key, the progress is kept in the
18
+ * state, so an interrupted run continues where it stopped.
19
+ * runSql: reverted by the SQL given with { revert }
20
+ *
21
+ * The records are { t: 4, a: action, c: args }, or { t: 3 } for a step run
22
+ * with { irreversible: true }, which can not be reverted.
23
+ */
24
+
25
+ const FLAG = '__dbmigrate__flag';
26
+ const BATCH = 1000;
27
+ const TYPES = ['dml'];
28
+
29
+ const SCHEMA_INSTRUCTIONS = [
30
+ 'createTable',
31
+ 'dropTable',
32
+ 'renameTable',
33
+ 'addColumn',
34
+ 'removeColumn',
35
+ 'renameColumn',
36
+ 'changeColumn',
37
+ 'addIndex',
38
+ 'removeIndex',
39
+ 'addForeignKey',
40
+ 'removeForeignKey',
41
+ 'createCollection',
42
+ 'dropCollection',
43
+ 'renameCollection'
44
+ ];
45
+
46
+ const isDml = file => (file.get()._meta || {}).type === 'dml';
47
+
48
+ const checkType = file => {
49
+ const type = (file.get()._meta || {}).type;
50
+
51
+ if (type !== undefined && TYPES.indexOf(type) === -1) {
52
+ throw new Error(
53
+ `Invalid migration type "${type}" in migration "${file.name}", use ` +
54
+ TYPES.join(', ') +
55
+ ' or leave it out for a schema migration'
56
+ );
57
+ }
58
+ };
59
+
60
+ const backupName = (file, op) =>
61
+ '__dbm_backup_' +
62
+ crypto
63
+ .createHash('sha256')
64
+ .update(`${file.name}#${op}`)
65
+ .digest('hex')
66
+ .slice(0, 16);
67
+
68
+ const isObject = x =>
69
+ x !== null &&
70
+ typeof x === 'object' &&
71
+ !Array.isArray(x) &&
72
+ !(x instanceof Date) &&
73
+ !Buffer.isBuffer(x);
74
+
75
+ // like db-migrate-base, objects and arrays are stored as JSON
76
+ const toValue = value => {
77
+ if (
78
+ value !== null &&
79
+ typeof value === 'object' &&
80
+ !(value instanceof Date) &&
81
+ !Buffer.isBuffer(value)
82
+ ) {
83
+ return JSON.stringify(value);
84
+ }
85
+
86
+ return value === undefined ? null : value;
87
+ };
88
+
89
+ /**
90
+ * The rows of an insert as objects, from any form db-migrate-base takes,
91
+ * and the options following them.
92
+ */
93
+ const insertRows = args => {
94
+ if (Array.isArray(args[0]) && Array.isArray(args[1])) {
95
+ const [columns, values, options = {}] = args;
96
+ const rows =
97
+ !values.length || Array.isArray(values[0]) ? values : [values];
98
+
99
+ return {
100
+ options,
101
+ rows: rows.map(row => {
102
+ if (!Array.isArray(row) || row.length !== columns.length) {
103
+ throw new Error(
104
+ 'The number of columns does not match the number of values.'
105
+ );
106
+ }
107
+
108
+ const object = {};
109
+ columns.forEach((column, i) => {
110
+ object[column] = row[i];
111
+ });
112
+ return object;
113
+ })
114
+ };
115
+ }
116
+
117
+ const [data, options = {}] = args;
118
+
119
+ if (isObject(data) && Array.isArray(data.columns)) {
120
+ const values = [];
121
+ for (let i = 0; i < data.data.length; i += data.columns.length) {
122
+ values.push(data.data.slice(i, i + data.columns.length));
123
+ }
124
+
125
+ return insertRows([data.columns, values, options]);
126
+ }
127
+
128
+ if (isObject(data)) {
129
+ return { rows: [data], options };
130
+ }
131
+
132
+ if (Array.isArray(data) && data.every(isObject)) {
133
+ return { rows: data, options };
134
+ }
135
+
136
+ throw new Error('insert needs the rows to insert');
137
+ };
138
+
139
+ class Dml {
140
+ constructor (context, file, internals, pdriver, recovery) {
141
+ this.driver = context._driver;
142
+ this.file = file;
143
+ this.internals = internals;
144
+ this.pdriver = pdriver;
145
+ this.recovery = recovery;
146
+ this.op = 0;
147
+ this.current = null;
148
+
149
+ SCHEMA_INSTRUCTIONS.forEach(method => {
150
+ this[method] = () => {
151
+ throw new Error(
152
+ `${method} is a schema instruction, which dml migrations do not ` +
153
+ 'allow. Change the schema in a v2 migration of its own.'
154
+ );
155
+ };
156
+ });
157
+ }
158
+
159
+ ddl (name) {
160
+ return this.driver.escapeDDL(name);
161
+ }
162
+
163
+ columns (table) {
164
+ const t = this.internals.schema.c[table];
165
+ return t ? t.columns || t : null;
166
+ }
167
+
168
+ /**
169
+ * The key of a table, to change and restore its rows by. Taken from the
170
+ * schema, for other tables it is passed with { key }.
171
+ */
172
+ key (table, options, action) {
173
+ if (options.key) {
174
+ return [].concat(options.key);
175
+ }
176
+
177
+ const columns = this.columns(table) || {};
178
+ const key = Object.keys(columns).filter(c => columns[c].primaryKey);
179
+
180
+ if (!key.length) {
181
+ throw new Error(
182
+ `${action}("${table}") needs the primary key of "${table}" to revert ` +
183
+ 'it, which is unknown to the schema of db-migrate. Pass it with ' +
184
+ "{ key: 'id' }, or pass { irreversible: true } to change the rows " +
185
+ 'without being able to revert it.'
186
+ );
187
+ }
188
+
189
+ return key;
190
+ }
191
+
192
+ where (where) {
193
+ if (typeof where === 'string') {
194
+ return { sql: where, params: [] };
195
+ }
196
+
197
+ if (Array.isArray(where) && typeof where[0] === 'string') {
198
+ return { sql: where[0], params: where[1] || [] };
199
+ }
200
+
201
+ if (isObject(where)) {
202
+ const params = [];
203
+ const parts = Object.keys(where).map(column => {
204
+ const value = where[column];
205
+
206
+ if (value === null || value === undefined) {
207
+ return `${this.ddl(column)} IS NULL`;
208
+ }
209
+
210
+ if (Array.isArray(value)) {
211
+ if (!value.length) {
212
+ return '1 = 0';
213
+ }
214
+
215
+ params.push(...value.map(toValue));
216
+ return `${this.ddl(column)} IN (${value.map(() => '?').join(', ')})`;
217
+ }
218
+
219
+ params.push(toValue(value));
220
+ return `${this.ddl(column)} = ?`;
221
+ });
222
+
223
+ return { sql: parts.length ? parts.join(' AND ') : '1 = 1', params };
224
+ }
225
+
226
+ throw new Error(
227
+ "where is an object like { status: 'old' }, a SQL string or " +
228
+ '[sql, params]'
229
+ );
230
+ }
231
+
232
+ // matches the rows by their key
233
+ byKey (rows, key) {
234
+ const params = [];
235
+
236
+ if (key.length === 1) {
237
+ rows.forEach(row => params.push(row[key[0]]));
238
+ return {
239
+ sql: `${this.ddl(key[0])} IN (${rows.map(() => '?').join(', ')})`,
240
+ params
241
+ };
242
+ }
243
+
244
+ const sql = rows
245
+ .map(row => {
246
+ key.forEach(k => params.push(row[k]));
247
+ return '(' + key.map(k => `${this.ddl(k)} = ?`).join(' AND ') + ')';
248
+ })
249
+ .join(' OR ');
250
+
251
+ return { sql, params };
252
+ }
253
+
254
+ /**
255
+ * Runs one instruction as a step: records what reverts it, executes it and
256
+ * records it as done. The step of an interrupted run is continued, with
257
+ * the record it left.
258
+ */
259
+ async step (action, args, record, execute) {
260
+ const op = ++this.op;
261
+ const internals = this.internals;
262
+ const mod = internals.modSchema;
263
+ const recovery = this.recovery;
264
+ this.current = `${action}(${args.map(a => JSON.stringify(a)).join(', ')})`;
265
+ internals.migrationOp = op;
266
+
267
+ if (recovery && op <= recovery.done) {
268
+ log.info(
269
+ `[recovery] ${this.file.name}: skipping already executed step ` +
270
+ `${op}/${recovery.done} ${this.current}`
271
+ );
272
+ return;
273
+ }
274
+
275
+ await State.step(this.pdriver, op, internals);
276
+
277
+ let entry = recovery && op <= recovery.learned
278
+ ? mod.s.find(e => e.n === op)
279
+ : null;
280
+
281
+ if (entry) {
282
+ log.info(
283
+ `[recovery] ${this.file.name}: continuing interrupted step ${op} ` +
284
+ this.current
285
+ );
286
+ } else {
287
+ mod.s = mod.s.filter(e => e.n !== op);
288
+ entry = record(op);
289
+ entry.n = op;
290
+ mod.s.push(entry);
291
+ await State.update(this.pdriver, this.file, mod, internals);
292
+ await State.learned(this.pdriver, op, internals);
293
+ }
294
+
295
+ await execute(entry, op);
296
+
297
+ internals.migrationDone = op;
298
+ await State.done(this.pdriver, op, internals);
299
+ }
300
+
301
+ save () {
302
+ return State.update(
303
+ this.pdriver,
304
+ this.file,
305
+ this.internals.modSchema,
306
+ this.internals
307
+ );
308
+ }
309
+
310
+ /**
311
+ * insert(table, rows, [options]), rows in any form of the SQL API insert.
312
+ */
313
+ insert (table, ...args) {
314
+ const { rows, options } = insertRows(args);
315
+ const irreversible = options.irreversible === true;
316
+
317
+ if (!irreversible) {
318
+ const columns = this.columns(table);
319
+
320
+ if (!columns || !columns[FLAG]) {
321
+ throw new Error(
322
+ `insert("${table}") can only be reverted for tables created by v2 ` +
323
+ `migrations, by their ${FLAG} column` +
324
+ (columns ? `, which "${table}" does not have` : '') +
325
+ '. Pass { irreversible: true } to insert the rows without being ' +
326
+ 'able to revert it.'
327
+ );
328
+ }
329
+
330
+ if (typeof this.driver._insertGroups !== 'function') {
331
+ throw new Error(
332
+ 'dml migrations need db-migrate-base 2.5.0 or newer, update your ' +
333
+ 'driver'
334
+ );
335
+ }
336
+ }
337
+
338
+ return this.step(
339
+ 'insert',
340
+ [table],
341
+ op =>
342
+ irreversible
343
+ ? { t: 3, a: 'insert', c: [table] }
344
+ : { t: 4, a: 'insert', c: [table, `${this.file.name}#${op}`] },
345
+ async entry => {
346
+ if (!rows.length) {
347
+ return;
348
+ }
349
+
350
+ if (irreversible) {
351
+ return this.driver.insert(table, rows);
352
+ }
353
+
354
+ const flag = entry.c[1];
355
+ // rows of an interrupted run of this step
356
+ await this.driver.runSql(
357
+ `DELETE FROM ${this.ddl(table)} WHERE ${this.ddl(FLAG)} = ?`,
358
+ [flag]
359
+ );
360
+ await this.driver.insert(
361
+ table,
362
+ rows.map(row => Object.assign({}, row, { [FLAG]: flag }))
363
+ );
364
+ }
365
+ );
366
+ }
367
+
368
+ /**
369
+ * update(table, set, where, [options]), set the values of the columns in
370
+ * set for the rows matching where.
371
+ */
372
+ update (table, set, where, options = {}) {
373
+ if (!isObject(set) || !Object.keys(set).length) {
374
+ throw new Error(`update("${table}") needs the values to set`);
375
+ }
376
+
377
+ const columns = Object.keys(set);
378
+ const condition = this.where(where);
379
+ const irreversible = options.irreversible === true;
380
+ const key = irreversible ? null : this.key(table, options, 'update');
381
+ const assign = columns.map(c => `${this.ddl(c)} = ?`).join(', ');
382
+ const values = columns.map(c => toValue(set[c]));
383
+
384
+ if (key) {
385
+ const changed = key.filter(k => columns.indexOf(k) !== -1);
386
+ if (changed.length) {
387
+ throw new Error(
388
+ `update("${table}") can not change the key ${changed.join(', ')}, ` +
389
+ 'by which it reverts the rows'
390
+ );
391
+ }
392
+ }
393
+
394
+ return this.step(
395
+ 'update',
396
+ [table, set],
397
+ op =>
398
+ irreversible
399
+ ? { t: 3, a: 'update', c: [table] }
400
+ : {
401
+ t: 4,
402
+ a: 'update',
403
+ c: [table, backupName(this.file, op), key, columns]
404
+ },
405
+ async entry => {
406
+ if (irreversible || this.internals.dryRun) {
407
+ return this.driver.runSql(
408
+ `UPDATE ${this.ddl(table)} SET ${assign} WHERE ${condition.sql}`,
409
+ values.concat(condition.params)
410
+ );
411
+ }
412
+
413
+ await this.backup(entry, table, key.concat(columns), condition);
414
+ await this.batches(entry, options, rows => {
415
+ const match = this.byKey(rows, key);
416
+ return this.driver.runSql(
417
+ `UPDATE ${this.ddl(table)} SET ${assign} WHERE ${match.sql}`,
418
+ values.concat(match.params)
419
+ );
420
+ });
421
+ }
422
+ );
423
+ }
424
+
425
+ /**
426
+ * delete(table, where, [options]), delete the rows matching where.
427
+ */
428
+ delete (table, where, options = {}) {
429
+ const condition = this.where(where);
430
+ const irreversible = options.irreversible === true;
431
+ const key = irreversible ? null : this.key(table, options, 'delete');
432
+
433
+ return this.step(
434
+ 'delete',
435
+ [table],
436
+ op =>
437
+ irreversible
438
+ ? { t: 3, a: 'delete', c: [table] }
439
+ : { t: 4, a: 'delete', c: [table, backupName(this.file, op), key] },
440
+ async entry => {
441
+ if (irreversible || this.internals.dryRun) {
442
+ return this.driver.runSql(
443
+ `DELETE FROM ${this.ddl(table)} WHERE ${condition.sql}`,
444
+ condition.params
445
+ );
446
+ }
447
+
448
+ await this.backup(entry, table, null, condition);
449
+ await this.batches(entry, options, rows => {
450
+ const match = this.byKey(rows, key);
451
+ return this.driver.runSql(
452
+ `DELETE FROM ${this.ddl(table)} WHERE ${match.sql}`,
453
+ match.params
454
+ );
455
+ });
456
+ }
457
+ );
458
+ }
459
+
460
+ /**
461
+ * runSql(sql, [params], { revert } | { irreversible: true }), revert is
462
+ * the SQL reverting it, or [sql, params].
463
+ */
464
+ runSql (sql, ...args) {
465
+ const params = Array.isArray(args[0]) ? args.shift() : [];
466
+ const options = args[0] || {};
467
+ const irreversible = options.irreversible === true;
468
+ const revert =
469
+ typeof options.revert === 'string' ? [options.revert, []] : options.revert;
470
+
471
+ if (
472
+ !irreversible &&
473
+ (!Array.isArray(revert) || typeof revert[0] !== 'string')
474
+ ) {
475
+ throw new Error(
476
+ 'runSql in a dml migration needs the SQL reverting it, ' +
477
+ "runSql(sql, { revert: 'sql' }), or { irreversible: true } to run " +
478
+ 'it without being able to revert it.'
479
+ );
480
+ }
481
+
482
+ return this.step(
483
+ 'runSql',
484
+ [sql],
485
+ () =>
486
+ irreversible
487
+ ? { t: 3, a: 'runSql', c: [sql] }
488
+ : { t: 4, a: 'runSql', c: [revert[0], revert[1] || []] },
489
+ () => this.driver.runSql(sql, params)
490
+ );
491
+ }
492
+
493
+ /**
494
+ * Reading is no step, it does not change anything.
495
+ */
496
+ all (...args) {
497
+ return this.driver.all(...args);
498
+ }
499
+
500
+ /**
501
+ * Copies the rows about to change into the backup table of the step, only
502
+ * once: a backup completed by an interrupted run holds the rows as they
503
+ * were before the step.
504
+ */
505
+ async backup (entry, table, columns, condition) {
506
+ if (entry.b) {
507
+ return;
508
+ }
509
+
510
+ const backup = this.ddl(entry.c[1]);
511
+ const select = columns ? columns.map(c => this.ddl(c)).join(', ') : '*';
512
+
513
+ await this.driver.runSql(`DROP TABLE IF EXISTS ${backup}`);
514
+ await this.driver.runSql(
515
+ `CREATE TABLE ${backup} AS SELECT ${select} FROM ${this.ddl(table)} ` +
516
+ 'WHERE 1 = 0'
517
+ );
518
+ await this.driver.runSql(
519
+ `INSERT INTO ${backup} SELECT ${select} FROM ${this.ddl(table)} ` +
520
+ `WHERE ${condition.sql}`,
521
+ condition.params
522
+ );
523
+
524
+ entry.b = 1;
525
+ entry.o = 0;
526
+ await this.save();
527
+ }
528
+
529
+ /**
530
+ * Changes the rows of the backup in batches, ordered by their key. The
531
+ * progress is saved after each batch, changing a batch again after an
532
+ * interruption sets the same values.
533
+ */
534
+ async batches (entry, options, change) {
535
+ const size = Number(options.batch) || BATCH;
536
+ const backup = this.ddl(entry.c[1]);
537
+ const key = entry.c[2].map(k => this.ddl(k)).join(', ');
538
+
539
+ for (;;) {
540
+ const rows = await this.driver.all(
541
+ `SELECT ${key} FROM ${backup} ORDER BY ${key} ` +
542
+ `LIMIT ${size} OFFSET ${entry.o}`
543
+ );
544
+
545
+ if (!rows.length) {
546
+ return;
547
+ }
548
+
549
+ await change(rows);
550
+ entry.o += rows.length;
551
+ log.verbose(`[dml] ${this.file.name}: ${entry.o} rows ${entry.a}d`);
552
+ await this.save();
553
+ }
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Reverts the record of one step. Each is safe to run again, so a revert
559
+ * interrupted in between is continued by running it again.
560
+ */
561
+ const reverts = {
562
+ insert: (d, [table, flag]) =>
563
+ d.driver.runSql(
564
+ `DELETE FROM ${d.ddl(table)} WHERE ${d.ddl(FLAG)} = ?`,
565
+ [flag]
566
+ ),
567
+
568
+ runSql: (d, [sql, params]) => d.driver.runSql(sql, params),
569
+
570
+ update: async (d, [table, backup, key, columns]) => {
571
+ let offset = 0;
572
+ const order = key.map(k => d.ddl(k)).join(', ');
573
+ const assign = columns.map(c => `${d.ddl(c)} = ?`).join(', ');
574
+ const match = key.map(k => `${d.ddl(k)} = ?`).join(' AND ');
575
+
576
+ for (;;) {
577
+ const rows = await d.driver.all(
578
+ `SELECT * FROM ${d.ddl(backup)} ORDER BY ${order} ` +
579
+ `LIMIT ${BATCH} OFFSET ${offset}`
580
+ );
581
+
582
+ if (!rows.length) {
583
+ return;
584
+ }
585
+
586
+ await Promise.each(rows, row =>
587
+ d.driver.runSql(
588
+ `UPDATE ${d.ddl(table)} SET ${assign} WHERE ${match}`,
589
+ columns.map(c => row[c]).concat(key.map(k => row[k]))
590
+ )
591
+ );
592
+ offset += rows.length;
593
+ }
594
+ },
595
+
596
+ delete: async (d, [table, backup, key]) => {
597
+ const [row] = await d.driver.all(`SELECT * FROM ${d.ddl(backup)} LIMIT 1`);
598
+
599
+ if (!row) {
600
+ return;
601
+ }
602
+
603
+ const columns = Object.keys(row)
604
+ .map(c => d.ddl(c))
605
+ .join(', ');
606
+ const match = key
607
+ .map(k => `${d.ddl('__dbm_t')}.${d.ddl(k)} = ${d.ddl('__dbm_b')}.${d.ddl(k)}`)
608
+ .join(' AND ');
609
+
610
+ // rows deleted by the step only, those not deleted yet stay as they are
611
+ await d.driver.runSql(
612
+ `INSERT INTO ${d.ddl(table)} (${columns}) SELECT ${columns} FROM ` +
613
+ `${d.ddl(backup)} ${d.ddl('__dbm_b')} WHERE NOT EXISTS (SELECT 1 ` +
614
+ `FROM ${d.ddl(table)} ${d.ddl('__dbm_t')} WHERE ${match})`
615
+ );
616
+ }
617
+ };
618
+
619
+ const irreversibleSteps = internals =>
620
+ internals.modSchema.s
621
+ .filter(entry => entry.t === 3)
622
+ .map(entry => `step ${entry.n} ${entry.a}("${entry.c[0]}")`);
623
+
624
+ /**
625
+ * Reverts the recorded steps, the last first. Steps with a backup table
626
+ * restore the rows from it and drop it afterwards.
627
+ */
628
+ const revert = async (context, file, internals, pdriver) => {
629
+ const d = new Dml(context, file, internals, pdriver, null);
630
+ const mod = internals.modSchema;
631
+
632
+ while (mod.s.length) {
633
+ const entry = mod.s[mod.s.length - 1];
634
+ const backup = entry.a === 'update' || entry.a === 'delete';
635
+
636
+ if (entry.t !== 4 || !reverts[entry.a]) {
637
+ throw new Error(`Invalid state record of a dml migration, ${entry.a}`);
638
+ }
639
+
640
+ if (internals.dryRun && backup) {
641
+ log.info(
642
+ `[dml] would restore the rows of ${entry.a}("${entry.c[0]}") from ` +
643
+ entry.c[1]
644
+ );
645
+ } else {
646
+ // b: 1 the backup is complete, 2 the rows are restored from it
647
+ if (!backup || entry.b === 1) {
648
+ await reverts[entry.a](d, entry.c);
649
+ }
650
+
651
+ if (backup) {
652
+ entry.b = 2;
653
+ await d.save();
654
+ await d.driver.runSql(`DROP TABLE IF EXISTS ${d.ddl(entry.c[1])}`);
655
+ }
656
+ }
657
+
658
+ mod.s.pop();
659
+ await d.save();
660
+ }
661
+
662
+ await State.deleteState(pdriver, file, internals);
663
+ };
664
+
665
+ const opts = context => ({
666
+ options: context.internals.safeOptions,
667
+ dbm: context.internals.safeOptions.dbmigrate
668
+ });
669
+
670
+ const down = async (context, driver, file, { abort } = {}) => {
671
+ const internals = context.internals;
672
+ const pdriver = context._pdriver;
673
+
674
+ if (abort !== true) {
675
+ await State.startMigration(pdriver, file, internals, { op: 'down' });
676
+ }
677
+
678
+ const irreversible = irreversibleSteps(internals);
679
+ if (irreversible.length) {
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
+
686
+ if (abort !== true) {
687
+ await State.endMigration(pdriver, file, internals);
688
+ }
689
+
690
+ throw err;
691
+ }
692
+
693
+ await revert(context, file, internals, pdriver);
694
+ await State.endMigration(pdriver, file, internals);
695
+ return Promise.promisify(context.deleteMigrationRecord.bind(context))(file);
696
+ };
697
+
698
+ const up = async (context, driver, file) => {
699
+ const internals = context.internals;
700
+ const pdriver = context._pdriver;
701
+ let recovery = null;
702
+
703
+ const interrupted = await State.startMigration(pdriver, file, internals, {
704
+ recover: true
705
+ });
706
+
707
+ if (interrupted) {
708
+ const mode = recoveryMode(file, interrupted);
709
+ log.warn(
710
+ `[recovery] ${file.name}: the previous run was interrupted at step ` +
711
+ `${interrupted.step}, ${interrupted.done} steps were executed, ` +
712
+ `recovering by ${mode}`
713
+ );
714
+
715
+ if (mode === 'rollback') {
716
+ await State.rollingBack(pdriver, internals);
717
+ await revert(context, file, internals, pdriver);
718
+ internals.modSchema = { i: {}, c: {}, f: {}, s: [] };
719
+ await State.endMigration(pdriver, file, internals);
720
+ await State.startMigration(pdriver, file, internals);
721
+ } else {
722
+ recovery = interrupted;
723
+ }
724
+ }
725
+
726
+ internals.migrationDone = recovery ? recovery.done : 0;
727
+ const db = new Dml(context, file, internals, pdriver, recovery);
728
+
729
+ try {
730
+ await file.get().migrate(db, opts(context));
731
+ } catch (err) {
732
+ describeFailure(err, db, internals);
733
+
734
+ if (irreversibleSteps(internals).length) {
735
+ log.error(
736
+ `Migration "${file.name}" failed and can not be rolled back, it ` +
737
+ 'ran a step with { irreversible: true }. The steps executed ' +
738
+ 'stay, the next run continues after them.'
739
+ );
740
+ throw err;
741
+ }
742
+
743
+ // the record of the failed step reverts what it changed, if anything
744
+ log.error(
745
+ `Migration "${file.name}" failed, rolling back: ${
746
+ (err && err.message) || err
747
+ }`
748
+ );
749
+ await State.rollingBack(pdriver, internals);
750
+ await down(context, driver, file, { abort: true });
751
+ throw err;
752
+ }
753
+
754
+ await Promise.promisify(context.writeMigrationRecord.bind(context))(file);
755
+ return State.endMigration(pdriver, file, internals);
756
+ };
757
+
758
+ module.exports = { isDml, checkType, up, down, Dml, FLAG, insertRows };