      1 var mysql = require("mysql"),
      2 		comb = require("comb"),
      3 		hitch = comb.hitch,
      4 		Promise = comb.Promise,
      5 		PromiseList = comb.PromiseList,
      6 		ConnectionPool = require("./ConnectionPool");
      7 //MySQLPool = require("mysql-pool").MySQLPool;
      8
      9
     10 var closedCount = 0;
     11 function throwCmdError(cmd, action) {
     12 	cmd.on('error', function(err) {
     13 		throw new Error('Failed to ' + action +
     14 				(err && err.message ? ': ' + err.message : ''));
     15 	});
     16 }
     17
     18 function throwError(action, err) {
     19 	throw new Error('Failed to ' + action +
     20 			(err && err.message ? ': ' + err.message : ''));
     21 }
     22
     23 var LOGGER = comb.logging.Logger.getLogger("comb.adapters.mysql.client");
     24
     25 /**
     26  * @class MySQL Client wrapper to standardize queries.
     27  *
     28  * <b>NOTE this class is not publicly exposed, but returned from calling getConnection on {@link moose.getConnection}</b>
     29  *
     30  * @name Query
     31  * @memberOf moose.adapters.client.mysql
     32  *
     33  * @param {moose.adapters.mysql.ConnectionPool} pool the pool to retrieve connections from.
     34  * @param {Boolean} forceClose=false whether or not to force close the connection
     35  * @param {String} [database] the database to connect to when querying.
     36  *
     37  * @property {String} database the name of the database the query is currently using.
     38  */
     39 var Query = comb.define(null, {
     40 			instance : {
     41 				/**@lends moose.adapters.client.mysql.Query.prototype*/
     42
     43 				constructor : function(pool, forceClose, database) {
     44 					if (!pool) throw "Query : Pool required to query";
     45 					if(database){
     46 						LOGGER.debug("QUERY DB = " + database)
     47 						this.database = database;
     48 					}
     49 					this.pool = pool;
     50 					this.forceClose = forceClose;
     51 					this._ended = false;
     52 				},
     53
     54 				__query : function(query) {
     55 					var ret = new Promise();
     56 					this.connection.query(query, hitch(this, function(err, results, info) {
     57 						var conn = this.connection;
     58 						this.connection = null;
     59 						if (this.forceClose) {
     60 							conn.end(hitch(this, function() {
     61 								if (err) {
     62 									ret.errback(err);
     63 								} else {
     64 									ret.callback(results, info);
     65 								}
     66 								this.pool.removeConnection(conn);
     67 							}));
     68 						} else {
     69 							this.pool.returnConnection(conn);
     70 							if (err) {
     71 								ret.errback(err);
     72 							} else {
     73 								ret.callback(results, info);
     74 							}
     75 						}
     76 					}));
     77 					return ret;
     78 				},
     79
     80 				/**
     81 				 * Queries the database.
     82 				 *
     83 				 * @param {String} query query to perform
     84 				 *
     85 				 * @return {comb.Promise} promise that is called back with the results, or error backs with an error.
     86 				 */
     87 				query : function(query) {
     88 					if (!this._ended) {
     89 						var ret = new Promise();
     90 						if (!this.connection) {
     91 							this.pool.getConnection().then(hitch(this, function(conn) {
     92 								this.connection = conn;
     93 								this.query(query).then(hitch(ret, "callback"), hitch(ret, "errback"));
     94 							}));
     95 						} else {
     96 							var conn = this.connection;
     97 							if (this.__setDatabase && this.__database != conn.database) {
     98 								this.connection.useDatabase(this.__database, hitch(this, function(err) {
     99 									this.__setDatabase = false;
    100 									this.connection.database = this.__database;
    101 									if (err) {
    102 										ret.errback(err);
    103 									} else {
    104 										this.__query(query).then(hitch(ret, "callback"), hitch(ret, "errback"));
    105 									}
    106 								}));
    107 							} else {
    108 								this.__query(query).then(hitch(ret, "callback"), hitch(ret, "errback"));
    109 							}
    110 						}
    111 					} else {
    112 						throw new Error("Query has already been closed");
    113 					}
    114 					return ret;
    115 				},
    116
    117 				/**@ignore*/
    118 				setters : {
    119 					database : function(database) {
    120 						if (comb.isString(database) && database != this.__database && !this._ended) {
    121 							this.__setDatabase = true;
    122 							this.__database = database;
    123 						}
    124 					}
    125 				},
    126
    127 				/**@ignore*/
    128 				getters : {
    129 					database : function(){
    130 						return this.__database;
    131 					}
    132 				}
    133 			}
    134 		});
    135
    136
    137 /**
    138  * @class Sub class of query to handle transaction based queries.
    139  *
    140  * <b>NOTE this class is not publicly exposed, but returned from calling getConnection on {@link moose.transaction}</b>
    141  *
    142  * @name TransactionQuery
    143  * @augemtns moose.adapters.client.mysql.Query
    144  * @memberOf moose.adapters.client.mysql
    145  *
    146  * @param {moose.adapters.mysql.ConnectionPool} pool the pool to retrieve connections from.
    147  * @param {Boolean} forceClose=false whether or not to force close the connection
    148  * @param {String} [database] the database to connect to when querying.
    149  */
    150 var TransactionQuery = comb.define(Query, {
    151 			instance : {
    152 				 /**@lends moose.adapters.client.mysql.TransactionQuery.prototype*/
    153
    154 				__query : function(query) {
    155 					var ret = new Promise();
    156 					this.connection.query(query, hitch(this, function(err, results, info) {
    157 						if (err) {
    158 							this.connection.clean = false;
    159 							ret.errback(err);
    160 						} else {
    161 							ret.callback(results, info);
    162 						}
    163 					}));
    164 					return ret;
    165 				},
    166
    167 				query : function(query, args) {
    168 					if (this.connection && !this.connection.clean) {
    169 						throw new Error("Cannot commit a transaction with an error");
    170 						return;
    171 					}
    172 					return this.super(arguments);
    173 				},
    174
    175 				/**
    176 				 * Call to commit a transaction.
    177 				 *
    178 				 * @return {comb.Promise} called back after the commit has finished.
    179 				 */
    180 				commit: function() {
    181 					var ret = new Promise();
    182 					this.query("COMMIT").then(hitch(this, function(res) {
    183 						this.connection.end();
    184 						this.pool.removeConnection(this.connection);
    185 						this.connection = null;
    186 						ret.callback(res);
    187 					}), hitch(ret, "errback"));
    188 					return ret;
    189 				},
    190
    191 				abort: function() {
    192 					var ret = new Promise();
    193 					this.query("ROLLBACK").then(hitch(this, function() {
    194 						this.connection.end();
    195 						this.pool.removeConnection(this.connection);
    196 						this.connection = null;
    197 						ret.callback();
    198 					}), hitch(ret, "errback"));
    199 					return ret;
    200 				},
    201
    202 				/**
    203 				 * Call to rollback a transaction.
    204 				 *
    205 				 * @return {comb.Promise} called back after the rollback has finished.
    206 				 */
    207 				rollback : function() {
    208 					return this.abort();
    209 				}
    210 			}
    211 		});
    212
    213 /**
    214  * @class MySQL connection pool class. This class is not used directly.
    215  *
    216  *
    217  * @name ConnectionPool
    218  * @augments moose.adapters.client.ConnectionPool
    219  * @memberOf moose.adapters.client.mysql
    220  */
    221 var MysqlConnectionPool = (exports.ConnectionPool = comb.define(ConnectionPool, {
    222 			instance : {
    223
    224 				createConnection : function() {
    225 					var conn = new mysql.Client(this._options);
    226 					conn.clean = true;
    227 					conn.connect();
    228 					return conn;
    229 				},
    230
    231 				closeConnection : function(conn) {
    232 					var ret = new Promise();
    233 					conn.end(function() {
    234 						ret.callback();
    235 					});
    236
    237 					return ret;
    238 				},
    239
    240 				validate : function(conn) {
    241 					var ret = new Promise();
    242 					if (!conn.clean || conn.ending) {
    243 						ret.callback(false);
    244 					} else if (!conn.database || conn.database != this.database) {
    245 						//reset to the default database.
    246 						conn.useDatabase(this.database, function(err) {
    247 							if (err) {
    248 								ret.callback(false);
    249 							} else {
    250 								conn.database = this.database;
    251 								ret.callback(true);
    252 							}
    253 						});
    254 					} else {
    255 						ret.callback(true);
    256 					}
    257 					return ret;
    258 				}
    259 			}
    260 		}));
    261
    262 /**
    263  * @class Manages mysql connections. This class manages the creation of a connection pool,
    264  * and is the class that retrieves/creates connections to be used my moose.
    265  *
    266  * @name Client
    267  * @memberOf moose.adapters.client
    268  */
    269 exports.Client = comb.define(null, {
    270 			instance : {
    271 				/**@lends moose.adapters.client.Client.prototype*/
    272
    273 				minConnections : null,
    274
    275 				constructor : function(options) {
    276 					this.options = options || {};
    277 					this.database = options.database;
    278 					this.maxConnections = typeof options.maxConnections == "number" ? options.maxConnections : 10;
    279 					this.pool = new MysqlConnectionPool(options);
    280 				},
    281
    282 				/**
    283 				 * Executes a query with the given sql and database.
    284 				 * @param {String} sql the sql to execute
    285 				 * @param {String} [database] the name of the database to execute the query on.
    286 				 *
    287 				 * @return {comb.Promise} a promise that is called back with the results.
    288 				 */
    289 				query : function(sql, database) {
    290 					var q = new Query(this.pool, false, database || this.database);
    291 					return q.query(sql);
    292 				},
    293
    294 				/**
    295 				 * Retrieces a connection from the connection pool.
    296 				 *
    297 				 * @param {boolean} [forceClose=false] whether or not to close the connection after the query is done.
    298 				 *                                     this is typically not needed.
    299 				 * @param [String] [database] the name of the database to connection to.
    300 				 *
    301 				 * @return {Query} the query object to perform queries on.
    302 				 */
    303 				getConnection : function(forceClose, database) {
    304 					LOGGER.debug("GET CONNECTION DB = " + database + " " + this.database);
    305 					return new Query(this.pool, forceClose, (database || this.database));
    306 				},
    307
    308 				/**
    309 				 * Retrieves a transaction query to perform a transaction on.
    310 				 *
    311 				 * @param {String} database the name of the database to perform the transaction on.
    312 				 */
    313 				transaction: function(database) {
    314 					var conn = new TransactionQuery(this.pool, false, database || this.database);
    315 					conn.query('START TRANSACTION').addErrback(hitch(null, throwError, 'start transaction'));
    316 					return conn;
    317 				},
    318
    319 				/**
    320 				 * Closes all connections.
    321 				 *
    322 				 * @return {comb.Promise} called back once all queries are done, or calls errback if an error occurs.
    323 				 */
    324 				close : function() {
    325 					var ret = new Promise();
    326 					this.pool.endAll(hitch(this, function(err) {
    327 						this.pool = null;
    328 						if (err) {
    329 							LOGGER.error("Error closing connections");
    330 							ret.errback(err);
    331 						} else {
    332 							ret.callback();
    333 						}
    334 					}));
    335 					return ret;
    336 				},
    337
    338 				/**@ignore*/
    339 				setters : {
    340 					database : function(database) {
    341 						if (comb.isString(database)) {
    342 							this.__defaultDatabase = database;
    343 						} else {
    344 							throw "moose.adapters.mysql.Client : Database required";
    345 						}
    346 					}
    347 				},
    348
    349 				/**@ignore*/
    350 				getters : {
    351 					database : function() {
    352 						return this.__defaultDatabase;
    353 					}
    354 				}
    355 			}
    356 		});
    357