db-migrate 1.0.0-beta.9 → 1.0.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.
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ const util = require('util');
4
+
5
+ /**
6
+ * Diagnostic fields drivers set on their errors (pg, cockroachdb, mysql2,
7
+ * sqlite3), printed when present. Everything else, like the server source
8
+ * location of pg errors, is only printed verbose.
9
+ */
10
+ const FIELDS = [
11
+ 'code',
12
+ 'errno',
13
+ 'sqlState',
14
+ 'sqlMessage',
15
+ 'detail',
16
+ 'hint',
17
+ 'where',
18
+ 'schema',
19
+ 'table',
20
+ 'column',
21
+ 'dataType',
22
+ 'constraint'
23
+ ];
24
+
25
+ const indent = (text, prefix) =>
26
+ String(text)
27
+ .split('\n')
28
+ .map(line => prefix + line)
29
+ .join('\n');
30
+
31
+ /**
32
+ * The SQL with a marker below the position reported by the database.
33
+ * Without parameters only, drivers rewrite their placeholders before
34
+ * sending, so the position would be off otherwise.
35
+ */
36
+ const formatSql = err => {
37
+ // the position refers to the statement as sent, so nothing is trimmed
38
+ // before locating it, leading blank lines are only dropped for display
39
+ const sql = String(err.sql).replace(/\s+$/, '');
40
+ const lines = sql.split('\n');
41
+ let first = 0;
42
+ while (first < lines.length - 1 && lines[first].trim() === '') first++;
43
+
44
+ const position = Number(err.position);
45
+ let marker = null;
46
+
47
+ if (Number.isInteger(position) && position >= 1 && position <= sql.length + 1) {
48
+ if (err.sqlParams) {
49
+ marker = { text: `position: ${position}` };
50
+ } else {
51
+ const before = sql.slice(0, position - 1);
52
+ const row = before.split('\n').length - 1;
53
+ const column = before.length - (before.lastIndexOf('\n') + 1);
54
+ // keep tabs, so the marker lines up with the statement
55
+ marker = {
56
+ row,
57
+ text: ' ' + lines[row].slice(0, column).replace(/[^\t]/g, ' ') + '^'
58
+ };
59
+ }
60
+ }
61
+
62
+ const out = [];
63
+ lines.slice(first).forEach((line, i) => {
64
+ out.push((i === 0 ? 'SQL: ' : ' ') + line);
65
+ if (marker && marker.row === first + i) out.push(marker.text);
66
+ });
67
+
68
+ if (marker && marker.row === undefined) out.push(marker.text);
69
+
70
+ return out;
71
+ };
72
+
73
+ /**
74
+ * Format an error of a failed migration for the log: what failed, the SQL
75
+ * and the diagnostic fields of the driver, and always the stack.
76
+ */
77
+ module.exports = function formatError (err, { verbose = false } = {}) {
78
+ if (!err || typeof err !== 'object') {
79
+ return String(err);
80
+ }
81
+
82
+ const message = err.message || String(err);
83
+ const lines = [
84
+ err.migration
85
+ ? `Migration "${err.migration}" failed` +
86
+ (err.instruction ? ` ${err.instruction}` : '') +
87
+ `: ${message}`
88
+ : message
89
+ ];
90
+
91
+ if (err.sql) {
92
+ lines.push(indent(formatSql(err).join('\n'), ' '));
93
+ }
94
+
95
+ FIELDS.forEach(field => {
96
+ const value = err[field];
97
+
98
+ if (
99
+ value !== undefined &&
100
+ value !== null &&
101
+ value !== '' &&
102
+ !(field === 'sqlMessage' && value === message)
103
+ ) {
104
+ lines.push(` ${field}: ${value}`);
105
+ }
106
+ });
107
+
108
+ if (err.position !== undefined && !err.sql) {
109
+ lines.push(` position: ${err.position}`);
110
+ }
111
+
112
+ lines.push(indent(err.stack || `${err.name}: ${message}`, ' '));
113
+
114
+ if (verbose) {
115
+ lines.push(indent(util.inspect(err, { depth: 4 }), ' '));
116
+ }
117
+
118
+ return lines.join('\n');
119
+ };
@@ -34,7 +34,7 @@
34
34
  * To note: If you rollback a migration, linked to a seeder, db-migrate will
35
35
  * also rollback the seed. This is also a reason why you can't rollback a
36
36
  * specific migration, you would going to break that much, you probably loose
37
- * a bunch of valueable time.
37
+ * a bunch of valuable time.
38
38
  */
39
39
 
40
40
  var Promise = require('bluebird');
package/lib/learn.js CHANGED
@@ -2,25 +2,34 @@ const Promise = require('bluebird');
2
2
  const Shadow = require('./driver/shadow');
3
3
 
