/**
 * Class to handle the Database select results. Also allows
 * updates, insertions and deletions on tables where the primary key is 
 * selected.
 *
 * Note that table and column names must be unique in the selection query,
 * otherwise updates are not allowed on that table/column. If two tables or
 * columns have the same name, an alias should be used to make it updatable.
 **/
Db.Model = new Class ().extend
({
	Status:
	{
		 CLEAN   : 1
		,LOADING : 2
		,READY   : 3
		,ERROR   : 4
	},
	Mode:
	{
		 ON_CHANGE: 1
		,ON_DEMAND: 2
	},
	Operation:
	{
		 INSERT: 1 << 1
		,UPDATE: 1 << 2
		,DELETE: 1 << 3
	}
	,SortWay:
	{
		ASC  : 1,
		DESC : 2 
	}
});

Db.Model.implement
({
	Extends: Vn.Object
	,Tag: 'db-model'
	,Properties:
	{
		/**
		 * The connection used to execute the statement.
		 **/
		conn:
		{
			type: Db.Conn
			,set: function (x)
			{
				this._conn = x;
				this._autoLoad ();
			}
			,get: function ()
			{
				return this._conn;
			}
		},
		/**
		 * The result index.
		 **/
		resultIndex:
		{
			type: Number
			,set: function (x)
			{
				this._resultIndex = x;
			}
			,get: function ()
			{
				return this._resultIndex;
			}
		},
		/**
		 * The batch used to execute the statement.
		 **/
		batch:
		{
			type: Sql.Batch
			,set: function (x)
			{
				this.link ({_batch: x}, {'changed': this._autoLoad});
				this._autoLoad ();
			}
			,get: function ()
			{
				return this._batch;
			}
		},
		/**
		 * The model select statement.
		 **/
		stmt:
		{
			type: Sql.Stmt
			,set: function (x)
			{
				this._stmt = x;
				this._autoLoad ();
			}
			,get: function ()
			{
				return this._stmt;
			}
		},
		/**
		 * The model query.
		 **/
		query:
		{
			type: String
			,set: function (x)
			{
				this._stmt = new Sql.String ({query: x});
			}
			,get: function ()
			{
				return this._stmt.render (null);
			}
		},
		/**
		 * The main table.
		 **/
		mainTable:
		{
			type: String
			,set: function (x)
			{
				this._mainTable = null;
				this.requestedMainTable = x;
				this._refreshMainTable ();
			}
			,get: function ()
			{
				return this._mainTable;
			}
		},
		/**
		 * Determines if the model is updatable.
		 **/
		updatable:
		{
			type: Boolean
			,set: function (x)
			{
				this._updatable = false;
				this.requestedUpdatable = x;
				this._refreshUpdatable ();
			}
			,get: function ()
			{
				return this._updatable;
			}
		},
		/**
		 * The number of rows in the model.
		 **/
		numRows:
		{
			type: Number
			,get: function ()
			{
				if (this.data)
					return this.data.length;
	
				return 0;
			}
		},
		/**
		 * The current status of the model.
		 **/
		status:
		{
			type: Number
			,get: function ()
			{
				return this._status;
			}
		},
		/**
		 * Checks if the model data is ready.
		 **/
		ready:
		{
			type: Boolean
			,get: function ()
			{
				return this._status == Db.Model.Status.READY;
			}
		},
		/**
		 * Update mode.
		 **/
		mode:
		{
			enumType: Db.Model.Mode
			,value: Db.Model.Mode.ON_CHANGE
		},
		/**
		 * Wether to execute the model query automatically.
		 **/
		autoLoad:
		{
			type: Boolean
			,value: true
		}
	}

	,_resultIndex: 0
	,_batch: null
	,_stmt: null
	,_status: Db.Model.Status.CLEAN
	,data: null
	,tables: null
	,columns: null
	,columnMap: null
	,_updatable: false

	,sortColumn: -1
	,sortWay: null
	,requestedIndexes: {}
	,indexes: []

	,requestedUpdatable: false
	,operations: null
	,operationsMap: null
	,defaults: []
	,requestedMainTable: null

	,initialize: function (props)
	{
		this.parent (props);
		this._cleanData ();
		this._setStatus (Db.Model.Status.CLEAN);
	}

	,loadXml: function (builder, node)
	{
		this.parent (builder, node);

		var query = node.firstChild.nodeValue;
		
		if (query)
			this.query = query;
	}
	
	,_autoLoad: function ()
	{
		if (this.autoLoad)
			this.refresh ();
	}

	/**
	 * Refresh the model data reexecuting the query on the database.
	 **/
	,refresh: function ()
	{
		if (this._stmt && this._batch)
			this._stmt.findHolders (this._batch);

		if (this._conn && this._stmt
		&& (!this._batch || this._batch.isReady ()))
		{
			this._setStatus (Db.Model.Status.LOADING);
			this._conn.execStmt (this._stmt, this._selectDone.bind (this), this._batch);
		}
		else
		{
			this._cleanData ();
			this._setStatus (Db.Model.Status.CLEAN);
		}
	}

	,_selectDone: function (resultSet)
	{
		var result;
		var dataResult;
		
		this._cleanData ();

		for (var i = 0; result = resultSet.fetchResult (); i++)
			if (i == this._resultIndex)
				dataResult = result;

		if (dataResult && typeof dataResult === 'object')
		{
			this.sortColumn = -1;
			this.data = dataResult.data;
			this.tables = dataResult.tables;
			this.columns = dataResult.columns;
			this.columnMap = dataResult.columnMap;
			this._repairColumns ();
			this._refreshRowIndexes (0);
			this._refreshMainTable ();
			
			for (column in this.requestedIndexes)
				this._buildIndex (column);
			
			this._setStatus (Db.Model.Status.READY);
		}
		else
			this._setStatus (Db.Model.Status.ERROR);
	}
	
	,_refreshRowIndexes: function (start)
	{
		for (var i = start; i < this.data.length; i++)
			this.data[i].index = i;
		
		if (this.operationsMap)
		{
			this.operationsMap = {};
			
			for (var i = 0; i < this.operations.length; i++)
				this.operationsMap[i] = this.operations[i];
		}
	}

	,_cleanData: function (error)
	{
		this.data = null;
		this.tables = null;
		this.columns = null;
		this.columnMap = null;
		this.indexes = [];
		this._resetOperations ();
	}
	
	,_refreshUpdatable: function ()
	{
		var oldValue = this._updatable;
		this._updatable = this._mainTable !== null && this.requestedUpdatable;
		
		if (oldValue != this._updatable)
			this.signalEmit ('updatable-changed');
	}
	
	,_refreshMainTable: function ()
	{
		var newMainTable = null;
		var tables = this.tables;

		if (tables)
		for (var i = 0; i < tables.length; i++)
		if (tables[i].pks.length > 0)
		if (!this.requestedMainTable
		|| tables[i].name === this.requestedMainTable)
		{
			newMainTable = i;
			break;
		}

		this._mainTable = newMainTable;
		this._refreshUpdatable ();
	}

	/**
	 * Sets the default value for inserted fields.
	 *
	 * @param {String} field The destination field name
	 * @param {String} table The destination table name
	 * @param {Sql.Expr} srcColumn The default value expression
	 **/
	,setDefault: function (field, table, expr)
	{
		this.defaults.push
		({
			 field: field
			,table: table
			,expr: expr
		});
	}

	/**
	 * Sets the default value for inserted fields.
	 *
	 * @param {String} field The destination field name
	 * @param {String} table The destination table name
	 * @param {Object} value The default value
	 **/
	,setDefaultFromValue: function (field, table, value)
	{
		this.defaults.push
		({
			 field: field
			,table: table
			,value: value
		});
	}

	/**
	 * Sets the default value for inserted fields from another column in the
	 * model.
	 *
	 * @param {String} field The destination field name
	 * @param {String} table The destination table name
	 * @param {String} srcColumn The source column
	 **/
	,setDefaultFromColumn: function (field, table, srcColumn)
	{
		this.defaults.push
		({
			 field: field
			,table: table
			,srcColumn: srcColumn
		});
	}

	/**
	 * Checks if the column exists.
	 *
	 * @param {integer} column The column index
	 * @return {Boolean} %true if column exists, %false otherwise
	 **/
	,checkColExists: function (column)
	{
		return this.columns
			&& column >= 0
			&& column < this.columns.length;
	}

	/**
	 * Checks if the row exists.
	 *
	 * @param {integer} rowIndex The row index
	 * @return {Boolean} %true if row exists, %false otherwise
	 **/
	,checkRowExists: function (rowIndex)
	{
		return this.data
			&& rowIndex >= 0
			&& rowIndex < this.data.length;
	}
	
	,_checkTableUpdatable: function (tableIndex)
	{
		var tableUpdatable = tableIndex !== null
			&& this.tables[tableIndex].pks.length > 0;
			
		if (!tableUpdatable)
			console.warn ("Db.Model: Table %s is not updatable",
				this.tables[tableIndex].name);
				
		return tableUpdatable;
	}
	
	/**
	 * Get the index of the column from its name.
	 *
	 * @param {string} columnName The column name
	 * @return {number} The column index or -1 if column not exists
	 **/
	,getColumnIndex: function (columnName)
	{
		var index;

		if (this.columnMap
		&& (index = this.columnMap[columnName]) !== undefined)
			return index;

		return -1;
	}
	
	/**
	 * Gets a value from the model.
	 *
	 * @param {number} rowIndex The row index
	 * @param {string} columnName The column name
	 * @return {mixed} The value
	 **/
	,get: function (rowIndex, columnName)
	{
		var index = this.getColumnIndex (columnName);
	
		if (index != -1)
			return this.getByIndex (rowIndex, index);
			
		return undefined;
	}

	/**
	 * Updates a value on the model.
	 *
	 * @param {number} rowIndex The row index
	 * @param {string} columnName The column name
	 * @param {mixed} value The new value
	 **/
	,set: function (rowIndex, columnName, value)
	{
		var index = this.getColumnIndex (columnName);
		
		if (index != -1)
			this.setByIndex (rowIndex, index, value);
		else
			console.warn ('Db.Model: Column %s doesn\'t exist', columnName);
	}

	/**
	 * Gets a value from the model using the column index.
	 *
	 * @param {number} rowIndex The row index
	 * @param {number} column The column index
	 * @return {mixed} The value
	 **/
	,getByIndex: function (rowIndex, column)
	{
		if (this.checkRowExists (rowIndex) && this.checkColExists (column))
			return this.data[rowIndex][column];

		return undefined;
	}

	/**
	 * Updates a value on the model using the column index.
	 *
	 * @param {number} rowIndex The row index
	 * @param {number} col The column index
	 * @param {mixed} value The new value
	 **/
	,setByIndex: function (rowIndex, col, value)
	{
		if (!this.checkRowExists (rowIndex)
		&& !this.checkColExists (col))
			return;

		var tableIndex = this.columns[col].table;
			
		if (!this._checkTableUpdatable (tableIndex))
			return;

		var row = this.data[rowIndex];

		var op = this._createOperation (rowIndex);
		op.type |= Db.Model.Operation.UPDATE;

		if (!op.oldValues)
			op.oldValues = [];
		if (!op.tables)
			op.tables = {};
			
		var tableOp = op.tables[tableIndex];
		
		if (!tableOp)
		{
			tableOp = Db.Model.Operation.UPDATE;
			var pks = this.tables[tableIndex].pks;
			
			for (var i = 0; i < pks.length; i++)
			if (!row[pks[i]] && !op.oldValues[pks[i]])
			{
				tableOp = Db.Model.Operation.INSERT;
				break;
			}

			op.tables[tableIndex] = tableOp;
		}
		
		if (tableOp & Db.Model.Operation.UPDATE
		&& op.oldValues[col] === undefined)
			op.oldValues[col] = row[col];

		this.signalEmit ('row-updated-before', rowIndex);
		row[col] = value;
		this.signalEmit ('row-updated', rowIndex, [col]);

		if (this.mode == Db.Model.Mode.ON_CHANGE
		&& !(op.type & Db.Model.Operation.INSERT))
			this.performOperations ();
	}

	/**
	 * Deletes a row from the model. 
	 *
	 * @param {number} rowIndex The row index
	 **/
	,deleteRow: function (rowIndex)
	{
		if (!this.checkRowExists (rowIndex)
		|| !this._checkTableUpdatable (this._mainTable))
			return;
	
		var op = this._createOperation (rowIndex);
		op.type |= Db.Model.Operation.DELETE;

		if (!this.requestedMainTable)
		{
			this.signalEmit ('row-deleted-before', rowIndex);
			this.data.splice (rowIndex, 1);
			this.signalEmit ('row-deleted', rowIndex);
			this._refreshRowIndexes (rowIndex);
		}
		else
		{
			this.signalEmit ('row-updated-before', rowIndex);

			if (!op.oldValues)
				op.oldValues = [];

			var updatedCols = [];

			for (var i = 0; i < this.columns.length; i++)
			if (this.columns[i].table == this._mainTable)
			{
				if (op.oldValues[i] === undefined)
					op.oldValues[i] = op.row[i];

				op.row[i] = null;
				updatedCols.push (i);
			}

			this.signalEmit ('row-updated', rowIndex, updatedCols);
		}
		
		if (this.mode === Db.Model.Mode.ON_CHANGE)
			this.performOperations ();
	}

	/**
	 * Inserts a new row on the model.
	 *
	 * @return The index of the inserted row
	 **/
	,insertRow: function ()
	{
		if (!this._checkTableUpdatable (this._mainTable))
			return -1;

		var cols = this.columns;
		var newRow = new Array (cols.length);
		
		for (var i = 0; i < cols.length; i++)
		if (cols[i].table === this._mainTable)
			newRow[i] = cols[i].def;
		else
			newRow[i] = null;
			
		var rowIndex = this.data.push (newRow) - 1;
		newRow.index = rowIndex;
			
		var op = this._createOperation (rowIndex);
		op.type |= Db.Model.Operation.INSERT;

		this.signalEmit ('row-inserted', rowIndex);
	
		return rowIndex;
	}
	
	/**
	 * Performs all model changes on the database.
	 **/
	,performOperations: function ()
	{
		var ops = this.operations;
	
		if (ops.length === 0)
		{
			this.signalEmit ('operations-done');
			return;
		}
		
		var stmts = new Sql.MultiStmt ();
		
		var query = new Sql.String ({query: 'START TRANSACTION'});
		stmts.addStmt (query);

		for (var i = 0; i < ops.length; i++)
		{
			query = null;
			var op = ops[i];

			if (op.type & Db.Model.Operation.DELETE)
			{
				if (op.type & Db.Model.Operation.INSERT)
					continue;
		
				var where = this._createWhere (this._mainTable, op, true);

				if (where)
				{
					query = new Sql.Delete ({where: where});
					query.addTarget (this._createTarget (this._mainTable));
				}
			}
			else if (op.type & (Db.Model.Operation.INSERT | Db.Model.Operation.UPDATE))
			{
				query = new Sql.MultiStmt ();

				for (var tableIndex in op.tables)
				{
					var stmt = this._createDmlQuery (op, parseInt (tableIndex));
					query.addStmt (stmt);
				}
			}

			if (query)
			{
				stmts.addStmt (query);
			}
			else
			{
				console.warn ('Db.Model: %s', _('ErrorSavingChanges'));
				return;
			}
		}

		var query = new Sql.String ({query: 'COMMIT'});
		stmts.addStmt (query);
	
		this._conn.execStmt (stmts,
			this._onOperationsDone.bind (this, ops));

		this._resetOperations ();
	}

	,_createDmlQuery: function (op, tableIndex)
	{
		var where = this._createWhere (tableIndex, op, false);

		if (!where)
			return null;

		var multiStmt = new Sql.MultiStmt ();
		var target = this._createTarget (tableIndex);
		
		var select = new Sql.Select ({where: where});
		select.addTarget (target);

		var row = op.row;
		var cols = this.columns;

		if (op.tables[tableIndex] & Db.Model.Operation.INSERT)
		{
			var dmlQuery = new Sql.Insert ();
			var table = this.tables[tableIndex];

			for (var i = 0; i < this.defaults.length; i++)
			{
				var def = this.defaults[i];

				if (def.table === table.name)
				{
					if (def.value)
						dmlQuery.addSet (def.field, def.value);
					else if (def.expr)
						dmlQuery.addExpr (def.field, def.expr);
					else if (def.srcColumn)
					{
						var columnIndex = this.getColumnIndex (def.srcColumn);
						dmlQuery.addSet (def.field, row[columnIndex]);
					}
				}
			}

			for (var i = 0; i < cols.length; i++)
			if (cols[i].table === tableIndex)
			{
				if (row[i] !== null)
					dmlQuery.addSet (cols[i].orgname, row[i]);

				select.addField (cols[i].orgname);
			}
		}
		else
		{
			var updateWhere = this._createWhere (tableIndex, op, true);
			
			if (!updateWhere)
				return null;
		
			var dmlQuery = new Sql.Update ({where: updateWhere});
		
			for (var i = 0; i < cols.length; i++)
			if (cols[i].table === tableIndex && op.oldValues[i] !== undefined)
			{
				var fieldName = cols[i].orgname;
				dmlQuery.addSet (fieldName, row[i]);
				select.addField (fieldName);
			}
		}

		dmlQuery.addTarget (target);

		multiStmt.addStmt (dmlQuery);
		multiStmt.addStmt (select);
		return multiStmt;
	}
	
	,_onOperationsDone: function (ops, resultSet)
	{
		if (resultSet.getError ())
		{
			this.operations = this.operations.concat (ops);
			
			for (var i = 0; i < ops.length; i++)
				this.operationsMap[ops[i].row.index] = ops[i];
		
			return;
		}

		var isOperation = false;

		resultSet.fetchResult ();
	
		for (var i = 0; i < ops.length; i++)
		{
			var op = ops[i];
			var row = op.row;
			
			if (!(op.type & Db.Model.Operation.DELETE
			&& op.type & Db.Model.Operation.INSERT))
				isOperation = true;
		
			if (op.type & Db.Model.Operation.DELETE)
			{
				resultSet.fetchResult ();
			}
			else if (op.type & (Db.Model.Operation.INSERT | Db.Model.Operation.UPDATE))
			{
				this.signalEmit ('row-updated-before', row.index);

				var updatedCols = [];
				var cols = this.columns;
		
				for (var tableIndex in op.tables)
				{	
					var j = 0;
					tableIndex = parseInt (tableIndex);

					resultSet.fetchResult ();
					var newValues = resultSet.fetchRow ();		

					if (op.tables[tableIndex] & Db.Model.Operation.INSERT)
					{
						for (var i = 0; i < cols.length; i++)
						if (cols[i].table === tableIndex)
						{
							row[i] = newValues[j++];
							updatedCols.push (i);
						}
					}
					else
					{
						for (var i = 0; i < cols.length; i++)
						if (cols[i].table === tableIndex
						&& op.oldValues[i] !== undefined)
						{
							row[i] = newValues[j++];
							updatedCols.push (i);
						}
					}
				}

				this.signalEmit ('row-updated', row.index, updatedCols);
			}
		}
		
		resultSet.fetchResult ();
			
//		if (isOperation)
			this.signalEmit ('operations-done');
	}
	
	/**
	 * Undoes all unsaved changes made to the model.
	 **/
	,reverseOperations: function ()
	{
		for (var i = 0; i < this.operations.length; i++)
		{
			var op = this.operations[i];
			var row = op.row;

			if (op.type & Db.Model.Operation.DELETE
			&& !(op.type & Db.Model.Operation.INSERT))
			{
				this.data.splice (row.index, 0, row);
				this.signalEmit ('row-inserted', row.index);
			}
			else if (op.type & Db.Model.Operation.UPDATE)
			{
				this.signalEmit ('row-updated-before', row.index);

				var updatedCols = [];
				var cols = this.columns;

				for (var i = 0; i < cols.length; i++)
				if (op.oldValues[i] !== undefined)
				{
					row[i] = op.oldValues[i];
					updatedCols.push (i);
				}

				this.signalEmit ('row-updated', row.index, updatedCols);
			}
		}

		this._resetOperations ();
		this._refreshRowIndexes (0);
	}
	
	,_resetOperations: function ()
	{
		this.operations = [];
		this.operationsMap = {};
	}
	
	/*
	 * Function used to sort the model ascending.
	 */
	,sortFunctionAsc: function (column, a, b)
	{
		if (a[column] < b[column])
			return -1;
		else if (a[column] > b[column])
			return 1;
			
		return 0;
	}
	
	/*
	 * Function used to sort the model descending.
	 */
	,sortFunctionDesc: function (column, a, b)
	{
		if (a[column] > b[column])
			return -1;
		else if (a[column] < b[column])
			return 1;
			
		return 0;
	}

	/**
	 * Orders the model by the specified column.
	 *
	 * @param {integer} column The column index
	 * @param {Db.Model.SortWay} way The sort way
	 **/
	,sort: function (column, way)
	{
		if (!this.checkColExists (column))
			return;
	
		this._setStatus (Db.Model.Status.LOADING);

		if (column !== this.sortColumn)
		{
			if (way === Db.Model.SortWay.DESC)
				var sortFunction = this.sortFunctionDesc;
			else
				var sortFunction = this.sortFunctionAsc;
		
			this.data.sort (sortFunction.bind (this, column));
		}
		else if (way !== this.sortWay)
			this.data.reverse ();
		
		this.sortColumn = column;
		this.sortWay = way;

		this._refreshRowIndexes (0);
		this._setStatus (Db.Model.Status.READY);
	}

	/**
	 * Builds an internal hash index for the specified column, this speeds
	 * significantly searches on that column, specially when model has a lot of
	 * rows.
	 *
	 * FIXME: Not fully implemented.
	 *
	 * @param {String} column The column name
	 **/
	,indexColumn: function (column)
	{
		this.requestedIndexes[column] = true;

		if (this._status === Db.Model.Status.READY)
			this._buildIndex (column);
	}

	,_buildIndex: function (column)
	{
		var columnIndex = this.getColumnIndex (column);

		if (columnIndex !== -1)
		{
			var index = {};
			var data = this.data;

			switch (this.columns[columnIndex].type)
			{
				case Db.Conn.Type.TIMESTAMP:
				case Db.Conn.Type.DATE_TIME:
				case Db.Conn.Type.DATE:
					for (var i = 0; i < data.length; i++)
						index[data[i][columnIndex].toString ()] = i;
					break;
				default:
					for (var i = 0; i < data.length; i++)
						index[data[i][columnIndex]] = i;
			}

			this.indexes[columnIndex] = index;
		}
	}

	/**
	 * Searchs a value on the model and returns the row index of the first
	 * ocurrence.
	 * If an index have been built on that column, it will be used, for more
	 * information see the indexColumn() method.
	 *
	 * @param {String} column The column name
	 * @param {Object} value The value to search
	 * @return {integer} The column index
	 **/
	,search: function (column, value)
	{
		var index = this.getColumnIndex (column);
		return this.searchByIndex (index, value);
	}

	/**
	 * Searchs a value on the model and returns the row index of the first
	 * ocurrence.
	 *
	 * @param {integer} col The column index
	 * @param {Object} value The value to search
	 * @return {integer} The column index
	 **/
	,searchByIndex: function (col, value)
	{
		if (!this.checkColExists (col))
			return -1;
		
		if (value)
		switch (this.columns[col].type)
		{
			case Db.Conn.Type.BOOLEAN:
				value = !!value;
				break;
			case Db.Conn.Type.INTEGER:
				value = parseInt (value);
				break;
			case Db.Conn.Type.DOUBLE:
				value = parseFloat (value);
				break;
			default:
				value = value.toString ();
		}
		
		// Searchs the value using an internal index.
		
		var index = this.indexes[col];
		
		if (index)
		{
			if (index[value] !== undefined)
				return index[value];
				
			return -1;
		}
		
		// Searchs the value using a loop.

		var data = this.data;
	
		switch (this.columns[col].type)
		{
			case Db.Conn.Type.TIMESTAMP:
			case Db.Conn.Type.DATE_TIME:
			case Db.Conn.Type.DATE:
			{
				for (var i = 0; i < data.length; i++)
				if (value === data[i][col].toString ());
					return i;

				break;
			}
			default:
				for (var i = 0; i < data.length; i++)
				if (value === data[i][col])
					return i;
		}
		
		return -1;
	}

	,_setStatus: function (status)
	{
		this._status = status;
		this.signalEmit ('status-changed', status);
		this.signalEmit ('status-changed-after', status);
	}
	
	,_createTarget: function (tableIndex)
	{
		var table = this.tables[tableIndex];

		return new Sql.Table
		({
			 name: table.orgname
			,schema: table.schema
		});
	}

	,_createWhere: function (tableIndex, op, useOldValues)
	{
		var where = new Sql.Operation ({type: Sql.Operation.Type.AND});
		var pks = this.tables[tableIndex].pks;
		
		if (pks.length === 0)
			return null;

		for (var i = 0; i < pks.length; i++)
		{
			var col = pks[i];
			var column = this.columns[col];
		
			var equalOp = new Sql.Operation ({type: Sql.Operation.Type.EQUAL});
			equalOp.exprs.add (new Sql.Field ({name: column.orgname}));
			where.exprs.add (equalOp);

			var pkValue = null;
			
			if (useOldValues && op.oldValues
			&& op.oldValues[col] !== undefined)
				pkValue = op.oldValues[col];
			else
				pkValue = op.row[col];

			if (pkValue)
				equalOp.exprs.add (new Sql.Value ({value: pkValue}));
			else if (column.flags & Db.Conn.Flag.AI && !useOldValues)
				equalOp.exprs.add (new Sql.Func ({name: 'LAST_INSERT_ID'}));
			else
				return null;
		}
		
		return where;
	}

	,_createOperation: function (rowIndex)
	{
		var op = this.operationsMap[rowIndex];

		if (!op)
		{		
			op = {
				type: 0,
				row: this.data[rowIndex]
			};
			this.operations.push (op);
			this.operationsMap[rowIndex] = op;
		}
			
		return op;
	}

	/**
	 * Overrides information about a table and its columns. If a parameter is
	 * not provided, the original will be preserved. This method should be used
	 * primarily to avoid the mysql bug that causes this information will not
	 * be set correctly.
	 * For more information see the following links:
	 *  - https://bugs.mysql.com/bug.php?id=44660
	 *  - https://bugs.mysql.com/bug.php?id=26894
	 *
	 * @param {String} table The table alias
	 * @param {String} orgtable The original table name
	 * @param {String} schema The original table schema
	 * @param {Array} pks Array with the names of primary keys
	 * @param {String} ai The autoincrement column name
	 **/
	,setInfo: function (table, orgname, schema, pks, ai)
	{
		if (!this.tableInfo)
			this.tableInfo = {};
	
		this.tableInfo[table] =
		{
			orgname: orgname,
			schema: schema,
			pks: pks,
			ai: ai
		};
		
		this._repairColumns ();
	}

	,_repairColumns: function ()
	{
		// Repairs wrong table info

		if (this.tableInfo && this.tables)
		for (var i = 0; i < this.tables.length; i++)
		{
			var table = this.tables[i];
			var tableInfo = this.tableInfo[table.name];
		
			if (!tableInfo)
				continue;

			table.orgname = tableInfo.orgname;
			table.schema = tableInfo.schema;

			if (tableInfo.pks)
			{
				table.pks = [];
			
				for (var j = 0; j < tableInfo.pks.length; j++)
				{
					var colIndex = this.getColumnIndex (tableInfo.pks[j]);
					table.pks.push (colIndex);
				}
			}

			if (tableInfo.ai)
			{
				var colIndex = this.getColumnIndex (tableInfo.ai);
				this.columns[colIndex].flags |= Db.Conn.Flag.AI;
			}
		}
	}
});