1 var comb = require("comb");
  2 var moose, adapter;
  3 var convert = function(op) {
  4 	return function() {
  5 		var columns = this.columns;
  6 		var args = Array.prototype.slice.call(arguments);
  7 		if (args.length > 1) {
  8 			var columnName = args[0], val = args[1];
  9 			this.validate(columnName, val);
 10 			return columns[columnName][op](val);
 11 		} else {
 12 			var object = args[0];
 13 			if (op == "toSql") this.validate(object);
 14 			var ret = {};
 15 			for (var i in object) {
 16 				if (i in columns) {
 17 					ret[i] = columns[i][op](object[i]);
 18 				} else {
 19 					ret[i] = object[i];
 20 				}
 21 			}
 22 			return ret;
 23 
 24 		}
 25 	};
 26 };
 27 
 28 
 29 /**
 30  * @class Represents a table in a Database. This class is used to update, create, alter... tables.
 31  * The Class is typically used in migrations, or wrapped by a {@link moose.Model}.
 32  *
 33  * @example
 34  *
 35  * //To load preexisting Table use moose.loadSchema
 36  *
 37  * moose.loadSchema("employee").then(function(employee){});
 38  *
 39  * //Table creation example
 40  *
 41  * var types = moose.adapters.mysql.types;
 42  *
 43  * var employee = new moose.Table("employee", {
 44  *    id : types.INT({allowNull : false, autoIncrement : true}),
 45  *    firstname : types.VARCHAR({length : 20, allowNull : false}),
 46  *    lastname : types.VARCHAR({length : 20, allowNull : false}),
 47  *    midinitial : types.CHAR({length : 1}),
 48  *    gender : types.ENUM({enums : ["M", "F"], allowNull : false}),
 49  *    street : types.VARCHAR({length : 50, allowNull : false}),
 50  *    city : types.VARCHAR({length : 20, allowNull : false}),
 51  *    primaryKey : "id"
 52  * });
 53  *
 54  * //or use through migration
 55  *
 56  *moose.createTable("employee", function(table) {
 57  *    table.column("id", types.INT({allowNull : false, autoIncrement : true}));
 58  *    table.column("firstname", types.VARCHAR({length : 20, allowNull : false}));
 59  *    table.column("lastname", types.VARCHAR({length : 20, allowNull : false}));
 60  *    table.column("midinitial", types.CHAR({length : 1}));
 61  *    table.column("gender", types.ENUM({enums : ["M", "F"], allowNull : false}));
 62  *    table.column("street", types.VARCHAR({length : 50, allowNull : false}));
 63  *    table.column("city", types.VARCHAR({length : 20, allowNull : false}));
 64  *    table.primaryKey("id");
 65  *});
 66  *
 67  * //alter table examples
 68  *
 69  *  employee.rename("employeeTwo");
 70  *  employee.addColumn("age", types.INT());
 71  *  employee.addUnique(["firstname", "midinitial"]);
 72  *
 73  * //use through migration
 74  *
 75  * moose.alterTable("company", function(table) {
 76  *      table.rename("companyNew");
 77  *      table.addUnique("companyName");
 78  *      table.addColumn("employeeCount", types.INT());
 79  *      table.addUnique(["companyName", "employeeCount"]);
 80  *});
 81  *
 82  * //to drop a table use moose.dropTable
 83  *
 84  * moose.dropTable("company");
 85  * moose.dropTable("employee");
 86  *
 87  *
 88  * @param {String} tableName the name of the table
 89  * @param {Object} properties properties to describe the table, all properties excpet for type, and primaryKey will be interpreted as columns
 90  *          each property should be a type described by the particular adapter (i.e {@link moose.adapters.mysql.types}).
 91  * @param {String} [properties.type="mysql"] the type of database the table resides in.
 92  * @param {String|Array} properties.primaryKey the primary key of the table.
 93  *
 94  * @property {Object} columns the columns contained in this table.
 95  * @property {String} createTableSql valid create table SQL for this table.
 96  * @property {String} alterTableSql valid alter table SQL for this table.
 97  * @property {String} dropTableSql valid drop table SQL for this table.
 98  * @property {String} database the schema of the table resides in.
 99  *
100  *
101  * @name  Table
102  * @memberOf moose
103 
104  */
105 exports.Table = (comb.define(null, {
106 			instance : {
107 
108 				/**@lends moose.Table.prototype*/
109 
110 				tableName : null,
111 
112 				type : null,
113 
114 				foreignKeys : null,
115 
116 
117 				constructor : function(tableName, properties) {
118 					if (!moose) {
119 						moose = require("./index"),adapter = moose.adapter;
120 					}
121 					if (!tableName) throw new Error("Table name required for schema");
122 					properties = properties || {};
123 					this.foreignKeys = [];
124 					this.uniqueSql = [];
125 					this.pk = null;
126 					this.__alteredColumns = {};
127 					this.__addColumns = {};
128 					this.__dropColumns = [];
129 					this.__addColumns = {};
130 					this.__newName = null;
131 					// pull it out then define our columns then check the primary key
132 					var db = properties.database;
133 					delete properties.database;
134 					if (db) {
135 						this.database = db;
136 					}
137 					var pk = properties.primaryKey;
138 					delete properties.primaryKey;
139 					this.primaryKeySql = null;
140 					this.tableName = tableName;
141 					this.__columns = columns = {};
142 					for (var i in properties) {
143 						this.column(i, properties[i]);
144 					}
145 					if (pk) {
146 						this.primaryKey(pk);
147 					}
148 				},
149 
150 				/**
151 				 * Set the engine that the table should leverage.
152 				 * <br/>
153 				 * <b>Not all databases support this property</b>
154 				 *
155 				 * @param {String} engine the name of the engine.
156 				 */
157 				engine : function(engine) {
158 					this.__engine = engine;
159 				},
160 
161 
162 				/**
163 				 * Add a column to this table.
164 				 *
165 				 * <p><b>This is only valid on new tables.</b></p>
166 				 *
167 				 * @param {String} name the name of the column to be created.
168 				 * @param {moose.adapters.Type} options a type specific to the adpater that this table uses
169 				 */
170 				column : function(name, options) {
171 					if (adapter.isValidType(options)) {
172 						this.__columns[name] = options;
173 					} else {
174 						throw new Error("When adding a column the type must be a type object");
175 					}
176 				},
177 
178 				/**
179 				 * Adds a foreign key to this table, see {@link moose.adapters.mysql.foreignKey}
180 				 *
181 				 * <p><b>This is only valid on new tables.</b></p>
182 				 *
183 				 *
184 				 */
185 				foreignKey : function(name, options) {
186 					this.foreignKeys.push(adapter.foreignKey(name, options));
187 				},
188 
189 				/**
190 				 * Adds a primary key to this table, see {@link moose.adapters.mysql.primaryKey}
191 				 *
192 				 * <p><b>This is only valid on new tables.</b></p>
193 				 *
194 				 * @param {Array | String} name the name or names to assign to a primary key.
195 				 *
196 				 */
197 				primaryKey : function(name) {
198 					var isValid = false;
199 					if (name instanceof Array && name.length)
200 						if (name.length == 1) {
201 							return this.primaryKey(name[0]);
202 						} else {
203 							isValid = name.every(function(n) {
204 								if (this.isInTable(n)) {
205 									this.columns[n].primaryKey = true;
206 									return true;
207 								} else {
208 									return false;
209 								}
210 							}, this);
211 							this.pk = name;
212 						}
213 					else {
214 						isValid = this.isInTable(name);
215 						isValid && (this.columns[name].primaryKey = true);
216 						this.pk = name;
217 					}
218 					if (isValid) {
219 						this.primaryKeySql = adapter.primaryKey(name);
220 					} else {
221 						throw new Error("Primary key is not in the table");
222 					}
223 				},
224 
225 				/**
226 				 * Adds a unique constraint to this table, see {@link moose.adapters.mysql.unique}
227 				 *
228 				 * <p><b>This is only valid on new tables.</b></p>
229 				 *
230 				 *
231 				 */
232 				unique : function(name) {
233 					var isValid = false;
234 					if (name instanceof Array && name.length)
235 						if (name.length == 1) {
236 							return this.unique(name[0]);
237 						} else {
238 							isValid = name.every(function(n) {
239 								if (this.isInTable(n)) {
240 									this.columns[n].unique = true;
241 									return true;
242 								} else {
243 									return false;
244 								}
245 							}, this);
246 						}
247 					else {
248 						isValid = this.isInTable(name);
249 						isValid && (this.columns[name].unique = true);
250 					}
251 					if (isValid) {
252 						this.uniqueSql.push(adapter.unique(name));
253 					} else {
254 						throw new Error("Unique key is not in the table");
255 					}
256 				},
257 
258 				/**
259 				 * Takes a columnName and determines if it is in the table.
260 				 *
261 				 * @param {String} columnName the name of the column.
262 				 *
263 				 * @return {Boolean} true if the columnd is in the table.
264 				 */
265 				isInTable : function(columnName) {
266 					return (columnName in this.__columns);
267 				},
268 
269 				/**
270 				 * Validate an object or columns and value against the columns in this table.
271 				 *
272 				 * @param {String|Object} name If the name is a string it is assumed to be the name of the column.
273 				 *                              If the name is an object it is assumed to be a an object consiting of {columnName : value}.
274 				 * @param {*} value if a string is the first argument to validate is a string then the value is compared against the type of the column contained in this table.
275 				 *
276 				 * @return {Boolean} true if the column/s are valid.
277 				 */
278 				validate : function() {
279 					var args = Array.prototype.slice.call(arguments);
280 					var columns = this.columns;
281 					self = this;
282 					function validateValue(columnName, value) {
283 						if (!columnName) throw new Error("columnName required");
284 						if (value == "undefined") value = null;
285 						if (self.isInTable(columnName)) {
286 							return columns[columnName].check(value);
287 						} else {
288 							throw new Error(columnName + " is not in table");
289 						}
290 					}
291 
292 					if (args.length > 1) {
293 						return validateValue(args[0], args[1]);
294 					} else {
295 						var object = args[0];
296 						if (!comb.isObject(object)) throw new Error("object is required");
297 						for (var i in object) {
298 							validateValue(i, object[i]);
299 						}
300 						return true;
301 					}
302 				},
303 
304 				/**
305 				 * Adds a column to this table.
306 				 *
307 				 * <p><b>This is only valid on previously saved tables.</b></p>
308 				 *
309 				 * @param {String} name the name of the column to add.
310 				 * @param {moose.adapters.mysql.Type} options the type information of the column.
311 				 */
312 				addColumn : function(name, options) {
313 					if (adapter.isValidType(options)) {
314 						this.__addColumns[name] = options;
315 					} else {
316 						throw new Error("When adding a column the type must be a Type object");
317 					}
318 				},
319 
320 				/**
321 				 * Drops a column from this table.
322 				 *
323 				 * <p><b>This is only valid on previously saved tables.</b></p>
324 				 *
325 				 * @param {String} name the name of the column to drop.
326 				 */
327 				dropColumn : function(name) {
328 					if (this.isInTable(name)) {
329 						this.__dropColumns.push(name);
330 					} else {
331 						throw new Error(name + " is not in table " + this.tableName);
332 					}
333 				},
334 
335 				/**
336 				 * Renames a column contained in this table.
337 				 *
338 				 * <p><b>This is only valid on previously saved tables.</b></p>
339 				 *
340 				 * @param {String} name the name of the column to rename.
341 				 * @param {String} newName the new name of the column.
342 				 */
343 				renameColumn : function(name, newName) {
344 					if (this.isInTable(name)) {
345 						var column = this.__alteredColumns[name];
346 						if (!column) {
347 							column = this.__alteredColumns[name] = {original : this.__columns[name]};
348 						}
349 						column.newName = newName;
350 					} else {
351 						throw new Error(name + " is not in table " + this.tableName);
352 					}
353 				},
354 
355 				/**
356 				 * Set the default value of a column.
357 				 *
358 				 * <p><b>This is only valid on previously saved tables.</b></p>
359 				 *
360 				 * @param {String} name the name of the column to alter.
361 				 * @param {*} defaultvalue the value to set as the default of the column.
362 				 */
363 				setColumnDefault : function(name, defaultvalue) {
364 					if (this.isInTable(name)) {
365 						var column = this.__alteredColumns[name];
366 						if (!column) {
367 							column = this.__alteredColumns[name] = {original : this.__columns[name]};
368 						}
369 						column["default"] = defaultvalue;
370 					} else {
371 						throw new Error(name + " is not in table " + this.tableName);
372 					}
373 				},
374 
375 				/**
376 				 * Set a new type on a column contained in this table.
377 				 *
378 				 * <p><b>This is only valid on previously saved tables.</b></p>
379 				 *
380 				 * @param {String} name the name of the column to alter.
381 				 * @param {moose.adapters.mysql.Type} options the new type information of the column.
382 				 */
383 				setColumnType : function(name, type) {
384 					if (adapter.isValidType(type) && this.isInTable(name)) {
385 						var column = this.__alteredColumns[name];
386 						if (!column) {
387 							column = this.__alteredColumns[name] = {original : this.__columns[name]};
388 						}
389 						column.type = type;
390 					} else {
391 						throw new Error(name + " is not in table " + this.tableName);
392 					}
393 				},
394 
395 				/**
396 				 * Set if a column should allow null.
397 				 *
398 				 * <p><b>This is only valid on previously saved tables.</b></p>
399 				 *
400 				 * @param {String} name the name of the column to alter.
401 				 * @param {Boolean} allowNull true if null is allowed, false otherwise.
402 				 */
403 				setAllowNull : function(name, allowNull) {
404 					if (this.isInTable(name)) {
405 						var column = this.__alteredColumns[name];
406 						if (!column) {
407 							column = this.__alteredColumns[name] = {original : this.__columns[name]};
408 						}
409 						column.allowNull = allowNull;
410 					} else {
411 						throw new Error(name + " is not in table " + this.tableName);
412 					}
413 				},
414 
415 				/**
416 				 * Replace the current primary key, see {@link moose.adapters.mysql.addPrimaryKey}.
417 				 * <p><b>This is only valid on previously saved tables.</b></p>
418 				 */
419 				addPrimaryKey : function() {
420 					if (this.pk) {
421 						this.dropPrimaryKey(this.pk);
422 					}
423 					this.addPrimaryKeySql = adapter.addPrimaryKey.apply(adapter, arguments);
424 				},
425 
426 				/**
427 				 * Drop current primary key, see {@link moose.adapters.mysql.dropPrimaryKey}.
428 				 * <p><b>This is only valid on previously saved tables.</b></p>
429 				 */
430 				dropPrimaryKey : function() {
431 					this.pk = null;
432 					this.dropPrimaryKeySql = adapter.dropPrimaryKey.apply(adapter, arguments);
433 				},
434 
435 				/**
436 				 * Add a foreign key to this table, see {@link moose.adapters.mysql.addForeignKey}.
437 				 * <p><b>This is only valid on previously saved tables.</b></p>
438 				 */
439 
440 				addForeignKey : function() {
441 					this.foreignKeys.push(adapter.addForeignKey.apply(adapter, arguments));
442 				},
443 
444 				/**
445 				 * Drop a foreign key on this table, see {@link moose.adapters.mysql.dropForeignKey}.
446 				 * <p><b>This is only valid on previously saved tables.</b></p>
447 				 */
448 				dropForeignKey : function() {
449 					this.foreignKeys.push(adapter.dropForeignKey.apply(adapter, arguments));
450 				},
451 
452 				/**
453 				 * Add a unique constraint to this table, see {@link moose.adapters.mysql.addUnique}.
454 				 * <p><b>This is only valid on previously saved tables.</b></p>
455 				 */
456 
457 				addUnique : function() {
458 					this.uniqueSql.push(adapter.addUnique.apply(adapter, arguments));
459 				},
460 
461 				/**
462 				 * Drop a unique constraint on this table, see {@link moose.adapters.mysql.dropUnique}.
463 				 * <p><b>This is only valid on previously saved tables.</b></p>
464 				 */
465 				dropUnique : function() {
466 					this.uniqueSql.push(adapter.dropUnique.apply(adapter, arguments));
467 				},
468 
469 				/**
470 				 * Rename this table.
471 				 * <p><b>This is only valid on previously saved tables.</b></p>
472 				 * @param {String} newName the new table name.
473 				 */
474 				rename : function(newName) {
475 					this.__newName = newName;
476 				},
477 
478 				/**
479 				 *
480 				 * @function
481 				 * Convert an object or column and value to a valid sql value.
482 				 *
483 				 * @param {String|Object} name If the name is a string it is assumed to be the name of the column.
484 				 *                              If the name is an object it is assumed to be a an object consisting of {columnName : value}.
485 				 * @param {*} value if a string is the first argument, then the value is compared against the type of the column contained in this table.
486 				 *
487 				 * @return {String|Object} the sql value/s.
488 				 */
489 				toSql : convert("toSql"),
490 
491 				/**
492 				 * Convert an object or column and value from a sql value.
493 				 *
494 				 * @function
495 				 * @param {String|Object} name If the name is a string it is assumed to be the name of the column.
496 				 *                              If the name is an object it is assumed to be a an object consisting of {columnName : value}.
497 				 * @param {*} value if a string is the first argument, then the value is compared against the type of the column contained in this table.
498 				 *
499 				 * @return {String|Object} the javascript value/s.
500 				 */
501 				fromSql : convert("fromSql"),
502 
503 				setters : {
504 					database : function(database) {
505 						if (comb.isString(database)) {
506 							this.__database = database;
507 						} else {
508 							throw "moose.Table : when setting a database it must be a string.";
509 						}
510 					}
511 				},
512 
513 				getters : {
514 
515 					database : function() {
516 						return this.__database;
517 					},
518 
519 					columns : function() {
520 						return this.__columns;
521 					},
522 
523 					createTableSql : function() {
524 						var sql = "CREATE TABLE " + this.tableName + "(";
525 						var columns = this.columns;
526 						var columnSql = [];
527 						for (var i in columns) {
528 							columnSql.push(adapter.column(i, columns[i]));
529 						}
530 						sql += columnSql.join(",");
531 						if (this.primaryKeySql) sql += ", " + this.primaryKeySql;
532 						if (this.foreignKeys.length) sql += ", " + this.foreignKeys.join(",");
533 						if (this.uniqueSql.length) sql += ", " + this.uniqueSql.join(","),needComma = true;
534 						if (this.__engine) {
535 							sql += ") ENGINE=" + this.__engine + ";";
536 						} else {
537 							sql += ");";
538 						}
539 						return sql;
540 					},
541 
542 					alterTableSql : function() {
543 						var sql = "ALTER TABLE " + this.tableName, needComma = false;
544 						if (this.__newName) {
545 							sql += " RENAME " + this.__newName;
546 							needComma = true;
547 						}
548 						var addColumns = this.__addColumns, addColumnsSql = [];
549 						var dropColumns = this.__dropColumns, dropColumnsSql = [];
550 						var alteredColumns = this.__alteredColumns, alterColumnsSql = [];
551 						for (var i in addColumns) {
552 							addColumnsSql.push(adapter.addColumn(i, addColumns[i]));
553 						}
554 						if (addColumnsSql.length) {
555 							sql += (needComma ? " , " : " ") + addColumnsSql.join(" ,");
556 							needComma = true;
557 						}
558 						if (dropColumns.length) {
559 							for (i in dropColumns) {
560 								dropColumnsSql.push(adapter.dropColumn(dropColumns[i]));
561 							}
562 							sql += (needComma ? " , " : " ") + dropColumnsSql.join(" ,"),needComma = true;
563 						}
564 						for (i in alteredColumns) {
565 							alterColumnsSql.push(adapter.alterColumn(i, alteredColumns[i]));
566 						}
567 						if (alterColumnsSql.length) {
568 							sql += (needComma ? " , " : " ") + alterColumnsSql.join(",");
569 							needComma = true;
570 						}
571 
572 						if (this.dropPrimaryKeySql) {
573 							sql += (needComma ? " , " : " ") + this.dropPrimaryKeySql;
574 							needComma = true;
575 						}
576 						if (this.addPrimaryKeySql) {
577 							sql += (needComma ? " , " : " ") + this.addPrimaryKeySql;
578 							needComma = true;
579 						}
580 						if (this.foreignKeys.length) {
581 							sql += (needComma ? " , " : " ") + this.foreignKeys.join(",");
582 							needComma = true;
583 						}
584 						if (this.uniqueSql.length) {
585 							sql += (needComma ? " , " : " ") + this.uniqueSql.join(",");
586 							needComma = true;
587 						}
588 						sql += ";";
589 						return sql;
590 					},
591 
592 					dropTableSql : function() {
593 						return "DROP TABLE IF EXISTS " + this.tableName;
594 					}
595 				}
596 
597 			}
598 		}));
599 
600 
601