diff --git a/README.md b/README.md index 49dcdb8..4fb5d47 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,12 @@ e.g. Reverse patch file : `patch-02-01.sql` UPDATE metadata SET value = '1' WHERE name = 'schema-patch-level'; ``` +## Changelog ## + +### v0.4.0 - 2014-11-14 ### + +* added a check between patches to make sure the patch level was incremented properly + ## License ## [Mozilla Public License v2](https://www.mozilla.org/MPL/2.0/) diff --git a/index.js b/index.js index d9bd3ce..08f2e5d 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,8 @@ var clone = require('clone') // globals for this package var noop = Function.prototype // a No Op function +var ERR_NO_SUCH_TABLE = 1146 + // the main export from this package function patch(options, callback) { callback = callback || noop @@ -100,14 +102,14 @@ function changeUser(callback) { } function checkDbMetadataExists(callback) { - var context = this + var ctx = this var query = "SELECT COUNT(*) AS count FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ?" this.connection.query( query, - [ this.options.database, this.options.metaTable ], + [ ctx.options.database, ctx.options.metaTable ], function (err, result) { if (err) { return callback(err) } - context.metaTableExists = result[0].count === 0 ? false : true + ctx.metaTableExists = result[0].count === 0 ? false : true callback() } ) @@ -229,7 +231,43 @@ function applyPatches(callback) { ctx.patchesToApply, function(patch, donePatch) { // emit : 'Updating DB for patch ' + patch.from + ' to ' + patch.to - ctx.connection.query(patch.sql, donePatch) + ctx.connection.query(patch.sql, function(err, info) { + if (err) return donePatch(err) + + // check that the database is now at the (intermediate) patch level + var query = "SELECT value FROM " + ctx.options.metaTable + " WHERE name = ?" + ctx.connection.query( + query, + [ ctx.options.patchKey ], + function(err, result) { + if (err) { + // this is not an error if we are wanting to patch to level 0 + // and the problem is that the metaTable is not there + if ( patch.to === 0 && err.errno === ERR_NO_SUCH_TABLE ) { + return donePatch() + } + + // otherwise, return this error since we don't know what it is + return donePatch(err) + } + + if ( result.length === 0 ) { + // nothing in the table yet + return donePatch(new Error('The patchKey does not exist in the metaTable')) + } + + // convert the patch level from a string to a number + result[0].value = +result[0].value + + // check if this value is incorrect + if ( result[0].value !== patch.to ) { + return donePatch(new Error('Patch level in metaTable (%s) is incorrect after this patch (%s)', result[0].value, patch.to)) + } + + donePatch() + } + ) + }) }, callback ) diff --git a/package.json b/package.json index a34ce3a..c22f9c9 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "dependencies": { "async": "^0.9.0", "bluebird": "^2.3.0", - "clone": "^0.1.18" + "clone": "^0.1.18", + "xtend": "^4.0.0" }, "devDependencies": { "mysql": "^2.4.2", diff --git a/test/end-to-end.js b/test/end-to-end.js index 724a11a..9015c06 100644 --- a/test/end-to-end.js +++ b/test/end-to-end.js @@ -12,7 +12,7 @@ var options = { createDatabase : true, user : 'root', database : 'patcher', - // password : '', + password : '', dir : path.join(__dirname, 'end-to-end'), metaTable : 'metadata', patchKey : 'schema-patch-level', @@ -24,14 +24,15 @@ var options = { var connection = mysql.createConnection(options) test('run an end to end test, with no error (to patch 0)', function(t) { + t.plan(5) + // change the expected patchLevel to 0 options.patchLevel = 0 patcher.patch(options, function(err, res) { - console.log(err, res) t.ok(!err, 'There was no error when patching the database') - // create a connection and check the metadata key has been updated to 3 + // check the metadata table does not yet exist connection.query("SELECT value FROM metadata WHERE name = 'schema-patch-level'", function(err, res) { t.ok(err, 'There was an error getting the database patch level') t.equal(err.code, 'ER_NO_SUCH_TABLE', 'No metadata table') @@ -44,13 +45,15 @@ test('run an end to end test, with no error (to patch 0)', function(t) { }) test('run an end to end test, with no error(to patch 3)', function(t) { - // change the expected patchLevel to 0 + t.plan(3) + + // change the expected patchLevel to 3 options.patchLevel = 3 patcher.patch(options, function(err, res) { t.ok(!err, 'There was no error when patching the database') - // create a connection and check the metadata key has been updated to 3 + // check the metadata key has been updated to 3 connection.query("SELECT value FROM metadata WHERE name = 'schema-patch-level'", function(err, res) { t.ok(!err, 'There was no error getting the database patch level') @@ -61,9 +64,26 @@ test('run an end to end test, with no error(to patch 3)', function(t) { }) }) +test('run an end to end test, with no error (back to patch 0)', function(t) { + t.plan(5) + // change the expected patchLevel to 0 + options.patchLevel = 0 + patcher.patch(options, function(err, res) { + t.ok(!err, 'There was no error when patching the database') + // check the metadata table no longer exists + connection.query("SELECT value FROM metadata WHERE name = 'schema-patch-level'", function(err, res) { + t.ok(err, 'There was an error getting the database patch level') + t.equal(err.code, 'ER_NO_SUCH_TABLE', 'No metadata table') + t.equal(err.errno, 1146, 'Correct error number') + t.equal(err.message, "ER_NO_SUCH_TABLE: Table 'patcher.metadata' doesn't exist", 'Correct message') + + t.end() + }) + }) +}) test('the last test, just to close the connection', function(t) { connection.end() diff --git a/test/failed-create-metatable/patch-000-001.sql b/test/failed-create-metatable/patch-000-001.sql new file mode 100644 index 0000000..57a54b1 --- /dev/null +++ b/test/failed-create-metatable/patch-000-001.sql @@ -0,0 +1 @@ +-- Do Nothing diff --git a/test/failed-create-metatable/patch-001-000.sql b/test/failed-create-metatable/patch-001-000.sql new file mode 100644 index 0000000..57a54b1 --- /dev/null +++ b/test/failed-create-metatable/patch-001-000.sql @@ -0,0 +1 @@ +-- Do Nothing diff --git a/test/failed-insert/patch-000000-000001.sql b/test/failed-insert/patch-000000-000001.sql new file mode 100644 index 0000000..1c89f1b --- /dev/null +++ b/test/failed-insert/patch-000000-000001.sql @@ -0,0 +1,10 @@ +-- Create the 'metadata' table. +-- Note: This should be the only thing in this initial patch. + +CREATE TABLE failed_insert ( + name VARCHAR(255) NOT NULL PRIMARY KEY, + value VARCHAR(255) NOT NULL +) ENGINE=InnoDB; + +-- Not doing this, so that the check fails. +-- INSERT INTO failed_insert SET name = 'schema-patch-level', value = '1'; diff --git a/test/failed-insert/patch-000001-000000.sql b/test/failed-insert/patch-000001-000000.sql new file mode 100644 index 0000000..168c0cb --- /dev/null +++ b/test/failed-insert/patch-000001-000000.sql @@ -0,0 +1,2 @@ +-- -- drop the metadata table +DROP TABLE failed_insert; diff --git a/test/failed.js b/test/failed.js new file mode 100644 index 0000000..11e7022 --- /dev/null +++ b/test/failed.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var path = require('path') + +var xtend = require('xtend') +var mysql = require('mysql') +var test = require('tape') + +var patcher = require('../') + +var options = { + createDatabase : true, + user : 'root', + database : 'patcher', + password : '', + patchKey : 'schema-patch-level', + mysql : mysql, + + // dir : ?, // \ + // metaTable : ?, // > to be set in each test + // patchLevel : ?, // / +} + +test('run an end to end test, error updating metaTable in patch 1', function(t) { + // set expected patch and metaTable + var opts = xtend( + options, + { + dir : path.join(__dirname, 'failed-insert'), + patchLevel : 1, + metaTable : 'failed_insert', + } + ) + + // create a connection for us to use directly + var connection = mysql.createConnection(opts) + + t.plan(3) + + patcher.patch(opts, function(err, res) { + t.ok(err, 'There was an error when patching the database') + t.ok(!res, 'No result was returned') + t.equal('' + err, 'Error: The patchKey does not exist in the metaTable', 'Error message is correct') + connection.end() + t.end() + }) +}) + +test('run an end to end test, metaTable not created', function(t) { + // set expected patch and metaTable + var opts = xtend( + options, + { + dir : path.join(__dirname, 'failed-create-metatable'), + patchLevel : 1, + metaTable : 'failed_create_metatable', + } + ) + + // create a connection for us to use directly + var connection = mysql.createConnection(opts) + + t.plan(4) + + patcher.patch(opts, function(err, res) { + t.ok(err, 'There was an error when patching the database') + t.ok(!res, 'No result was returned') + // t.equal('' + err, 'Error: ER_NO_SUCH_TABLE: Table \'patcher.failed_create_metatable\' doesn\'t exist', 'Error message is correct') + t.equal(err.errno, 1146, 'Error number is correct') + t.equal(err.code, 'ER_NO_SUCH_TABLE', 'Error code is correct') + connection.end() + t.end() + }) +}) diff --git a/test/patches.js b/test/patches.js index afe18c4..b8de61d 100644 --- a/test/patches.js +++ b/test/patches.js @@ -145,24 +145,39 @@ test('check all patches are available (fails, no patch #1)', function(t) { test('checking that these patch files are executed', function(t) { var count = 0 var ctx = { + options : { + dir : path.join(__dirname, 'end-to-end'), + metaTable : 'metadata', + patchKey : 'schema-patch-level', + }, connection : { - query : function(sql, callback) { + query : function(sql, args, callback) { + if ( typeof callback === 'undefined' ) { + callback = args + args = undefined + } + // if this query is the patcher trying to get the patch level, ignore it + if ( sql.match(/SELECT value FROM metadata WHERE name/) ) { + return callback(null, []) + } t.equal(sql, ctx.patchesToApply[count].sql, 'SQL is correct') count += 1 - callback() + callback(null, []) }, }, - patchesToApply : [ - { sql : '-- 0->1' }, - { sql : '-- 1->2' }, - { sql : '-- 2->3' }, - ], } - patcher.applyPatches.call(ctx, function(err) { + patcher.readPatchFiles.call(ctx, function(err) { t.ok(!err, 'No error occurred') - t.end() + patcher.checkAllPatchesAvailable.call(ctx, function(err) { + t.ok(!err, 'No error occurred') + patcher.applyPatches.call(ctx, function(err) { + t.ok(!err, 'No error occurred') + t.end() + }) + }) }) + }) test('checking that an error comes back if a patch is missing', function(t) { @@ -170,15 +185,19 @@ test('checking that an error comes back if a patch is missing', function(t) { var count = 0 var ctx = { + options : { + metaTable : 'metadata', + patchKey : 'level', + }, + patchesToApply : [ + { sql : '-- 0->1' }, + ], connection : { query : function(sql, callback) { t.equal(sql, '-- 0->1', 'The sql is what is expected') callback(new Error('Something went wrong')) }, }, - patchesToApply : [ - { sql : '-- 0->1' }, - ], } patcher.applyPatches.call(ctx, function(err) {