Add status method

Returns 0 if all migrations are run and DB is up to date. Return negative number if DB is missing migrations. Return positive number if DB is ahead (rare case, e.g. if migrations were run then code is reverted)
This commit is contained in:
Dustin Martin 2015-11-18 16:31:54 -06:00
parent ebd361ffce
commit d9ac2c1f8f
8 changed files with 368 additions and 292 deletions

View File

@ -10,5 +10,5 @@
"undef": true,
"trailing": true,
"unused": true,
"predef": [ "-Promise", "before", "after" ]
"predef": [ "-Promise", "before", "after", "beforeEach", "afterEach" ]
}

View File

@ -1,12 +1,6 @@
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
exports.__esModule = true;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
@ -20,7 +14,7 @@ var Connection = (function (_EventEmitter) {
function Connection(connection) {
_classCallCheck(this, Connection);
_get(Object.getPrototypeOf(Connection.prototype), 'constructor', this).call(this);
_EventEmitter.call(this);
this.connection = connection;
// Flag indicating whether the connection is "managed",
@ -28,12 +22,9 @@ var Connection = (function (_EventEmitter) {
this.managed = false;
}
_createClass(Connection, [{
key: 'execute',
value: function execute() {
return this._execute();
}
}]);
Connection.prototype.execute = function execute() {
return this._execute();
};
return Connection;
})(_events.EventEmitter);

View File

@ -7,6 +7,7 @@
var inherits = require('inherits');
var ColumnCompiler = require('../../../schema/columncompiler');
var assign = require('lodash/object/assign');
var helpers = require('../../../helpers');
function ColumnCompiler_PG() {
ColumnCompiler.apply(this, arguments);
@ -38,8 +39,11 @@ assign(ColumnCompiler_PG.prototype, {
floating: 'real',
increments: 'serial primary key',
json: function json(jsonb) {
if (!this.client.version || parseFloat(this.client.version) >= 9.2) return jsonb ? 'jsonb' : 'json';
return 'text';
if (jsonb) helpers.deprecate('json(true)', 'jsonb()');
return jsonColumn(this.client, jsonb);
},
jsonb: function jsonb() {
return jsonColumn(this.client, true);
},
smallint: 'smallint',
tinyint: 'smallint',
@ -61,4 +65,9 @@ assign(ColumnCompiler_PG.prototype, {
});
function jsonColumn(client, jsonb) {
if (!client.version || parseFloat(client.version) >= 9.2) return jsonb ? 'jsonb' : 'json';
return 'text';
}
module.exports = ColumnCompiler_PG;

View File

@ -2,11 +2,7 @@
// -------
"use strict";
Object.defineProperty(exports, '__esModule', {
value: true
});
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
exports.__esModule = true;
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
@ -34,285 +30,270 @@ var Migrator = (function () {
// Migrators to the latest configuration.
_createClass(Migrator, [{
key: 'latest',
value: function latest(config) {
var _this = this;
Migrator.prototype.latest = function latest(config) {
var _this = this;
this.config = this.setConfig(config);
return this._migrationData().tap(validateMigrationList).spread(function (all, completed) {
return _this._runBatch(_.difference(all, completed), 'up');
});
}
this.config = this.setConfig(config);
return this._migrationData().tap(validateMigrationList).spread(function (all, completed) {
return _this._runBatch(_.difference(all, completed), 'up');
});
};
// Rollback the last "batch" of migrations that were run.
}, {
key: 'rollback',
value: function rollback(config) {
var _this2 = this;
// Rollback the last "batch" of migrations that were run.
return Promise['try'](function () {
_this2.config = _this2.setConfig(config);
return _this2._migrationData().tap(validateMigrationList).then(function (val) {
return _this2._getLastBatch(val);
}).then(function (migrations) {
return _this2._runBatch(_.pluck(migrations, 'name'), 'down');
});
});
}
Migrator.prototype.rollback = function rollback(config) {
var _this2 = this;
// Retrieves and returns the current migration version
// we're on, as a promise. If there aren't any migrations run yet,
// return "none" as the value for the `currentVersion`.
}, {
key: 'currentVersion',
value: function currentVersion(config) {
this.config = this.setConfig(config);
return this._listCompleted(config).then(function (completed) {
var val = _.chain(completed).map(function (value) {
return value.split('_')[0];
}).max().value();
return val === -Infinity ? 'none' : val;
});
}
// Creates a new migration, with a given name.
}, {
key: 'make',
value: function make(name, config) {
var _this3 = this;
this.config = this.setConfig(config);
if (!name) Promise.rejected(new Error('A name must be specified for the generated migration'));
return this._ensureFolder(config).then(function (val) {
return _this3._generateStubTemplate(val);
}).then(function (val) {
return _this3._writeNewMigration(name, val);
});
}
// Lists all available migration versions, as a sorted array.
}, {
key: '_listAll',
value: function _listAll(config) {
this.config = this.setConfig(config);
return Promise.promisify(fs.readdir, fs)(this._absoluteConfigDir()).then(function (migrations) {
return _.filter(migrations, function (value) {
var extension = path.extname(value);
return _.contains(['.co', '.coffee', '.eg', '.iced', '.js', '.litcoffee', '.ls'], extension);
}).sort();
});
}
// Ensures a folder for the migrations exist, dependent on the
// migration config settings.
}, {
key: '_ensureFolder',
value: function _ensureFolder() {
var dir = this._absoluteConfigDir();
return Promise.promisify(fs.stat, fs)(dir)['catch'](function () {
return Promise.promisify(mkdirp)(dir);
});
}
// Ensures that the proper table has been created,
// dependent on the migration config settings.
}, {
key: '_ensureTable',
value: function _ensureTable() {
var _this4 = this;
var table = this.config.tableName;
return this.knex.schema.hasTable(table).then(function (exists) {
if (!exists) return _this4._createMigrationTable(table);
});
}
// Create the migration table, if it doesn't already exist.
}, {
key: '_createMigrationTable',
value: function _createMigrationTable(tableName) {
return this.knex.schema.createTable(tableName, function (t) {
t.increments();
t.string('name');
t.integer('batch');
t.timestamp('migration_time');
});
}
// Run a batch of current migrations, in sequence.
}, {
key: '_runBatch',
value: function _runBatch(migrations, direction) {
var _this5 = this;
return Promise.all(_.map(migrations, this._validateMigrationStructure, this)).then(function () {
return _this5._latestBatchNumber();
}).then(function (batchNo) {
if (direction === 'up') batchNo++;
return batchNo;
}).then(function (batchNo) {
return _this5._waterfallBatch(batchNo, migrations, direction);
})['catch'](function (error) {
helpers.warn('migrations failed with error: ' + error.message);
throw error;
});
}
// Validates some migrations by requiring and checking for an `up` and `down` function.
}, {
key: '_validateMigrationStructure',
value: function _validateMigrationStructure(name) {
var migration = require(path.join(this._absoluteConfigDir(), name));
if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
throw new Error('Invalid migration: ' + name + ' must have both an up and down function');
}
return name;
}
// Lists all migrations that have been completed for the current db, as an array.
}, {
key: '_listCompleted',
value: function _listCompleted() {
var _this6 = this;
var tableName = this.config.tableName;
return this._ensureTable(tableName).then(function () {
return _this6.knex(tableName).orderBy('id').select('name');
return Promise['try'](function () {
_this2.config = _this2.setConfig(config);
return _this2._migrationData().tap(validateMigrationList).then(function (val) {
return _this2._getLastBatch(val);
}).then(function (migrations) {
return _.pluck(migrations, 'name');
return _this2._runBatch(_.pluck(migrations, 'name'), 'down');
});
}
});
};
// Gets the migration list from the specified migration directory,
// as well as the list of completed migrations to check what
// should be run.
}, {
key: '_migrationData',
value: function _migrationData() {
return Promise.all([this._listAll(), this._listCompleted()]);
}
Migrator.prototype.status = function status(config) {
this.config = this.setConfig(config);
// Generates the stub template for the current migration, returning a compiled template.
}, {
key: '_generateStubTemplate',
value: function _generateStubTemplate() {
var stubPath = this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub');
return Promise.promisify(fs.readFile, fs)(stubPath).then(function (stub) {
return _.template(stub.toString(), null, { variable: 'd' });
return Promise.all([this.knex(this.config.tableName).select('*'), this._listAll()]).spread(function (db, code) {
return db.length - code.length;
});
};
// Retrieves and returns the current migration version
// we're on, as a promise. If there aren't any migrations run yet,
// return "none" as the value for the `currentVersion`.
Migrator.prototype.currentVersion = function currentVersion(config) {
this.config = this.setConfig(config);
return this._listCompleted(config).then(function (completed) {
var val = _.chain(completed).map(function (value) {
return value.split('_')[0];
}).max().value();
return val === -Infinity ? 'none' : val;
});
};
// Creates a new migration, with a given name.
Migrator.prototype.make = function make(name, config) {
var _this3 = this;
this.config = this.setConfig(config);
if (!name) Promise.rejected(new Error('A name must be specified for the generated migration'));
return this._ensureFolder(config).then(function (val) {
return _this3._generateStubTemplate(val);
}).then(function (val) {
return _this3._writeNewMigration(name, val);
});
};
// Lists all available migration versions, as a sorted array.
Migrator.prototype._listAll = function _listAll(config) {
this.config = this.setConfig(config);
return Promise.promisify(fs.readdir, fs)(this._absoluteConfigDir()).then(function (migrations) {
return _.filter(migrations, function (value) {
var extension = path.extname(value);
return _.contains(['.co', '.coffee', '.eg', '.iced', '.js', '.litcoffee', '.ls'], extension);
}).sort();
});
};
// Ensures a folder for the migrations exist, dependent on the
// migration config settings.
Migrator.prototype._ensureFolder = function _ensureFolder() {
var dir = this._absoluteConfigDir();
return Promise.promisify(fs.stat, fs)(dir)['catch'](function () {
return Promise.promisify(mkdirp)(dir);
});
};
// Ensures that the proper table has been created,
// dependent on the migration config settings.
Migrator.prototype._ensureTable = function _ensureTable() {
var _this4 = this;
var table = this.config.tableName;
return this.knex.schema.hasTable(table).then(function (exists) {
if (!exists) return _this4._createMigrationTable(table);
});
};
// Create the migration table, if it doesn't already exist.
Migrator.prototype._createMigrationTable = function _createMigrationTable(tableName) {
return this.knex.schema.createTable(tableName, function (t) {
t.increments();
t.string('name');
t.integer('batch');
t.timestamp('migration_time');
});
};
// Run a batch of current migrations, in sequence.
Migrator.prototype._runBatch = function _runBatch(migrations, direction) {
var _this5 = this;
return Promise.all(_.map(migrations, this._validateMigrationStructure, this)).then(function () {
return _this5._latestBatchNumber();
}).then(function (batchNo) {
if (direction === 'up') batchNo++;
return batchNo;
}).then(function (batchNo) {
return _this5._waterfallBatch(batchNo, migrations, direction);
})['catch'](function (error) {
helpers.warn('migrations failed with error: ' + error.message);
throw error;
});
};
// Validates some migrations by requiring and checking for an `up` and `down` function.
Migrator.prototype._validateMigrationStructure = function _validateMigrationStructure(name) {
var migration = require(path.join(this._absoluteConfigDir(), name));
if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
throw new Error('Invalid migration: ' + name + ' must have both an up and down function');
}
return name;
};
// Lists all migrations that have been completed for the current db, as an array.
Migrator.prototype._listCompleted = function _listCompleted() {
var _this6 = this;
var tableName = this.config.tableName;
return this._ensureTable(tableName).then(function () {
return _this6.knex(tableName).orderBy('id').select('name');
}).then(function (migrations) {
return _.pluck(migrations, 'name');
});
};
// Gets the migration list from the specified migration directory,
// as well as the list of completed migrations to check what
// should be run.
Migrator.prototype._migrationData = function _migrationData() {
return Promise.all([this._listAll(), this._listCompleted()]);
};
// Generates the stub template for the current migration, returning a compiled template.
Migrator.prototype._generateStubTemplate = function _generateStubTemplate() {
var stubPath = this.config.stub || path.join(__dirname, 'stub', this.config.extension + '.stub');
return Promise.promisify(fs.readFile, fs)(stubPath).then(function (stub) {
return _.template(stub.toString(), null, { variable: 'd' });
});
};
// Write a new migration to disk, using the config and generated filename,
// passing any `variables` given in the config to the template.
Migrator.prototype._writeNewMigration = function _writeNewMigration(name, tmpl) {
var config = this.config;
var dir = this._absoluteConfigDir();
if (name[0] === '-') name = name.slice(1);
var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
return Promise.promisify(fs.writeFile, fs)(path.join(dir, filename), tmpl(config.variables || {}))['return'](path.join(dir, filename));
};
// Get the last batch of migrations, by name, ordered by insert id
// in reverse order.
Migrator.prototype._getLastBatch = function _getLastBatch() {
var tableName = this.config.tableName;
return this.knex(tableName).where('batch', function (qb) {
qb.max('batch').from(tableName);
}).orderBy('id', 'desc');
};
// Returns the latest batch number.
Migrator.prototype._latestBatchNumber = function _latestBatchNumber() {
return this.knex(this.config.tableName).max('batch as max_batch').then(function (obj) {
return obj[0].max_batch || 0;
});
};
// If transaction conf for a single migration is defined, use that.
// Otherwise, rely on the common config. This allows enabling/disabling
// transaction for a single migration by will, regardless of the common
// config.
Migrator.prototype._useTransaction = function _useTransaction(migration, allTransactionsDisabled) {
var singleTransactionValue = _.get(migration, 'config.transaction');
return _.isBoolean(singleTransactionValue) ? singleTransactionValue : !allTransactionsDisabled;
};
// Runs a batch of `migrations` in a specified `direction`,
// saving the appropriate database information as the migrations are run.
Migrator.prototype._waterfallBatch = function _waterfallBatch(batchNo, migrations, direction) {
var _this7 = this;
var knex = this.knex;
var _config = this.config;
var tableName = _config.tableName;
var disableTransactions = _config.disableTransactions;
var directory = this._absoluteConfigDir();
var current = Promise.bind({ failed: false, failedOn: 0 });
var log = [];
_.each(migrations, function (migration) {
var name = migration;
migration = require(directory + '/' + name);
// We're going to run each of the migrations in the current "up"
current = current.then(function () {
if (_this7._useTransaction(migration, disableTransactions)) {
return _this7._transaction(migration, direction, name);
}
return warnPromise(migration[direction](knex, Promise), name);
}).then(function () {
log.push(path.join(directory, name));
if (direction === 'up') {
return knex(tableName).insert({
name: name,
batch: batchNo,
migration_time: new Date()
});
}
if (direction === 'down') {
return knex(tableName).where({ name: name }).del();
}
});
}
});
// Write a new migration to disk, using the config and generated filename,
// passing any `variables` given in the config to the template.
}, {
key: '_writeNewMigration',
value: function _writeNewMigration(name, tmpl) {
var config = this.config;
var dir = this._absoluteConfigDir();
if (name[0] === '-') name = name.slice(1);
var filename = yyyymmddhhmmss() + '_' + name + '.' + config.extension;
return Promise.promisify(fs.writeFile, fs)(path.join(dir, filename), tmpl(config.variables || {}))['return'](path.join(dir, filename));
}
return current.thenReturn([batchNo, log]);
};
// Get the last batch of migrations, by name, ordered by insert id
// in reverse order.
}, {
key: '_getLastBatch',
value: function _getLastBatch() {
var tableName = this.config.tableName;
return this.knex(tableName).where('batch', function (qb) {
qb.max('batch').from(tableName);
}).orderBy('id', 'desc');
}
// Returns the latest batch number.
}, {
key: '_latestBatchNumber',
value: function _latestBatchNumber() {
return this.knex(this.config.tableName).max('batch as max_batch').then(function (obj) {
return obj[0].max_batch || 0;
Migrator.prototype._transaction = function _transaction(migration, direction, name) {
return this.knex.transaction(function (trx) {
return warnPromise(migration[direction](trx, Promise), name, function () {
trx.commit();
});
}
});
};
// If transaction conf for a single migration is defined, use that.
// Otherwise, rely on the common config. This allows enabling/disabling
// transaction for a single migration by will, regardless of the common
// config.
}, {
key: '_useTransaction',
value: function _useTransaction(migration, allTransactionsDisabled) {
var singleTransactionValue = _.get(migration, 'config.transaction');
Migrator.prototype._absoluteConfigDir = function _absoluteConfigDir() {
return path.resolve(process.cwd(), this.config.directory);
};
return _.isBoolean(singleTransactionValue) ? singleTransactionValue : !allTransactionsDisabled;
}
// Runs a batch of `migrations` in a specified `direction`,
// saving the appropriate database information as the migrations are run.
}, {
key: '_waterfallBatch',
value: function _waterfallBatch(batchNo, migrations, direction) {
var _this7 = this;
var knex = this.knex;
var _config = this.config;
var tableName = _config.tableName;
var disableTransactions = _config.disableTransactions;
var directory = this._absoluteConfigDir();
var current = Promise.bind({ failed: false, failedOn: 0 });
var log = [];
_.each(migrations, function (migration) {
var name = migration;
migration = require(directory + '/' + name);
// We're going to run each of the migrations in the current "up"
current = current.then(function () {
if (_this7._useTransaction(migration, disableTransactions)) {
return _this7._transaction(migration, direction, name);
}
return warnPromise(migration[direction](knex, Promise), name);
}).then(function () {
log.push(path.join(directory, name));
if (direction === 'up') {
return knex(tableName).insert({
name: name,
batch: batchNo,
migration_time: new Date()
});
}
if (direction === 'down') {
return knex(tableName).where({ name: name }).del();
}
});
});
return current.thenReturn([batchNo, log]);
}
}, {
key: '_transaction',
value: function _transaction(migration, direction, name) {
return this.knex.transaction(function (trx) {
return warnPromise(migration[direction](trx, Promise), name, function () {
trx.commit();
});
});
}
}, {
key: '_absoluteConfigDir',
value: function _absoluteConfigDir() {
return path.resolve(process.cwd(), this.config.directory);
}
}, {
key: 'setConfig',
value: function setConfig(config) {
return assign({
extension: 'js',
tableName: 'knex_migrations',
directory: './migrations'
}, this.config || {}, config);
}
}]);
Migrator.prototype.setConfig = function setConfig(config) {
return assign({
extension: 'js',
tableName: 'knex_migrations',
directory: './migrations'
}, this.config || {}, config);
};
return Migrator;
})();

View File

@ -83,7 +83,7 @@ var columnTypes = [
'char', 'varchar', 'tinytext', 'tinyText', 'text', 'mediumtext', 'mediumText', 'longtext', 'longText', 'binary', 'varbinary', 'tinyblob', 'tinyBlob', 'mediumblob', 'mediumBlob', 'blob', 'longblob', 'longBlob', 'enum', 'set',
// Increments, Aliases, and Additional
'bool', 'dateTime', 'increments', 'bigincrements', 'bigIncrements', 'integer', 'biginteger', 'bigInteger', 'string', 'timestamps', 'json', 'uuid', 'enu', 'specificType'];
'bool', 'dateTime', 'increments', 'bigincrements', 'bigIncrements', 'integer', 'biginteger', 'bigInteger', 'string', 'timestamps', 'json', 'jsonb', 'uuid', 'enu', 'specificType'];
// For each of the column methods, create a new "ColumnBuilder" interface,
// push it onto the "allStatements" stack, and then return the interface,

View File

@ -1,8 +1,6 @@
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
exports.__esModule = true;
exports['default'] = parseConnectionString;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
@ -32,7 +30,7 @@ function parseConnectionString(str) {
}
return {
client: protocol,
connection: protocol === 'postgres' ? (0, _pgConnectionString.parse)(str) : connectionObject(parsed)
connection: protocol === 'postgres' ? _pgConnectionString.parse(str) : connectionObject(parsed)
};
}

View File

@ -43,6 +43,19 @@ export default class Migrator {
})
}
status(config) {
this.config = this.setConfig(config);
return Promise.all([
this.knex(this.config.tableName).select('*'),
this._listAll()
])
.spread(function(db, code) {
return db.length - code.length;
});
}
// Retrieves and returns the current migration version
// we're on, as a promise. If there aren't any migrations run yet,
// return "none" as the value for the `currentVersion`.

View File

@ -28,6 +28,90 @@ module.exports = function(knex) {
var tables = ['migration_test_1', 'migration_test_2', 'migration_test_2_1'];
describe('knex.migrate.status', function() {
beforeEach(function() {
return knex.migrate.latest({directory: 'test/integration/migrate/test'}).catch(function() {});
});
afterEach(function() {
return knex.migrate.rollback({directory: 'test/integration/migrate/test'});
});
it('should return 0 if code matches DB', function() {
// Put in a couple dummy migrations. Needed
// since the migrations directory has a couple
// bad migrations that don't get loaded. This
// will simulate the DB and source being in sync.
return Promise.all([
knex('knex_migrations').returning('id').insert({
name: 'foobar',
batch: 5,
migration_time: new Date()
}),
knex('knex_migrations').returning('id').insert({
name: 'foobarbaz',
batch: 6,
migration_time: new Date()
})
])
.spread(function(migration1, migration2) {
return knex.migrate.status({directory: 'test/integration/migrate/test'}).then(function(migrationLevel) {
expect(migrationLevel).to.equal(0);
})
.then(function() {
// Cleanup the added migrations
return knex('knex_migrations')
.where('id', migration1[0])
.orWhere('id', migration2[0])
.del()
});
});
});
it('should return a negative number if the DB is behind', function() {
return knex.migrate.status({directory: 'test/integration/migrate/test'}).then(function(migrationLevel) {
expect(migrationLevel).to.equal(-2);
});
});
it('should return a positive number if the DB is ahead', function() {
return Promise.all([
knex('knex_migrations').returning('id').insert({
name: 'foobar',
batch: 5,
migration_time: new Date()
}),
knex('knex_migrations').returning('id').insert({
name: 'foobar',
batch: 5,
migration_time: new Date()
}),
knex('knex_migrations').returning('id').insert({
name: 'foobarbaz',
batch: 6,
migration_time: new Date()
})
])
.spread(function(migration1, migration2, migration3) {
return knex.migrate.status({directory: 'test/integration/migrate/test'}).then(function(migrationLevel) {
expect(migrationLevel).to.equal(1);
})
.then(function() {
// Cleanup the added migrations
return knex('knex_migrations')
.where('id', migration1[0])
.orWhere('id', migration2[0])
.orWhere('id', migration3[0])
.del()
});
});
});
});
describe('knex.migrate.latest', function() {
before(function() {