feat(mysql): use mysql patcher to allow incremental schema updates

This commit is contained in:
Zachary Carter 2015-04-06 14:32:55 -07:00
Родитель cdc997dfdf
Коммит 2fbfbbdab9
9 изменённых файлов: 242 добавлений и 53 удалений

Просмотреть файл

@ -2,85 +2,103 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const path = require('path');
const mysql = require('mysql');
const buf = require('buf').hex;
const MysqlPatcher = require('mysql-patcher');
const AppError = require('../error');
const config = require('../config');
const logger = require('../logging')('db.mysql');
const P = require('../promise');
const AppError = require('../../error');
const config = require('../../config');
const logger = require('../../logging')('db.mysql');
const P = require('../../promise');
const patch = require('./patch');
const SCHEMA = require('fs').readFileSync(__dirname + '/schema.sql').toString();
function MysqlStore(options) {
if (options.charset && options.charset !== 'UTF8_UNICODE_CI') {
logger.warn('createDatabase', { charset: options.charset });
logger.warn('createDatabase', { charset: options.charset });
} else {
options.charset = 'UTF8_UNICODE_CI';
}
options.typeCast = function(field, next) {
if (field.type === 'TINY' && field.length === 1) {
return field.string() === '1';
}
return next();
};
this._pool = mysql.createPool(options);
}
function createSchema(client, options) {
logger.verbose('createSchema', options);
// Apply patches up to the current patch level.
// This will also create the DB if it is missing.
function updateDbSchema(patcher) {
logger.verbose('updateDbSchema', patcher.options);
var d = P.defer();
var database = options.database;
patcher.patch(function(err) {
if (err) {
logger.error('updateDbSchema', err);
return d.reject(err);
}
d.resolve();
});
logger.verbose('createDatabase', database);
client.query('CREATE DATABASE IF NOT EXISTS ' + database
+ ' CHARACTER SET utf8 COLLATE utf8_unicode_ci', function(err) {
if (err) {
logger.error('createDatabase', err);
return d.reject(err);
return d.promise;
}
// Sanity-check that we're working with a compatible patch level.
function checkDbPatchLevel(patcher) {
logger.verbose('checkDbPatchLevel', patcher.options);
var d = P.defer();
patcher.readDbPatchLevel(function(err) {
if (err) {
logger.error('checkDbPatchLevel', err);
return d.reject(err);
}
// We are only guaranteed to run correctly if we're at the current
// patch level for this version of the code (the normal state of
// affairs) or the one immediately above it (during a deployment).
if (patcher.currentPatchLevel !== patch.level) {
if (patcher.currentPatchLevel !== patch.level + 1) {
err = 'unexpected db patch level: ' + patcher.currentPatchLevel;
logger.error('checkDbPatchLevel', err);
return d.reject(new Error(err));
}
}
d.resolve();
});
logger.verbose('changeUser');
client.changeUser({
user: options.user,
password: options.password,
database: database
}, function(err) {
if (err) {
logger.error('changeUser', err);
return d.reject(err);
}
logger.verbose('creatingSchema');
client.query(SCHEMA, function(err) {
if (err) {
logger.error('creatingSchema', err);
return d.reject(err);
}
d.resolve();
});
});
});
return d.promise;
}
MysqlStore.connect = function mysqlConnect(options) {
if (options.createSchema) {
// ugly, but you can't connect to a database before the database actually
// exists. So remove and restore it later.
var database = options.database;
delete options.database;
options.multipleStatements = true;
var schemaConn = mysql.createConnection(options);
options.database = database;
options.createDatabase = options.createSchema;
options.dir = path.join(__dirname, 'patches');
options.metaTable = 'dbMetadata';
options.patchKey = 'schema-patch-level';
options.patchLevel = patch.level;
options.mysql = mysql;
var patcher = new MysqlPatcher(options);
return createSchema(schemaConn, options).then(function() {
schemaConn.end();
delete options.multipleStatements;
options.database = database;
return new MysqlStore(options);
});
} else {
return P.resolve(new MysqlStore(options));
}
return P.promisify(patcher.connect, patcher)().then(function() {
if (options.createSchema) {
return updateDbSchema(patcher);
}
}).then(function() {
return checkDbPatchLevel(patcher);
}).then(function() {
return P.promisify(patcher.end, patcher)();
}).then(function() {
return new MysqlStore(options);
});
};
const Q_AVATAR_INSERT = 'INSERT INTO avatars (id, url, userId, providerId) ' +
'VALUES (?, ?, ?, ?)';
const Q_AVATAR_UPDATE = 'INSERT INTO avatar_selected (userId, avatarId) '
@ -98,6 +116,12 @@ const Q_PROVIDER_INSERT = 'INSERT INTO avatar_providers (name) VALUES (?)';
const Q_PROVIDER_GET_BY_NAME = 'SELECT * FROM avatar_providers WHERE name=?';
const Q_PROVIDER_GET_BY_ID = 'SELECT * FROM avatar_providers WHERE id=?';
const Q_PROFILE_DISPLAY_NAME_UPDATE = 'INSERT INTO profile ' +
'(userId, displayName) VALUES (?, ?) ON DUPLICATE KEY UPDATE ' +
'displayName = VALUES(displayName)';
const Q_PROFILE_DISPLAY_NAME_GET = 'SELECT displayName FROM profile ' +
'WHERE userId=?';
function firstRow(rows) {
return rows[0];
}
@ -173,6 +197,14 @@ MysqlStore.prototype = {
return this._readOne(Q_PROVIDER_GET_BY_ID, [id]);
},
setDisplayName: function setDisplayName(uid, displayName) {
return this._write(Q_PROFILE_DISPLAY_NAME_UPDATE, [buf(uid), displayName]);
},
getDisplayName: function getDisplayName(uid) {
return this._readOne(Q_PROFILE_DISPLAY_NAME_GET, [buf(uid)]);
},
_write: function _write(sql, params) {
return this._query(this._pool, sql, params);
},

10
lib/db/mysql/patch.js Normal file
Просмотреть файл

@ -0,0 +1,10 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// The expected patch level of the database.
// Update this if you add a new patch, and don't forget to update
// the documentation for the current schema in ../schema.sql.
module.exports.level = 2;

Просмотреть файл

@ -0,0 +1,9 @@
-- Create the 'dbMetadata' table.
-- Note: This should be the only thing in this initial patch.
CREATE TABLE dbMetadata (
name VARCHAR(255) NOT NULL PRIMARY KEY,
value VARCHAR(255) NOT NULL
) ENGINE=InnoDB;
INSERT INTO dbMetadata SET name = 'schema-patch-level', value = '1';

Просмотреть файл

@ -0,0 +1,2 @@
-- -- drop the dbMetadata table
-- DROP TABLE dbMetadata;

Просмотреть файл

@ -0,0 +1,30 @@
-- Create the initial set of tables.
--
-- Since this is the first migration, we use `IF NOT EXISTS` to allow us
-- to run this on a db that already has the original schema in place. The
-- patch will then be a no-op. Subsequent patches should *not* use `IF
-- NOT EXISTS` but should fail noisily if the db is in an unexpected state.
CREATE TABLE IF NOT EXISTS avatar_providers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(32) NOT NULL,
UNIQUE INDEX avatar_providers_name(name)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
CREATE TABLE IF NOT EXISTS avatars (
id BINARY(16) PRIMARY KEY,
url VARCHAR(256) NOT NULL,
userId BINARY(16) NOT NULL,
INDEX avatars_user_id(userId),
providerId INT NOT NULL,
FOREIGN KEY (providerId) REFERENCES avatar_providers(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
CREATE TABLE IF NOT EXISTS avatar_selected (
userId BINARY(16) NOT NULL PRIMARY KEY,
avatarId BINARY(16) NOT NULL,
FOREIGN KEY (avatarId) REFERENCES avatars(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;
UPDATE dbMetadata SET value = '2' WHERE name = 'schema-patch-level';

Просмотреть файл

@ -0,0 +1,9 @@
-- Drop all the tables.
-- (commented out to avoid accidentally running this in production...)
-- DROP TABLE avatar_selected;
-- DROP TABLE avatars;
-- DROP TABLE avatar_providers;
-- UPDATE dbMetadata SET value = '1' WHERE name = 'schema-patch-level';

Просмотреть файл

@ -1,3 +1,12 @@
--
-- This file represents the current db schema.
-- It exists mainly for documentation purposes; any automated database
-- modifications are controlled by the files in the ./patches/ directory.
--
-- If you make a change here, you should also create a new database patch
-- file and increment the level in ./patch.js to reflect the change.
--
CREATE TABLE IF NOT EXISTS avatar_providers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(32) NOT NULL,

87
npm-shrinkwrap.json сгенерированный
Просмотреть файл

@ -599,6 +599,93 @@
}
}
},
"mysql-patcher": {
"version": "0.7.0",
"from": "mysql-patcher@0.7.0",
"resolved": "https://registry.npmjs.org/mysql-patcher/-/mysql-patcher-0.7.0.tgz",
"dependencies": {
"async": {
"version": "0.9.0",
"from": "async@^0.9.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz"
},
"clone": {
"version": "0.1.19",
"from": "clone@^0.1.18",
"resolved": "https://registry.npmjs.org/clone/-/clone-0.1.19.tgz"
},
"glob": {
"version": "5.0.5",
"from": "glob@^5.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-5.0.5.tgz",
"dependencies": {
"inflight": {
"version": "1.0.4",
"from": "inflight@^1.0.4",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.1",
"from": "wrappy@1",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz"
}
}
},
"inherits": {
"version": "2.0.1",
"from": "inherits@2",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
},
"minimatch": {
"version": "2.0.4",
"from": "minimatch@^2.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.4.tgz",
"dependencies": {
"brace-expansion": {
"version": "1.1.0",
"from": "brace-expansion@^1.0.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.0.tgz",
"dependencies": {
"balanced-match": {
"version": "0.2.0",
"from": "balanced-match@^0.2.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.2.0.tgz"
},
"concat-map": {
"version": "0.0.1",
"from": "concat-map@0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
}
}
}
}
},
"once": {
"version": "1.3.1",
"from": "once@^1.3.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.1.tgz",
"dependencies": {
"wrappy": {
"version": "1.0.1",
"from": "wrappy@1",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz"
}
}
},
"path-is-absolute": {
"version": "1.0.0",
"from": "path-is-absolute@^1.0.0",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
}
}
},
"xtend": {
"version": "4.0.0",
"from": "xtend@^4.0.0",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.0.tgz"
}
}
},
"request": {
"version": "2.47.0",
"from": "request@2.47.0",

Просмотреть файл

@ -21,6 +21,7 @@
"joi": "4.7.0",
"mozlog": "1.0.0",
"mysql": "2.5.2",
"mysql-patcher": "0.7.0",
"request": "2.47.0",
"stream-to-array": "2.0.2"
},