Merge pull request #11 from strongloop/feature/connection-pool

Feature/connection pool
This commit is contained in:
Raymond Feng 2013-11-27 09:44:19 -08:00
commit 76d7f71766
3 changed files with 186 additions and 138 deletions

View File

@ -20,18 +20,24 @@ exports.initialize = function initializeDataSource(dataSource, callback) {
}
var s = dataSource.settings;
if (s.collation) {
s.charset = s.collation.substr(0,s.collation.indexOf('_')); // Charset should be first 'chunk' of collation.
s.charset = s.collation.substr(0, s.collation.indexOf('_')); // Charset should be first 'chunk' of collation.
} else {
s.collation = 'utf8_general_ci';
s.charset = 'utf8';
}
s.supportBigNumbers = (s.supportBigNumbers || false);
s.timezone = (s.timezone || 'local');
dataSource.client = mysql.createConnection({
if(isNaN(s.connectionLimit)) {
s.connectionLimit = s.connectionLimit;
} else {
s.connectionLimit = 10;
}
var options = {
host: s.host || s.hostname || 'localhost',
port: s.port || 3306,
user: s.username || s.user,
@ -40,44 +46,39 @@ exports.initialize = function initializeDataSource(dataSource, callback) {
debug: s.debug,
socketPath: s.socketPath,
charset: s.collation.toUpperCase(), // Correct by docs despite seeming odd.
supportBigNumbers: s.supportBigNumbers
});
supportBigNumbers: s.supportBigNumbers,
connectionLimit: s.connectionLimit
};
// Don't configure the DB if the pool can be used for multiple DBs
if(!s.createDatabase) {
options.database = s.database;
}
dataSource.client = mysql.createPool(options);
dataSource.client.on('error', function (err) {
dataSource.emit('error', err);
dataSource.connected = false;
dataSource.connecting = false;
});
if(s.debug) {
if (s.debug) {
console.log('Settings: ', s);
}
dataSource.connector = new MySQL(dataSource.client, s);
dataSource.connector.dataSource = dataSource;
dataSource.client.query('USE `' + s.database + '`', function (err) {
if (err) {
if (err.message.match(/(^|: )unknown database/i)) {
var dbName = s.database;
var charset = s.charset;
var collation = s.collation;
var q = 'CREATE DATABASE ' + dbName + ' CHARACTER SET ' + charset + ' COLLATE ' + collation;
dataSource.client.query(q, function (error) {
if (!error) {
dataSource.client.query('USE ' + s.database, callback);
} else {
throw error;
}
});
} else throw err;
} else callback();
// MySQL specific column types
juggler.ModelBuilder.registerType(function Point() {
});
// MySQL specific column types
juggler.ModelBuilder.registerType(function Point() {});
dataSource.EnumFactory = EnumFactory; // factory for Enums. Note that currently Enums can not be registered.
process.nextTick(function() {
callback && callback();
});
};
exports.MySQL = MySQL;
@ -103,6 +104,7 @@ require('util').inherits(MySQL, juggler.BaseSQL);
* @param {Function} [callback] The callback after the SQL statement is executed
*/
MySQL.prototype.query = function (sql, callback) {
var self = this;
if (!this.dataSource.connected) {
return this.dataSource.on('connected', function () {
this.query(sql, callback);
@ -111,36 +113,69 @@ MySQL.prototype.query = function (sql, callback) {
var client = this.client;
var time = Date.now();
var debug = this.settings.debug;
var db = this.settings.database;
var log = this.log;
if (typeof callback !== 'function') throw new Error('callback should be a function');
if(debug) {
console.log('SQL:' , sql);
if (debug) {
console.log('SQL:', sql);
}
this.client.query(sql, function (err, data) {
if(err) {
if(debug) {
console.error('Error:', err);
}
}
if (err && err.message.match(/(^|: )unknown database/i)) {
var dbName = err.message.match(/(^|: )unknown database '(.*?)'/i)[1];
client.query('CREATE DATABASE ' + dbName, function (error) {
if (!error) {
client.query(sql, callback);
} else {
callback(err);
function releaseConnectionAndCallback(connection, err, result) {
connection.release();
callback && callback(err, result);
}
function runQuery(connection) {
connection.query(sql, function (err, data) {
if (debug) {
if (err) {
console.error('Error:', err);
}
});
console.log('Data:', data);
}
if (log) log(sql, time);
releaseConnectionAndCallback(connection, err, data);
});
}
client.getConnection(function (err, connection) {
if (err) {
callback && callback(err);
return;
}
if(debug) {
console.log('Data:' , data);
if (self.settings.createDatabase) {
// Call USE db ...
connection.query('USE `' + db + '`', function (err) {
if (err) {
if (err && err.message.match(/(^|: )unknown database/i)) {
var charset = self.settings.charset;
var collation = self.settings.collation;
var q = 'CREATE DATABASE ' + db + ' CHARACTER SET ' + charset + ' COLLATE ' + collation;
connection.query(q, function (err) {
if (!err) {
connection.query('USE `' + db + '`', function (err) {
runQuery(connection);
});
} else {
releaseConnectionAndCallback(connection, err);
}
});
return;
} else {
releaseConnectionAndCallback(connection, err);
return;
}
}
runQuery(connection);
});
} else {
// Bypass USE db
runQuery(connection);
}
if (log) log(sql, time);
callback(err, data);
});
};
/**
* Create the data model in MySQL
*
@ -243,8 +278,8 @@ MySQL.prototype.toDatabase = function (prop, val) {
val = val[operator];
if (operator === 'between') {
return this.toDatabase(prop, val[0]) +
' AND ' +
this.toDatabase(prop, val[1]);
' AND ' +
this.toDatabase(prop, val[1]);
} else if (operator == 'inq' || operator == 'nin') {
if (!(val.propertyIsEnumerable('length')) && typeof val === 'object' && typeof val.length === 'number') { //if value is array
for (var i = 0; i < val.length; i++) {
@ -267,7 +302,7 @@ MySQL.prototype.toDatabase = function (prop, val) {
}
if (prop.type.name == "Boolean") return val ? 1 : 0;
if (prop.type.name === 'GeoPoint') {
return val ? 'Point(' + val.lat + ',' + val.lng +')' : 'NULL';
return val ? 'Point(' + val.lat + ',' + val.lng + ')' : 'NULL';
}
if (typeof prop.type === 'function') return this.client.escape(prop.type(val));
return this.client.escape(val.toString());
@ -285,27 +320,27 @@ MySQL.prototype.fromDatabase = function (model, data) {
}
var props = this._models[model].properties;
var json = {};
for(var p in props) {
for (var p in props) {
var key = this.column(model, p);
var val = data[key];
if (typeof val === 'undefined' || val === null) {
continue;
}
if (props[p]) {
switch(props[p].type.name) {
switch (props[p].type.name) {
case 'Date':
val = new Date(val.toString().replace(/GMT.*$/, 'GMT'));
break;
val = new Date(val.toString().replace(/GMT.*$/, 'GMT'));
break;
case 'Boolean':
val = Boolean(val);
break;
val = Boolean(val);
break;
case 'GeoPoint':
case 'Point':
val = {
lat: val.x,
lng: val.y
};
break;
val = {
lat: val.x,
lng: val.y
};
break;
}
}
json[p] = val;
@ -317,9 +352,9 @@ MySQL.prototype.escapeName = function (name) {
return '`' + name.replace(/\./g, '`.`') + '`';
};
MySQL.prototype.getColumns = function(model, props){
MySQL.prototype.getColumns = function (model, props) {
var cols = this._models[model].properties;
if(!cols) {
if (!cols) {
return '*';
}
var self = this;
@ -347,7 +382,7 @@ MySQL.prototype.getColumns = function(model, props){
});
}
}
var names = keys.map(function(c) {
var names = keys.map(function (c) {
return self.columnEscaped(model, c);
});
return names.join(', ');
@ -411,7 +446,7 @@ function buildOrderBy(self, model, order) {
if (typeof order === 'string') {
order = [order];
}
return 'ORDER BY ' + order.map(function(o) {
return 'ORDER BY ' + order.map(function (o) {
var t = o.split(/[\s,]+/);
if (t.length === 1) {
return self.columnEscaped(model, o);
@ -435,14 +470,14 @@ MySQL.prototype.all = function all(model, filter, callback) {
var self = this;
// Order by id if no order is specified
filter = filter || {};
if(!filter.order) {
if (!filter.order) {
var idNames = this.idNames(model);
if(idNames && idNames.length) {
if (idNames && idNames.length) {
filter.order = idNames.join(' ');
}
}
var sql = 'SELECT '+ this.getColumns(model, filter.fields) + ' FROM ' + this.tableEscaped(model);
var sql = 'SELECT ' + this.getColumns(model, filter.fields) + ' FROM ' + this.tableEscaped(model);
if (filter) {
@ -488,7 +523,7 @@ MySQL.prototype.all = function all(model, filter, callback) {
*
*/
MySQL.prototype.destroyAll = function destroyAll(model, where, callback) {
if(!callback && 'function' === typeof where) {
if (!callback && 'function' === typeof where) {
callback = where;
where = undefined;
}
@ -552,7 +587,7 @@ MySQL.prototype.createTable = function (model, cb) {
var engine = metadata && metadata.engine;
var sql = 'CREATE TABLE ' + this.tableEscaped(model) +
' (\n ' + this.propertiesSQL(model) + '\n)';
if(engine) {
if (engine) {
sql += 'ENGINE=' + engine + '\n';
}
this.query(sql, cb);
@ -645,7 +680,7 @@ MySQL.prototype.alterTable = function (model, actualFields, actualIndexes, done,
// remove indexes
aiNames.forEach(function (indexName) {
if (indexName === 'PRIMARY'|| (m.properties[indexName] && self.id(model, indexName))) return;
if (indexName === 'PRIMARY' || (m.properties[indexName] && self.id(model, indexName))) return;
if (indexNames.indexOf(indexName) === -1 && !m.properties[indexName] || m.properties[indexName] && !m.properties[indexName].index) {
sql.push('DROP INDEX `' + indexName + '`');
} else {
@ -733,15 +768,15 @@ MySQL.prototype.alterTable = function (model, actualFields, actualIndexes, done,
function changed(newSettings, oldSettings) {
if (oldSettings.Null === 'YES') { // Used to allow null and does not now.
if(newSettings.allowNull === false) return true;
if(newSettings.null === false) return true;
if (newSettings.allowNull === false) return true;
if (newSettings.null === false) return true;
}
if (oldSettings.Null === 'NO') { // Did not allow null and now does.
if(newSettings.allowNull === true) return true;
if(newSettings.null === true) return true;
if(newSettings.null === undefined && newSettings.allowNull === undefined) return true;
if (newSettings.allowNull === true) return true;
if (newSettings.null === true) return true;
if (newSettings.null === undefined && newSettings.allowNull === undefined) return true;
}
if (oldSettings.Type.toUpperCase() !== datatype(newSettings).toUpperCase()) return true;
return false;
}
@ -771,12 +806,12 @@ MySQL.prototype.propertiesSQL = function (model) {
}
/*
var sql = ['`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'];
Object.keys(this._models[model].properties).forEach(function (prop) {
if (self.id(model, prop)) return;
sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop));
});
*/
var sql = ['`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY'];
Object.keys(this._models[model].properties).forEach(function (prop) {
if (self.id(model, prop)) return;
sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop));
});
*/
// Declared in model index property indexes.
Object.keys(this._models[model].properties).forEach(function (prop) {
@ -787,8 +822,8 @@ MySQL.prototype.propertiesSQL = function (model) {
});
// Settings might not have an indexes property.
var dxs = this._models[model].settings.indexes;
if(dxs){
Object.keys(this._models[model].settings.indexes).forEach(function(prop){
if (dxs) {
Object.keys(this._models[model].settings.indexes).forEach(function (prop) {
sql.push(self.indexSettingsSQL(model, prop));
});
}
@ -835,19 +870,19 @@ MySQL.prototype.indexSettingsSQL = function (model, prop) {
MySQL.prototype.propertySettingsSQL = function (model, prop) {
var p = this._models[model].properties[prop];
var line = this.columnDataType(model, prop) + ' ' +
(p.nullable === false || p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL');
var line = this.columnDataType(model, prop) + ' ' +
(p.nullable === false || p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL');
return line;
};
MySQL.prototype.columnDataType = function (model, property) {
var columnMetadata = this.columnMetadata(model, property);
var colType = columnMetadata && columnMetadata.dataType;
if(colType) {
if (colType) {
colType = colType.toUpperCase();
}
var prop = this._models[model].properties[property];
if(!prop) {
if (!prop) {
return null;
}
var colLength = columnMetadata && columnMetadata.dataLength || prop.length;
@ -865,36 +900,36 @@ function datatype(p) {
case 'JSON':
dt = columnType(p, 'VARCHAR');
dt = stringOptionsByType(p, dt);
break;
break;
case 'Text':
dt = columnType(p, 'TEXT');
dt = stringOptionsByType(p, dt);
break;
break;
case 'Number':
dt = columnType(p, 'INT');
dt = numericOptionsByType(p, dt);
break;
break;
case 'Date':
dt = columnType(p, 'DATETIME'); // Currently doesn't need options.
break;
break;
case 'Boolean':
dt = 'TINYINT(1)';
break;
break;
case 'Point':
case 'GeoPoint':
dt = 'POINT';
break;
break;
case 'Enum':
dt = 'ENUM(' + p.type._string + ')';
dt = stringOptions(p, dt); // Enum columns can have charset/collation.
break;
break;
}
return dt;
}
function columnType(p, defaultType) {
var dt = defaultType;
if(p.dataType){
if (p.dataType) {
dt = String(p.dataType);
}
return dt;
@ -906,24 +941,24 @@ function stringOptionsByType(p, dt) {
case 'varchar':
case 'char':
dt += '(' + (p.limit || 255) + ')';
break;
break;
case 'text':
case 'tinytext':
case 'mediumtext':
case 'longtext':
break;
break;
}
dt = stringOptions(p, dt);
return dt;
}
function stringOptions(p, dt){
if(p.charset){
function stringOptions(p, dt) {
if (p.charset) {
dt += " CHARACTER SET " + p.charset;
}
if(p.collation){
if (p.collation) {
dt += " COLLATE " + p.collation;
}
return dt;
@ -939,17 +974,17 @@ function numericOptionsByType(p, dt) {
case 'integer':
case 'bigint':
dt = integerOptions(p, dt);
break;
break;
case 'decimal':
case 'numeric':
dt = fixedPointOptions(p, dt);
break;
break;
case 'float':
case 'double':
dt = floatingPointOptions(p, dt);
break;
break;
}
dt = unsigned(p, dt);
return dt;
@ -958,17 +993,17 @@ function numericOptionsByType(p, dt) {
function floatingPointOptions(p, dt) {
var precision = 16;
var scale = 8;
if(p.precision){
if (p.precision) {
precision = Number(p.precision);
}
if(p.scale){
if (p.scale) {
scale = Number(p.scale);
}
if (p.precision && p.scale) {
dt += '(' + precision + ',' + scale + ')';
} else if(p.precision){
} else if (p.precision) {
dt += '(' + precision + ')';
}
}
return dt;
}
@ -979,10 +1014,10 @@ function floatingPointOptions(p, dt) {
function fixedPointOptions(p, dt) {
var precision = 9;
var scale = 2;
if(p.precision){
if (p.precision) {
precision = Number(p.precision);
}
if(p.scale){
if (p.scale) {
scale = Number(p.scale);
}
dt += '(' + precision + ',' + scale + ')';
@ -994,56 +1029,68 @@ function integerOptions(p, dt) {
if (p.display || p.limit) {
tmp = Number(p.display || p.limit);
}
if(tmp > 0){
if (tmp > 0) {
dt += '(' + tmp + ')';
} else if(p.unsigned){
} else if (p.unsigned) {
switch (dt.toLowerCase()) {
default:
case 'int':
dt += '(10)';
break;
break;
case 'mediumint':
dt += '(8)';
break;
break;
case 'smallint':
dt += '(5)';
break;
break;
case 'tinyint':
dt += '(3)';
break;
break;
case 'bigint':
dt += '(20)';
break;
break;
}
} else {
switch (dt.toLowerCase()) {
default:
case 'int':
dt += '(11)';
break;
break;
case 'mediumint':
dt += '(9)';
break;
break;
case 'smallint':
dt += '(6)';
break;
break;
case 'tinyint':
dt += '(4)';
break;
break;
case 'bigint':
dt += '(20)';
break;
break;
}
}
return dt;
}
function unsigned(p, dt){
function unsigned(p, dt) {
if (p.unsigned) {
dt += ' UNSIGNED';
}
return dt;
}
/**
* Disconnect from MySQL
*/
MySQL.prototype.disconnect = function () {
if(this.debug) {
console.log('disconnect');
}
if(this.client) {
this.client.end();
}
};
require('./discovery')(MySQL);

View File

@ -8,7 +8,7 @@ describe('migrations', function() {
before(function() {
require('./init.js');
odb = getDataSource({collation: 'utf8_general_ci'});
odb = getDataSource({collation: 'utf8_general_ci', createDatabase: true});
db = odb;
});
@ -41,7 +41,7 @@ describe('migrations', function() {
});
it('should drop db and disconnect all', function(done) {
db.adapter.query('DROP DATABASE IF EXISTS ' + db.settings.database, function(err) {
db.connector.query('DROP DATABASE IF EXISTS ' + db.settings.database, function(err) {
db.client.end(function(){
done();
});
@ -57,14 +57,14 @@ function charsetTest(test_set, test_collo, test_set_str, test_set_collo, done){
assert.ok(!err);
odb.client.end(function(){
db = getSchema({collation: test_set_collo});
db = getSchema({collation: test_set_collo, createDatabase: true});
DummyModel = db.define('DummyModel', {string: String});
db.automigrate(function(){
var q = 'SELECT DEFAULT_COLLATION_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ' + db.client.escape(db.settings.database) + ' LIMIT 1';
db.client.query(q, function(err, r) {
db.connector.query(q, function(err, r) {
assert.ok(!err);
assert.ok(r[0].DEFAULT_COLLATION_NAME.match(test_collo));
db.client.query('SHOW VARIABLES LIKE "character_set%"', function(err, r){
db.connector.query('SHOW VARIABLES LIKE "character_set%"', function(err, r){
assert.ok(!err);
var hit_all = 0;
for (var result in r) {
@ -75,7 +75,7 @@ function charsetTest(test_set, test_collo, test_set_str, test_set_collo, done){
}
assert.equal(hit_all, 4);
});
db.client.query('SHOW VARIABLES LIKE "collation%"', function(err, r){
db.connector.query('SHOW VARIABLES LIKE "collation%"', function(err, r){
assert.ok(!err);
var hit_all = 0;
for (var result in r) {
@ -101,7 +101,7 @@ function matchResult(result, variable_name, match) {
}
var query = function (sql, cb) {
odb.adapter.query(sql, cb);
odb.connector.query(sql, cb);
};

View File

@ -11,7 +11,8 @@ global.getConfig = function(options) {
port: config.port || 3306,
database: 'myapp_test',
username: config.username,
password: config.password
password: config.password,
createDatabase: true
};
if (options) {