4
4
  class STD {
5
- constructor ({ schema, modSchema: mod }, driver) {
5
+ constructor ({ schema, modSchema: mod, unlearn }, driver) {
6
6
  this.checkColumn = function (t, c) {
7
7
  if (!this.schema[t]) {
8
8
  throw new Error(`There is no ${t} table in schema!`);
9
9
  }
10
10
 
11
- if (!this.schema[t][c]) {
11
+ if (!this.schema[t][c] && !this.schema[t].columns?.[c]) {
12
12
  throw new Error(`There is no ${c} column in schema!`);
13
13
  }
14
14
  };
15
+ this.unlearn = unlearn;
15
16
  this.validations = {};
16
17
  this.driver = driver;
17
18
  this.indizies = schema.i;
18
19
  this.schema = schema.c;
19
20
  this.foreign = schema.f;
21
+ if (!schema.e) {
22
+ schema.e = {};
23
+ }
24
+ this.extra = schema.e;
25
+ if (unlearn) {
26
+ this.modC = [];
27
+ } else {
28
+ this.modC = mod.s;
29
+ }
20
30
  this.modS = mod.c;
21
31
  this.modI = mod.i;
22
32
  this.modF = mod.f;
23
- this.modC = mod.s;
24
33
  }
25
34
 
26
35
  dropTable (t) {
@@ -87,6 +96,16 @@ class STD {
87
96
  delete this.schema[t];
88
97
  }
89
98
 
99
+ if (this.foreign[t]) {
100
+ this.modF[n] = this.foreign[n];
101
+ delete this.foreign[t];
102
+ }
103
+
104
+ if (this.indizies[t]) {
105
+ this.modI[n] = this.indizies[t];
106
+ delete this.indizies[t];
107
+ }
108
+
90
109
  this.modC.push({ t: 0, a: 'renameTable', c: [n, t] });
91
110
 
92
111
  return Promise.resolve();
@@ -107,8 +126,10 @@ class STD {
107
126
  removeColumn (t, c, o = {}) {
108
127
  const alter = {};
109
128
  this.checkColumn(t, c);
129
+ const hasColumns = this.schema[t].columns !== undefined;
130
+ const columns = hasColumns ? this.schema[t].columns : this.schema[t];
110
131
 
111
- if (this.schema[t][c].notNull === true) {
132
+ if (columns[c].notNull === true && !this.unlearn) {
112
133
  if (this.validations.columnStrategies !== true) {
113
134
  if (
114
135
  this.driver._meta &&
@@ -154,12 +175,13 @@ class STD {
154
175
  }
155
176
  }
156
177
 
157
- this.modS[t] = {};
178
+ // keep the columns removed before from the same table
179
+ if (!this.modS[t]) this.modS[t] = {};
158
180
 
159
- if (this.schema[t][c].notNull === true) {
181
+ if (columns[c].notNull === true && !this.unlearn) {
160
182
  switch (o.columnStrategy) {
161
183
  case 'delay':
162
- this.modS[t][c] = this.schema[t][c];
184
+ this.modS[t][c] = columns[c];
163
185
 
164
186
  o.passthrough = o.passthrough || {};
165
187
  o.passthrough.column =
@@ -174,19 +196,23 @@ class STD {
174
196
 
175
197
  break;
176
198
  case 'defaultValue':
177
- this.modS[t][c] = this.schema[t][c];
199
+ this.modS[t][c] = columns[c];
178
200
  this.modS[t][c].defaultValue = o.passthrough.defaultValue;
179
201
 
180
202
  this.modC.push({ t: 1, a: 'addColumn', c: [t, c, o] });
181
203
  break;
182
204
  }
183
- } else {
184
- this.modS[t][c] = this.schema[t][c];
205
+ } else if (!this.unlearn) {
206
+ this.modS[t][c] = columns[c];
185
207
 
186
208
  this.modC.push({ t: 1, a: 'addColumn', c: [t, c, o] });
187
209
  }
188
210
 
189
- delete this.schema[t][c];
211
+ if (hasColumns) {
212
+ delete this.schema[t].columns[c];
213
+ } else {
214
+ delete this.schema[t][c];
215
+ }
190
216
 
191
217
  return Promise.resolve(alter);
192
218
  }
@@ -207,7 +233,10 @@ class STD {
207
233
  throw new Error(`There is no ${t} table in schema!`);
208
234
  }
209
235
  this.schema[t] = this.schema[t] || {};
210
- this.schema[t][c] = s;
236
+ const hasColumns = this.schema[t].columns !== undefined;
237
+ const columns = hasColumns ? this.schema[t].columns : this.schema[t];
238
+
239
+ columns[c] = s;
211
240
 
212
241
  this.modC.push({ t: 0, a: 'removeColumn', c: [t, c] });
213
242
 
@@ -219,8 +248,11 @@ class STD {
219
248
 
220
249
  if (!this.modS[t]) this.modS[t] = {};
221
250
 
222
- this.modS[t][c] = this.schema[t][c];
223
- this.schema[t][c] = Object.assign(this.schema[t][c], s);
251
+ const hasColumns = this.schema[t].columns !== undefined;
252
+ const columns = hasColumns ? this.schema[t].columns : this.schema[t];
253
+
254
+ this.modS[t][c] = columns[c];
255
+ columns[c] = Object.assign(columns[c], s);
224
256
 
225
257
  this.modC.push({ t: 1, a: 'changeColumn', c: [t, c] });
226
258
 
@@ -229,7 +261,7 @@ class STD {
229
261
 
230
262
  addIndex (t, i, c, u) {
231
263
  if (Array.isArray(c)) {
232
- c.forEach(x => this.checkColumn(t, x));
264
+ c.forEach(x => this.checkColumn(t, typeof x === 'object' ? x.name : x));
233
265
  } else {
234
266
  this.checkColumn(t, c);
235
267
  }
@@ -269,7 +301,7 @@ class STD {
269
301
 
270
302
  alter.i[t] = {};
271
303
  alter.i[t][i] = this.indizies[t][i];
272
- this.modI[t] = {};
304
+ if (!this.modI[t]) this.modI[t] = {};
273
305
  this.modI[t][i] = this.indizies[t][i];
274
306
  delete this.indizies[t][i];
275
307
 
@@ -314,10 +346,12 @@ class STD {
314
346
 
315
347
  alter.f[t] = {};
316
348
  alter.f[t][k] = this.foreign[t][k];
317
- this.modF[t] = {};
349
+ if (!this.modF[t]) this.modF[t] = {};
318
350
  this.modF[t][k] = this.foreign[t][k];
319
351
  delete this.foreign[t][k];
320
352
 
353
+ this.modC.push({ t: 1, a: 'addForeignKey', c: [t, k] });
354
+
321
355
  return Promise.resolve(alter);
322
356
  }
323
357
 
@@ -340,6 +374,9 @@ const noLearnError = prop => {
340
374
  };
341
375
 
342
376
  module.exports = {
377
+ // learns the schema and records the reverse operations
378
+ learns: true,
379
+
343
380
  getInterface: (context, file, driver, internals) => {
344
381
  if (context.learnable) {
345
382
  const _std = new STD(internals, context);
@@ -0,0 +1,18 @@
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
+ // in memory first, a failing write must not hide the executed step
12
+ internals.migrationDone = internals.migrationOp;
13
+ return State.done(pdriver, internals.migrationOp, internals);
14
+ };
15
+
16
+ return Shadow.overshadow(driver, {}, done);
17
+ }
18
+ };
@@ -17,41 +17,45 @@ 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
- constructor (internals, file, driver) {
26
+ constructor (internals, file, driver, pdriver, step = true) {
22
27
  this.file = file;
23
28
  this.internals = internals;
24
29
  this.driver = driver;
25
- this._counter = 0;
26
- }
30
+ this.pdriver = pdriver;
31
+ this._default = async function _default () {
32
+ const op = this.internals.migrationOp;
27
33
 
28
- async step () {
29
- return State.step(this.driver, ++this._counter, this.internals);
30
- }
34
+ if (step) {
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
+ });
40
+ }
31
41
 
32
- async update () {
33
- return State.update(
34
- this.driver,
35
- this.file,
36
- this.internals.modSchema,
37
- this.internals
38
- );
39
- }
40
-
41
- async default () {
42
- await this.step();
43
- const res = await this.update();
42
+ await State.update(
43
+ this.pdriver,
44
+ this.file,
45
+ this.internals.modSchema,
46
+ this.internals
47
+ );
44
48
 
45
- return res;
49
+ if (!step) {
50
+ await State.learned(this.pdriver, op, this.internals);
51
+ }
52
+ };
46
53
  }
47
54
  }
48
55
 
49
56
  Object.keys(DEFAULT).forEach(m => {
50
57
  StateTravel.prototype[m] = async function (...args) {
51
- await this.step();
52
- const res = await this.update();
53
-
54
- return res;
58
+ return this._default();
55
59
  };
56
60
  });
57
61
 
@@ -61,24 +65,25 @@ const noTravelError = prop => {
61
65
  };
62
66
  };
63
67
 
64
- module.exports = {
65
- getInterface: (context, file, driver, internals) => {
66
- if (context.learnable) {
67
- const st = new StateTravel(internals, file, context);
68
+ module.exports = function (step = true) {
69
+ return {
70
+ getInterface: (context, file, driver, internals, pdriver) => {
71
+ if (context.learnable) {
72
+ const st = new StateTravel(internals, file, context, pdriver, step);
73
+ return Shadow.overshadow(
74
+ driver,
75
+ Object.assign(st, context.statechanger),
76
+ noTravelError
77
+ );
78
+ }
79
+
68
80
  return Shadow.overshadow(
69
81
  driver,
70
- Object.assign(st, context.statechanger),
82
+ new StateTravel(internals, file, context, pdriver, step),
71
83
  noTravelError
72
84
  );
73
85
  }
74
86
 
75
- return Shadow.overshadow(
76
- driver,
77
- new StateTravel(internals, file, context),
78
- noTravelError
79
- );
80
- }
81
-
82
87
  /* hasModification: true,
83
88
 
84
89
  modify: (context, driver, internals) => {
@@ -96,4 +101,5 @@ module.exports = {
96
101
 
97
102
  return { driver: _driver };
98
103
  } */
104
+ };
99
105
  };
@@ -2,6 +2,8 @@ const Promise = require('bluebird');
2
2
  const State = require('../../state');
3
3
  const Chain = require('../../chain');
4
4
  const Migrate = require('./migrate');
5
+ const Learn = require('../../learn');
6
+ const log = require('db-migrate-shared').log;
5
7
 
6
8
  const methods = {
7
9
  createTable: async (driver, [t], internals) => {
@@ -42,6 +44,31 @@ const methods = {
42
44
 
43
45
  changeColumn: async (driver, [t, c], internals) => {
44
46
  return driver.changeColumn(t, c, internals.modSchema.c[t][c]);
47
+ },
48
+
49
+ addForeignKey: async (driver, [t, k], internals) => {
50
+ const foreign = internals.modSchema.f[t][k];
51
+ return driver.addForeignKey(t, foreign.rt, k, foreign.m, foreign.r);
52
+ },
53
+
54
+ addIndex: async (driver, [t, i], internals) => {
55
+ if (!internals.modSchema.i[t][i]) {
56
+ log.warn(
57
+ `There was an attempt to create the index "${i}" in the table "${t}".`
58
+ );
59
+ log.warn(
60
+ `This index did not exist at the time of deleting it. ` +
61
+ `This can happen if an index was created by hand and then deleted ` +
62
+ `by db-migrate. Make sure to recreate it by hand to avoid any problems.`
63
+ );
64
+ return Promise.resolve();
65
+ }
66
+ return driver.addIndex(
67
+ t,
68
+ i,
69
+ internals.modSchema.i[t][i].c,
70
+ internals.modSchema.i[t][i].u ? true : undefined
71
+ );
45
72
  }
46
73
  };
47
74
 
@@ -50,6 +77,7 @@ async function processEntry (
50
77
  file,
51
78
  driver,
52
79
  internals,
80
+ pdriver,
53
81
  { t: type, a: action, c: args }
54
82
  ) {
55
83
  let skip = false;
@@ -57,7 +85,7 @@ async function processEntry (
57
85
  if (
58
86
  internals.rollback === true &&
59
87
  internals.rollbackContinue !== true &&
60
- driver.udriver._meta.signalColumns.indexOf(action) !== -1
88
+ driver.udriver._meta?.signalColumns?.indexOf(action) !== -1
61
89
  ) {
62
90
  internals.rollbackContinue = true;
63
91
  skip = driver.udriver._counter.previousSignal() === null;
@@ -78,19 +106,23 @@ async function processEntry (
78
106
  }
79
107
  }
80
108
 
81
- internals.modSchema.s.shift();
82
- await State.update(context, file, internals.modSchema, internals);
109
+ internals.modSchema.s.pop();
110
+ await State.update(pdriver, file, internals.modSchema, internals);
83
111
  }
84
112
 
85
- module.exports = async (context, file, driver, internals) => {
113
+ module.exports = async (context, file, driver, internals, pdriver) => {
86
114
  const mod = internals.modSchema;
87
115
 
88
- const chain = new Chain(context, file, driver, internals);
89
- // chain.addChain(Learn);
116
+ const chain = new Chain(context, file, driver, internals, pdriver);
117
+ internals.unlearn = true;
118
+
119
+ chain.addChain(Learn);
90
120
  chain.addChain(Migrate);
91
- await Promise.resolve(mod.s.reverse()).each(args =>
92
- processEntry(context, file, chain, internals, args)
121
+ // the stored records keep their order, so an interrupted rollback can
122
+ // be continued
123
+ await Promise.resolve(mod.s.slice().reverse()).each(args =>
124
+ processEntry(context, file, chain, internals, pdriver, args)
93
125
  );
94
126
 
95
- await State.deleteState(context, file, internals);
127
+ await State.deleteState(pdriver, file, internals);
96
128
  };