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