feat(mysql): use mysql patcher to allow incremental schema updates
This commit is contained in:
Родитель
cdc997dfdf
Коммит
2fbfbbdab9
|
@ -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);
|
||||
},
|
|
@ -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,
|
|
@ -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"
|
||||
},
|
||||
|
|
Загрузка…
Ссылка в новой задаче