Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support DB schema for migrations - fixes #2499 #2559

Merged
merged 12 commits into from
Apr 4, 2018
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
"import/parser": "babel-eslint"
},
"env": {
"node": true
"node": true,
"mocha": true
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linting is complaining about afterEach usage otherwise.

}
};
82 changes: 55 additions & 27 deletions src/migrate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const CONFIG_DEFAULT = Object.freeze({
'.co', '.coffee', '.eg', '.iced', '.js', '.litcoffee', '.ls', '.ts'
],
tableName: 'knex_migrations',
schemaName: null,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've chosen this name to be consistent with currently used "tableName"

directory: './migrations',
disableTransactions: false
});
Expand All @@ -33,9 +34,9 @@ const CONFIG_DEFAULT = Object.freeze({
export default class Migrator {

constructor(knex) {
this.knex = knex
this.knex = knex;
this.config = this.setConfig(knex.client.config.migrations);

this._activeMigration = {
fileName: null
}
Expand Down Expand Up @@ -81,7 +82,8 @@ export default class Migrator {
this.config = this.setConfig(config);

return Promise.all([
this.knex(this.config.tableName).select('*'),
getTable(this.knex, this.config.tableName, this.config.schemaName)
.select('*'),
this._listAll()
])
.spread((db, code) => db.length - code.length);
Expand All @@ -102,7 +104,7 @@ export default class Migrator {
forceFreeMigrationsLock(config) {
this.config = this.setConfig(config);
const lockTable = this._getLockTableName();
return this.knex.schema.hasTable(lockTable)
return getSchemaBuilder(this.knex, this.config.schemaName).hasTable(lockTable)
.then(exist => exist && this._freeLock());
}

Expand Down Expand Up @@ -142,18 +144,19 @@ export default class Migrator {
// Ensures that a proper table has been created, dependent on the migration
// config settings.
_ensureTable(trx = this.knex) {
const table = this.config.tableName;
const { tableName, schemaName } = this.config;
const lockTable = this._getLockTableName();
return trx.schema.hasTable(table)
.then(exists => !exists && this._createMigrationTable(table, trx))
.then(() => trx.schema.hasTable(lockTable))
const lockTableWithSchema = this._getLockTableNameWithSchema();
return getSchemaBuilder(trx, schemaName).hasTable(tableName)
.then(exists => !exists && this._createMigrationTable(tableName, schemaName, trx))
.then(() => getSchemaBuilder(trx, schemaName).hasTable(lockTable))
.then(exists => !exists && this._createMigrationLockTable(lockTable, trx))
.then(() => trx.from(lockTable).select('*'))
.then(data => !data.length && trx.into(lockTable).insert({ is_locked: 0 }));
.then(() => getTable(trx, lockTable, this.config.schemaName).select('*'))
.then(data => !data.length && trx.into(lockTableWithSchema).insert({ is_locked: 0 }));
}

_createMigrationTable(tableName, trx = this.knex) {
return trx.schema.createTable(tableName, function(t) {
_createMigrationTable(tableName, schemaName, trx = this.knex) {
return getSchemaBuilder(trx, schemaName).createTable(getTableName(tableName), function(t) {
t.increments();
t.string('name');
t.integer('batch');
Expand All @@ -162,7 +165,7 @@ export default class Migrator {
}

_createMigrationLockTable(tableName, trx = this.knex) {
return trx.schema.createTable(tableName, function(t) {
return getSchemaBuilder(trx, this.config.schemaName).createTable(tableName, function(t) {
t.integer('is_locked');
});
}
Expand All @@ -171,9 +174,15 @@ export default class Migrator {
return this.config.tableName + '_lock';
}

_getLockTableNameWithSchema() {
return this.config.schemaName ?
this.config.schemaName + '.' + this._getLockTableName()
: this._getLockTableName();
}

_isLocked(trx) {
const tableName = this._getLockTableName();
return this.knex(tableName)
return getTable(this.knex, tableName, this.config.schemaName)
.transacting(trx)
.forUpdate()
.select('*')
Expand All @@ -182,7 +191,7 @@ export default class Migrator {

_lockMigrations(trx) {
const tableName = this._getLockTableName();
return this.knex(tableName)
return getTable(this.knex, tableName, this.config.schemaName)
.transacting(trx)
.update({ is_locked: 1 });
}
Expand All @@ -205,7 +214,7 @@ export default class Migrator {

_freeLock(trx = this.knex) {
const tableName = this._getLockTableName();
return trx.table(tableName)
return getTable(trx, tableName, this.config.schemaName)
.update({ is_locked: 0 });
}

Expand Down Expand Up @@ -235,7 +244,7 @@ export default class Migrator {
helpers.warn(
'If you are sure migrations are not running you can release the ' +
'lock manually by deleting all the rows from migrations lock ' +
'table: ' + this._getLockTableName()
'table: ' + this._getLockTableNameWithSchema()
);
} else {
if (this._activeMigration.fileName) {
Expand Down Expand Up @@ -265,9 +274,9 @@ export default class Migrator {
// Lists all migrations that have been completed for the current db, as an
// array.
_listCompleted(trx = this.knex) {
const { tableName } = this.config
const { tableName, schemaName } = this.config;
return this._ensureTable(trx)
.then(() => trx.from(tableName).orderBy('id').select('name'))
.then(() => trx.from(getTableName(tableName, schemaName)).orderBy('id').select('name'))
.then((migrations) => map(migrations, 'name'))
}

Expand Down Expand Up @@ -306,17 +315,17 @@ export default class Migrator {
// Get the last batch of migrations, by name, ordered by insert id in reverse
// order.
_getLastBatch() {
const { tableName } = this.config;
return this.knex(tableName)
const { tableName, schemaName } = this.config;
return getTable(this.knex, tableName, schemaName)
.where('batch', function(qb) {
qb.max('batch').from(tableName)
qb.max('batch').from(getTableName(tableName, schemaName))
})
.orderBy('id', 'desc');
}

// Returns the latest batch number.
_latestBatchNumber(trx = this.knex) {
return trx.from(this.config.tableName)
return trx.from(getTableName(this.config.tableName, this.config.schemaName))
.max('batch as max_batch').then(obj => obj[0].max_batch || 0);
}

Expand All @@ -336,7 +345,7 @@ export default class Migrator {
// appropriate database information as the migrations are run.
_waterfallBatch(batchNo, migrations, direction, trx) {
const trxOrKnex = trx || this.knex;
const {tableName, disableTransactions} = this.config;
const {tableName, schemaName, disableTransactions} = this.config;
const directory = this._absoluteConfigDir();
let current = Promise.bind({failed: false, failedOn: 0});
const log = [];
Expand All @@ -355,17 +364,17 @@ export default class Migrator {
.then(() => {
log.push(path.join(directory, name));
if (direction === 'up') {
return trxOrKnex.into(tableName).insert({
return trxOrKnex.into(getTableName(tableName, schemaName)).insert({
name,
batch: batchNo,
migration_time: new Date()
});
}
if (direction === 'down') {
return trxOrKnex.from(tableName).where({name}).del();
return trxOrKnex.from(getTableName(tableName, schemaName)).where({name}).del();
}
});
})
});

return current.thenReturn([batchNo, log]);
}
Expand Down Expand Up @@ -425,3 +434,22 @@ function yyyymmddhhmmss() {
padDate(d.getMinutes()) +
padDate(d.getSeconds());
}

//Get schema-aware table name
function getTableName(tableName, schemaName) {
return schemaName ?
`${schemaName}.${tableName}`
: tableName;
}

//Get schema-aware query builder for a given table and schema name
function getTable(trxOrKnex, tableName, schemaName) {
return schemaName ? trxOrKnex(tableName).withSchema(schemaName)
: trxOrKnex(tableName);
}

//Get schema-aware schema builder for a given schema nam
function getSchemaBuilder(trxOrKnex, schemaName) {
return schemaName ? trxOrKnex.schema.withSchema(schemaName)
: trxOrKnex.schema;
}
4 changes: 2 additions & 2 deletions test/integration/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ var logger = require('./logger');
var config = require('../knexfile');
var fs = require('fs');

var Promise = require('bluebird')
var Promise = require('bluebird');

Promise.each(Object.keys(config), function(dialectName) {
return require('./suite')(logger(knex(config[dialectName])));
})
});

after(function(done) {
if (config.sqlite3 && config.sqlite3.connection.filename !== ':memory:') {
Expand Down
71 changes: 54 additions & 17 deletions test/integration/migrate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,19 @@ module.exports = function(knex) {
return knex.migrate.status({directory: 'test/integration/migrate/test'}).then(function(migrationLevel) {
expect(migrationLevel).to.equal(3);
})
.then(function() {
// Cleanup the added migrations
if (/redshift/.test(knex.client.dialect)){
.then(function() {
// Cleanup the added migrations
if (/redshift/.test(knex.client.dialect)){
return knex('knex_migrations')
.where('name', 'like', '%foobar%')
.del();
}
return knex('knex_migrations')
.where('name', 'like', '%foobar%')
.del();
}
return knex('knex_migrations')
.where('id', migration1[0])
.orWhere('id', migration2[0])
.orWhere('id', migration3[0])
.del()
});
.where('id', migration1[0])
.orWhere('id', migration2[0])
.orWhere('id', migration3[0])
.del()
});
});
});

Expand Down Expand Up @@ -239,11 +239,11 @@ module.exports = function(knex) {
knex.migrate.latest({directory: 'test/integration/migrate/test'}),
knex.migrate.latest({directory: 'test/integration/migrate/test'})
])
.then(function () {
return knex('knex_migrations').select('*').then(function (data) {
expect(data.length).to.equal(2);
.then(function () {
return knex('knex_migrations').select('*').then(function (data) {
expect(data.length).to.equal(2);
});
});
});
});
}

Expand Down Expand Up @@ -344,6 +344,43 @@ module.exports = function(knex) {

});

});
if (knex.client.dialect === 'postgresql') {
describe('knex.migrate.latest with specific changelog schema', function () {

it('should create changelog in the correct schema without transactions', function (done) {
knex.migrate.latest({
directory: 'test/integration/migrate/test',
disableTransactions: true,
schemaName: 'testschema'
}).then(() => {
return knex('testschema.knex_migrations').select('*').then(function (data) {
expect(data.length).to.equal(2);
done();
});
});
});

it('should create changelog in the correct schema with transactions', function (done) {
knex.migrate.latest({
directory: 'test/integration/migrate/test',
disableTransactions: false,
schemaName: 'testschema'
}).then(() => {
return knex('testschema.knex_migrations').select('*').then(function (data) {
expect(data.length).to.equal(2);
done();
});
});
});

afterEach(function() {
return knex.migrate.rollback({
directory: 'test/integration/migrate/test',
disableTransactions: true,
schemaName: 'testschema'
});
});
});
}
});
};
3 changes: 2 additions & 1 deletion test/integration/suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ module.exports = function(knex) {

after(function() {
return knex.destroy()
})
});

require('./schema')(knex);
require('./migrate')(knex);

require('./seed')(knex);
require('./builder/inserts')(knex);
require('./builder/selects')(knex);
Expand Down