/* Copyright (c) 2012, 2014, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ /*global unified_debug, exports */ "use strict"; /* Requires version 2.0 of Felix Geisendoerfer's MySQL client */ var stats = { "created" : 0, "list_tables" : 0, "get_table_metadata" : 0, "connections" : { "successful" : 0, "failed" : 0 } }; var path = require("path"); var mysql = require("mysql"); var mysqlConnection = require("./MySQLConnection.js"); var mysqlDictionary = require("./MySQLDictionary.js"); var udebug = unified_debug.getLogger("MySQLConnectionPool.js"); var util = require('util'); var stats_module = require(mynode.api.stats); var MySQLTime = require("../common/MySQLTime.js"); var DBTableHandler = require("../common/DBTableHandler.js").DBTableHandler; var meta = require("../../api/TableMapping.js").meta; stats_module.register(stats, "spi","mysql","DBConnectionPool"); /* Translate our properties to the driver's */ function getDriverProperties(props) { var driver = {}; if(props.mysql_socket) { driver.SocketPath = props.mysql_socket; } else { driver.host = props.mysql_host; driver.port = props.mysql_port; } if(props.mysql_user) { driver.user = props.mysql_user; } if(props.mysql_password) { driver.password = props.mysql_password; } driver.database = props.database; driver.debug = props.mysql_debug; driver.trace = props.mysql_trace; if (props.mysql_charset) { driver.charset = props.mysql_charset; } else { // by default, use utf-8 multibyte for character encoding driver.charset = 'UTF8MB4'; } if (typeof props.mysql_sql_mode !== 'undefined') { driver.sql_mode = props.mysql_sql_mode; } else { // default to STRICT_ALL_TABLES driver.sql_mode = 'STRICT_ALL_TABLES'; } // connection pool maximum size if (typeof props.mysql_pool_size !== 'undefined') { driver.connectionLimit = props.mysql_pool_size; if (props.mysql_pool_queue_size !== 'undefined') { driver.queueLimit = props.mysql_pool_queue_size; } } // allow multiple statements in one query (used to set character set) driver.multipleStatements = true; return driver; } /** Default domain type converter for timestamp and datetime objects. The domain type is Date * and the intermediate type is MySQLTime. MySQLTime provides a lossless conversion from * database DATETIME and TIMESTAMP with fractional microseconds. The default domain type converter * to javascript Date is lossy: javascript Date does not support microseconds. Users might supply * their own domain type with a converter that supports microseconds. */ var DomainTypeConverterDateTime = function() { // just a bit of documentation for debugging this.converter = 'DomainTypeConverterDateTime'; }; DomainTypeConverterDateTime.prototype.toDB = function toDB(userDate) { if (userDate === null || userDate === undefined) { return userDate; } // convert to the string form of the mySQLTime object var mysqlTime = new MySQLTime(); mysqlTime.fsp = 6; mysqlTime.initializeFromJsDateLocal(userDate); return mysqlTime; }; DomainTypeConverterDateTime.prototype.fromDB = function fromDB(mysqlTime) { if (mysqlTime === null || mysqlTime === undefined) { return mysqlTime; } var jsDate = mysqlTime.toJsDateLocal(); return jsDate; }; /** Default database type converter for timestamp and datetime objects. The database type is string * and the intermediate type is MySQLTime. MySQLTime provides a lossless conversion from * database DATETIME and TIMESTAMP with fractional microseconds. */ var DatabaseTypeConverterDateTime = function() { // just a bit of documentation for debugging this.converter = 'DatabaseTypeConverterDateTime'; }; DatabaseTypeConverterDateTime.prototype.toDB = function toDB(mysqlTime) { if (mysqlTime === null || mysqlTime === undefined) { return mysqlTime; } // convert to the string form of the mySQLTime object var dbDateTime = mysqlTime.toDateTimeString(); return dbDateTime; }; DatabaseTypeConverterDateTime.prototype.fromDB = function fromDB(dbDateTime) { if (dbDateTime === null || dbDateTime === undefined) { return dbDateTime; } var mysqlTime = new MySQLTime(); mysqlTime.initializeFromDateTimeString(dbDateTime); return mysqlTime; }; /* Constructor saves properties but doesn't actually do anything with them until connect is called. */ exports.DBConnectionPool = function(props) { this.props = props; this.driverproperties = getDriverProperties(props); udebug.log('MySQLConnectionPool constructor with driverproperties: ' + util.inspect(this.driverproperties)); // connections that are being used (wrapped by DBSession) this.openConnections = []; this.is_connected = false; // create database type converter map this.databaseTypeConverterMap = {}; this.databaseTypeConverterMap.TIMESTAMP = new DatabaseTypeConverterDateTime(); this.databaseTypeConverterMap.DATETIME = new DatabaseTypeConverterDateTime(); // create domain type converter map this.domainTypeConverterMap = {}; this.domainTypeConverterMap.TIMESTAMP = new DomainTypeConverterDateTime(); this.domainTypeConverterMap.DATETIME = new DomainTypeConverterDateTime(); this.pooling = props.mysql_pool_size ? true:false ; stats.created++; }; /** Register a user-specified domain type converter for this connection pool. * Called by SessionFactory.registerTypeConverter. */ exports.DBConnectionPool.prototype.registerTypeConverter = function(typeName, converterObject) { if (converterObject) { this.domainTypeConverterMap[typeName] = converterObject; } else { this.domainTypeConverterMap[typeName] = undefined; } }; /** Get the database type converter for the parameter type name. * Called when creating the DBTableHandler for a constructor. */ exports.DBConnectionPool.prototype.getDatabaseTypeConverter = function(typeName) { return this.databaseTypeConverterMap[typeName]; }; /** Get the domain type converter for the parameter type name. * Called when creating the DBTableHandler for a constructor. */ exports.DBConnectionPool.prototype.getDomainTypeConverter = function(typeName) { return this.domainTypeConverterMap[typeName]; }; /** Get a connection. If pooling via felix, get a connection from the pool. * If not, create a connection. This api does not manage the list of open connections. * * @param callback (err, connection) */ exports.DBConnectionPool.prototype.getConnection = function(callback) { var connectionPool = this; var connection, error, pool; function getConnectionOnConnection(err, c) { udebug.log('getConnectionOnConnection'); if (err) { stats.connections.failed++; // create a new Error with a message and this stack error = new Error('Connection failed.'); // add cause to the error error.cause = err; // add sqlstate to error error.sqlstate = '08000'; callback(error); } else { stats.connections.successful++; if (connectionPool.pooling) { // some older versions of node-mysql do not have release() if(typeof c.release !== 'function') { c.release = c.end; } callback(null, c); } else { callback(null, connection); } } } // getConnection starts here if (connectionPool.pooling) { // get a connection from the felix pool udebug.log('getConnection using connection pooling: true'); connectionPool.pool.getConnection(getConnectionOnConnection); } else if (connectionPool.is_connected || connectionPool.is_connecting) { // create a new connection udebug.log('getConnection using connection pooling: false'); connection = mysql.createConnection(connectionPool.driverproperties); connection.connect(getConnectionOnConnection); } else { // error callback(new Error('getConnection called before connect.')); } }; /** Release a connection (synchronous). If pooling via felix, return the connection to the pool. * If not, end the connection. No errors are reported to the user. */ exports.DBConnectionPool.prototype.releaseConnection = function(connection) { var connectionPool = this; if (connectionPool.pooling) { udebug.log('releaseConnection using connection pooling: true'); connection.release(); } else { udebug.log('releaseConnection using connection pooling: false'); connection.end(); } }; /** Connect to the database. Verify connection properties. * @param user_callback (err, connectionPool) */ exports.DBConnectionPool.prototype.connect = function(callback) { var connectionPool = this; var error; function connectOnConnection(err, connection) { connectionPool.is_connecting = false; if (err) { stats.connections.failed++; // create a new Error with a message and this stack error = new Error('Connection failed.'); // add cause to the error error.cause = err; // add sqlstate to error error.sqlstate = '08000'; callback(error); } else { stats.connections.successful++; connectionPool.is_connected = true; connectionPool.releaseConnection(connection); callback(null, connectionPool); } } // connect begins here if (connectionPool.is_connected) { udebug.log('MySQLConnectionPool.connect is already connected'); callback(null, connectionPool); } else { connectionPool.is_connecting = true; if (connectionPool.pooling) { connectionPool.pool = mysql.createPool(connectionPool.driverproperties); } // verify that the connection properties work by getting a connection connectionPool.getConnection(connectOnConnection); } }; exports.DBConnectionPool.prototype.close = function(user_callback) { var connectionPool = this; udebug.log('close'); var i; for (i = 0; i < this.openConnections.length; ++i) { var openConnection = this.openConnections[i]; udebug.log('close ending open connection', i); if (openConnection && openConnection._connectCalled) { connectionPool.releaseConnection(openConnection); } } this.openConnections = []; this.is_connected = false; user_callback(); }; exports.DBConnectionPool.prototype.destroy = function() { }; exports.DBConnectionPool.prototype.isConnected = function() { return this.is_connected; }; var countOpenConnections = function(connectionPool) { var i, count = 0; for (i = 0; i < connectionPool.openConnections.length; ++i) { if (connectionPool.openConnections[i] !== null) { count++; } } return count; }; exports.DBConnectionPool.prototype.getDBSession = function(index, callback) { var connectionPool = this; var newDBSession = null; var charset = connectionPool.driverproperties.charset; var charsetQuery = 'SET character_set_client=\'' + charset + '\';SET character_set_connection=\'' + charset + '\';SET character_set_results=\'' + charset + '\';'; var sqlModeQuery = ''; // set SQL_MODE if specified in driverproperties if (typeof connectionPool.driverproperties.sql_mode !== 'undefined') { sqlModeQuery = 'SET SQL_MODE = \'' + connectionPool.driverproperties.sql_mode + '\';'; } udebug.log(sqlModeQuery); function charsetComplete(err) { callback(err, newDBSession); } var connected_callback = function(err, pooledConnection) { if (err) { callback(err); return; } newDBSession = new mysqlConnection.DBSession(pooledConnection, connectionPool, index); connectionPool.openConnections[index] = pooledConnection; udebug.log_detail('MySQLConnectionPool.getDBSession created a new pooledConnection for index ' + index + ' ; ', ' openConnections: ', countOpenConnections(connectionPool)); // set character set server variables pooledConnection.query(charsetQuery + sqlModeQuery, charsetComplete); }; // create a new connection connectionPool.getConnection(connected_callback); }; /** Close the connection being used by the dbSession. * @param dbSession contains index the index into the openConnections array * pooledConnection the connection being used * @param callback when the connection is closed call the user */ exports.DBConnectionPool.prototype.closeConnection = function(dbSession, callback) { var connectionPool = this; if (dbSession.pooledConnection) { connectionPool.releaseConnection(dbSession.pooledConnection); dbSession.pooledConnection = null; } connectionPool.openConnections[dbSession.index] = null; if (typeof(callback) === 'function') { callback(null); } }; exports.DBConnectionPool.prototype.getTableMetadata = function(databaseName, tableName, dbSession, user_callback) { var connectionPool = this; var connection, dictionary; stats.get_table_metadata++; function getTableMetadataOnMetadata(err, metadata) { if (!dbSession) { connectionPool.releaseConnection(connection); } user_callback(err, metadata); } function getTableMetadataOnConnection(err, c) { if (err) { user_callback(err); } else { connection = c; dictionary = new mysqlDictionary.DataDictionary(connection, connectionPool); udebug.log_detail('MySQLConnectionPool.getTableMetadata calling dictionary.getTableMetadata for', databaseName, tableName); dictionary.getTableMetadata(databaseName, tableName, getTableMetadataOnMetadata); } } // getTableMetadata starts here if (dbSession) { // dbSession exists; use the connection in the db session getTableMetadataOnConnection(null, dbSession.pooledConnection); } else { // dbSession does not exist; get a connection for the call connectionPool.getConnection(getTableMetadataOnConnection); } }; exports.DBConnectionPool.prototype.listTables = function(databaseName, dbSession, user_callback) { var connectionPool = this; var connection, dictionary; stats.list_tables++; function listTablesOnTableList(err, list) { if (!dbSession) { // return the connection we got just for this call connectionPool.releaseConnection(connection); } // return the list to the user user_callback(err, list); } function listTablesOnConnection(err, c) { if (err) { user_callback(err); } else { connection = c; dictionary = new mysqlDictionary.DataDictionary(connection); dictionary.listTables(databaseName, listTablesOnTableList); } } // listTables starts here if (dbSession) { listTablesOnConnection(null, dbSession.pooledConnection); } else { // dbSession does not exist; get a connection for the call connectionPool.getConnection(listTablesOnConnection); } }; function defaultFieldMeta(fieldMapping) { if (fieldMapping.fieldName == 'id') { return 'id INT PRIMARY KEY'; } return fieldMapping.fieldName + ' VARCHAR(32) '; } function pn(nullable) {return nullable? '': ' NOT NULL ';} function pu(unsigned) {return unsigned? ' UNSIGNED' : '';} var translateMeta = {}; translateMeta.binary = function(length, nullable) {return 'BINARY(' + length + ')' + pn(nullable);}; translateMeta.char = function(length, nullable) {return 'CHAR(' + length + ')' + pn(nullable);}; translateMeta.date = function(nullable) {return 'DATE' + pn(nullable);}; translateMeta.datetime = function(fsp, nullable) {return 'DATETIME(' + fsp + ')' + pn(nullable);}; translateMeta.decimal = function(precision, scale, nullable) {return 'DECIMAL(' + precision + ', ' + scale + ')' + pn(nullable);}; translateMeta.double = function(nullable) {return 'DOUBLE' + pn(nullable);}; translateMeta.float = function(nullable) {return 'FLOAT' + pn(nullable);}; translateMeta.integer = function(bits, unsigned, nullable) { var u = pu(unsigned); var n = pn(nullable); if (bits < 8) return 'BIT' + u + n; if (bits == 8) return 'TINYINT' + u + n; if (bits <= 16) return 'SMALLINT' + u + n; if (bits <= 24) return 'MEDIUMINT' + u + n; if (bits <= 32) return 'INT' + u + n; return 'BIGINT' + u + n; }; translateMeta.interval = function(fsp, nullable) {return 'TIME' + pn(nullable);}; translateMeta.time = function(fsp, nullable) {return 'TIME' + pn(nullable);}; translateMeta.timestamp = function(fsp, nullable) {return 'TIMESTAMP' + pn(nullable);}; translateMeta.varbinary = function(length, lob, nullable) { if (lob) { return 'BLOB(' + length + ')' + pn(nullable); } return 'VARBINARY(' + length + ')' + pn(nullable); }; translateMeta.varchar = function(length, lob, nullable) { if (lob) { return 'TEXT(' + length + ')' + pn(nullable); } return 'VARCHAR(' + length + ')' + pn(nullable); }; translateMeta.year = function(nullable) {return 'YEAR' + pn(nullable);}; function sqlForTableCreation(tableMapping, defaultDatabaseName, engine) { udebug.log('sqlForTableCreation tableMapping', tableMapping, engine); var i, field, delimiter = ''; var tableMeta; var sql = 'CREATE TABLE '; var columnMeta; sql += tableMapping.database || defaultDatabaseName; sql += '.'; sql += tableMapping.table; sql += '('; for (i = 0; i < tableMapping.fields.length; ++i) { sql += delimiter; delimiter = ', '; field = tableMapping.fields[i]; sql += field.columnName; sql += ' '; meta = field.meta; if (meta) { columnMeta = meta.doit(translateMeta); sql += columnMeta; sql += meta.isPrimaryKey? ' PRIMARY KEY ' : ''; sql += meta.isUniqueKey? ' UNIQUE KEY ': ''; udebug.log('sqlForTableCreation field:', field.fieldName, 'column:', field.columnName, 'meta:', meta, 'columnMeta:', columnMeta); } else { sql += defaultFieldMeta(field); } } // process meta for the table // need to support PRIMARY and HASH for (i = 0; i < tableMapping.meta.length; ++i) { tableMeta = tableMapping.meta[i]; if (tableMeta.index) { sql += delimiter; // index name calculation sql += tableMeta.unique?' UNIQUE': ''; sql += ' INDEX ' + tableMeta.columns + ' ( ' + tableMeta.columns + ') '; } } sql += ') ENGINE=' + engine; udebug.log('sqlForTableMapping sql: ', sql); return sql; } /** Create the table in the database for the mapping. * @param tableMapping the mapping for this table * @param session the session to use for database operations * @param sessionFactory the session factory to use for database operations * @param user_callback the user callback(err) * @return err if any errors */ exports.DBConnectionPool.prototype.createTable = function(tableMapping, session, sessionFactory, user_callback) { var connectionPool = this; var createTableSQL, connection, dictionary, databaseName, tableName, qualifiedTableName, tableHandler; tableName = tableMapping.table; databaseName = tableMapping.database || connectionPool.driverproperties.database; qualifiedTableName = databaseName + '.' + tableName; function createTableOnTableMetadata(err, tableMetadata) { udebug.log('createTableOnTableMetadata with err:', err, '\n', util.inspect(tableMetadata)); // remember the table metadata in the session factory sessionFactory.tableMetadatas[qualifiedTableName] = tableMapping; // create the table handler tableHandler = new DBTableHandler(tableMetadata, tableMapping, null); // remember the table handler in the session factory sessionFactory.tableHandlers[qualifiedTableName] = tableHandler; if (!session || !session.dbSession) { // return the connection we got just for this call connectionPool.releaseConnection(connection); } user_callback(err); } function createTableOnTableCreation(err) { if (err) { user_callback(err); } else { // create the table metadata and table handler for the new table dictionary = new mysqlDictionary.DataDictionary(connection, connectionPool); dictionary.getTableMetadata(databaseName, tableName, createTableOnTableMetadata); } } function createTableOnConnection(err, c) { if (err) { user_callback(err); } else { connection = c; createTableSQL = sqlForTableCreation(tableMapping, databaseName, connectionPool.props.engine); connection.query(createTableSQL, createTableOnTableCreation); } } // createTable starts here udebug.log('createTable for tableMapping:', util.inspect(tableMapping)); if (session && session.dbSession) { // dbSession exists; use the connection in the db session createTableOnConnection(null, session.dbSession.pooledConnection); } else { // dbSession does not exist; get a connection for the call connectionPool.getConnection(createTableOnConnection); } };