<?php

/**
 * This file is part of the Propel package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @license    MIT License
 */

/**
 * This is a utility class for all generated Peer classes in the system.
 *
 * Peer classes are responsible for isolating all of the database access
 * for a specific business object.  They execute all of the SQL
 * against the database.  Over time this class has grown to include
 * utility methods which ease execution of cross-database queries and
 * the implementation of concrete Peers.
 *
 * @author     Hans Lellelid <hans@xmpl.org> (Propel)
 * @author     Kaspars Jaudzems <kaspars.jaudzems@inbox.lv> (Propel)
 * @author     Heltem <heltem@o2php.com> (Propel)
 * @author     Frank Y. Kim <frank.kim@clearink.com> (Torque)
 * @author     John D. McNally <jmcnally@collab.net> (Torque)
 * @author     Brett McLaughlin <bmclaugh@algx.net> (Torque)
 * @author     Stephen Haberman <stephenh@chase3000.com> (Torque)
 * @version    $Revision: 1772 $
 * @package    propel.runtime.util
 */
class BasePeer
{

	/** Array (hash) that contains the cached mapBuilders. */
	private static $mapBuilders = array();

	/** Array (hash) that contains cached validators */
	private static $validatorMap = array();

	/**
	 * phpname type
	 * e.g. 'AuthorId'
	 */
	const TYPE_PHPNAME = 'phpName';

	/**
	 * studlyphpname type
	 * e.g. 'authorId'
	 */
	const TYPE_STUDLYPHPNAME = 'studlyPhpName';

	/**
	 * column (peer) name type
	 * e.g. 'book.AUTHOR_ID'
	 */
	const TYPE_COLNAME = 'colName';

	/**
	 * column part of the column peer name
	 * e.g. 'AUTHOR_ID'
	 */
	const TYPE_RAW_COLNAME = 'rawColName';

	/**
	 * column fieldname type
	 * e.g. 'author_id'
	 */
	const TYPE_FIELDNAME = 'fieldName';

	/**
	 * num type
	 * simply the numerical array index, e.g. 4
	 */
	const TYPE_NUM = 'num';

	static public function getFieldnames ($classname, $type = self::TYPE_PHPNAME) {

		// TODO we should take care of including the peer class here

		$peerclass = 'Base' . $classname . 'Peer'; // TODO is this always true?
		$callable = array($peerclass, 'getFieldnames');

		return call_user_func($callable, $type);
	}

	static public function translateFieldname($classname, $fieldname, $fromType, $toType) {

		// TODO we should take care of including the peer class here

		$peerclass = 'Base' . $classname . 'Peer'; // TODO is this always true?
		$callable = array($peerclass, 'translateFieldname');
		$args = array($fieldname, $fromType, $toType);

		return call_user_func_array($callable, $args);
	}

	/**
	 * Method to perform deletes based on values and keys in a
	 * Criteria.
	 *
	 * @param      Criteria $criteria The criteria to use.
	 * @param      PropelPDO $con A PropelPDO connection object.
	 * @return     int	The number of rows affected by last statement execution.  For most
	 * 				uses there is only one delete statement executed, so this number
	 * 				will correspond to the number of rows affected by the call to this
	 * 				method.  Note that the return value does require that this information
	 * 				is returned (supported) by the PDO driver.
	 * @throws     PropelException
	 */
	public static function doDelete(Criteria $criteria, PropelPDO $con)
	{
		$db = Propel::getDB($criteria->getDbName());
		$dbMap = Propel::getDatabaseMap($criteria->getDbName());

		// Set up a list of required tables (one DELETE statement will
		// be executed per table)
		$tables = $criteria->getTablesColumns();
		if (empty($tables)) {
			throw new PropelException("Cannot delete from an empty Criteria");
		}

		$affectedRows = 0; // initialize this in case the next loop has no iterations.
		
		foreach ($tables as $tableName => $columns) {

			$whereClause = array();
			$params = array();
			$stmt = null;
			try {
				$sql = 'DELETE ';
				if ($queryComment = $criteria->getComment()) {
					$sql .= '/* ' . $queryComment . ' */ ';
				}
				if ($realTableName = $criteria->getTableForAlias($tableName)) {
					if ($db->useQuoteIdentifier()) {
						$realTableName = $db->quoteIdentifierTable($realTableName);
					}
					$sql .= $tableName . ' FROM ' . $realTableName . ' AS ' . $tableName;
				} else {
					if ($db->useQuoteIdentifier()) {
						$tableName = $db->quoteIdentifierTable($tableName);
					}
					$sql .= 'FROM ' . $tableName;
				}

				foreach ($columns as $colName) {
					$sb = "";
					$criteria->getCriterion($colName)->appendPsTo($sb, $params);
					$whereClause[] = $sb;
				}
				$sql .= " WHERE " .  implode(" AND ", $whereClause);

				$stmt = $con->prepare($sql);
				self::populateStmtValues($stmt, $params, $dbMap, $db);
				$stmt->execute();
				$affectedRows = $stmt->rowCount();
			} catch (Exception $e) {
				Propel::log($e->getMessage(), Propel::LOG_ERR);
				throw new PropelException(sprintf('Unable to execute DELETE statement [%s]', $sql), $e);
			}

		} // for each table

		return $affectedRows;
	}

	/**
	 * Method to deletes all contents of specified table.
	 *
	 * This method is invoked from generated Peer classes like this:
	 * <code>
	 * public static function doDeleteAll($con = null)
	 * {
	 *   if ($con === null) $con = Propel::getConnection(self::DATABASE_NAME);
	 *   BasePeer::doDeleteAll(self::TABLE_NAME, $con, self::DATABASE_NAME);
	 * }
	 * </code>
	 *
	 * @param      string $tableName The name of the table to empty.
	 * @param      PropelPDO $con A PropelPDO connection object.
	 * @param      string $databaseName the name of the database.
	 * @return     int	The number of rows affected by the statement.  Note
	 * 				that the return value does require that this information
	 * 				is returned (supported) by the Propel db driver.
	 * @throws     PropelException - wrapping SQLException caught from statement execution.
	 */
	public static function doDeleteAll($tableName, PropelPDO $con, $databaseName = null)
	{
		try {
			$db = Propel::getDB($databaseName);
			if ($db->useQuoteIdentifier()) {
				$tableName = $db->quoteIdentifierTable($tableName);
			}
			$sql = "DELETE FROM " . $tableName;
			$stmt = $con->prepare($sql);
			$stmt->execute();
			return $stmt->rowCount();
		} catch (Exception $e) {
			Propel::log($e->getMessage(), Propel::LOG_ERR);
			throw new PropelException(sprintf('Unable to execute DELETE ALL statement [%s]', $sql), $e);
		}
	}

	/**
	 * Method to perform inserts based on values and keys in a
	 * Criteria.
	 * <p>
	 * If the primary key is auto incremented the data in Criteria
	 * will be inserted and the auto increment value will be returned.
	 * <p>
	 * If the primary key is included in Criteria then that value will
	 * be used to insert the row.
	 * <p>
	 * If no primary key is included in Criteria then we will try to
	 * figure out the primary key from the database map and insert the
	 * row with the next available id using util.db.IDBroker.
	 * <p>
	 * If no primary key is defined for the table the values will be
	 * inserted as specified in Criteria and null will be returned.
	 *
	 * @param      Criteria $criteria Object containing values to insert.
	 * @param      PropelPDO $con A PropelPDO connection.
	 * @return     mixed The primary key for the new row if (and only if!) the primary key
	 *				is auto-generated.  Otherwise will return <code>null</code>.
	 * @throws     PropelException
	 */
	public static function doInsert(Criteria $criteria, PropelPDO $con) {

		// the primary key
		$id = null;

		$db = Propel::getDB($criteria->getDbName());

		// Get the table name and method for determining the primary
		// key value.
		$keys = $criteria->keys();
		if (!empty($keys)) {
			$tableName = $criteria->getTableName( $keys[0] );
		} else {
			throw new PropelException("Database insert attempted without anything specified to insert");
		}

		$dbMap = Propel::getDatabaseMap($criteria->getDbName());
		$tableMap = $dbMap->getTable($tableName);
		$keyInfo = $tableMap->getPrimaryKeyMethodInfo();
		$useIdGen = $tableMap->isUseIdGenerator();
		//$keyGen = $con->getIdGenerator();

		$pk = self::getPrimaryKey($criteria);

		// only get a new key value if you need to
		// the reason is that a primary key might be defined
		// but you are still going to set its value. for example:
		// a join table where both keys are primary and you are
		// setting both columns with your own values

		// pk will be null if there is no primary key defined for the table
		// we're inserting into.
		if ($pk !== null && $useIdGen && !$criteria->keyContainsValue($pk->getFullyQualifiedName()) && $db->isGetIdBeforeInsert()) {
			try {
				$id = $db->getId($con, $keyInfo);
			} catch (Exception $e) {
				throw new PropelException("Unable to get sequence id.", $e);
			}
			$criteria->add($pk->getFullyQualifiedName(), $id);
		}

		try {
			$adapter = Propel::getDB($criteria->getDBName());

			$qualifiedCols = $criteria->keys(); // we need table.column cols when populating values
			$columns = array(); // but just 'column' cols for the SQL
			foreach ($qualifiedCols as $qualifiedCol) {
				$columns[] = substr($qualifiedCol, strrpos($qualifiedCol, '.') + 1);
			}

			// add identifiers
			if ($adapter->useQuoteIdentifier()) {
				$columns = array_map(array($adapter, 'quoteIdentifier'), $columns);
				$tableName = $adapter->quoteIdentifierTable($tableName); 
			}

			$sql = 'INSERT INTO ' . $tableName
			. ' (' . implode(',', $columns) . ')'
			. ' VALUES (';
			// . substr(str_repeat("?,", count($columns)), 0, -1) . 
			for($p=1, $cnt=count($columns); $p <= $cnt; $p++) {
				$sql .= ':p'.$p;
				if ($p !== $cnt) $sql .= ',';
			}
			$sql .= ')';

			$stmt = $con->prepare($sql);
			self::populateStmtValues($stmt, self::buildParams($qualifiedCols, $criteria), $dbMap, $db);
			$stmt->execute();

		} catch (Exception $e) {
			Propel::log($e->getMessage(), Propel::LOG_ERR);
			throw new PropelException(sprintf('Unable to execute INSERT statement [%s]', $sql), $e);
		}

		// If the primary key column is auto-incremented, get the id now.
		if ($pk !== null && $useIdGen && $db->isGetIdAfterInsert()) {
			try {
				$id = $db->getId($con, $keyInfo);
			} catch (Exception $e) {
				throw new PropelException("Unable to get autoincrement id.", $e);
			}
		}

		return $id;
	}

	/**
	 * Method used to update rows in the DB.  Rows are selected based
	 * on selectCriteria and updated using values in updateValues.
	 * <p>
	 * Use this method for performing an update of the kind:
	 * <p>
	 * WHERE some_column = some value AND could_have_another_column =
	 * another value AND so on.
	 *
	 * @param      $selectCriteria A Criteria object containing values used in where
	 *		clause.
	 * @param      $updateValues A Criteria object containing values used in set
	 *		clause.
	 * @param      PropelPDO $con The PropelPDO connection object to use.
	 * @return     int	The number of rows affected by last update statement.  For most
	 * 				uses there is only one update statement executed, so this number
	 * 				will correspond to the number of rows affected by the call to this
	 * 				method.  Note that the return value does require that this information
	 * 				is returned (supported) by the Propel db driver.
	 * @throws     PropelException
	 */
	public static function doUpdate(Criteria $selectCriteria, Criteria $updateValues, PropelPDO $con) {

		$db = Propel::getDB($selectCriteria->getDbName());
		$dbMap = Propel::getDatabaseMap($selectCriteria->getDbName());

		// Get list of required tables, containing all columns
		$tablesColumns = $selectCriteria->getTablesColumns();
		if (empty($tablesColumns)) {
			$tablesColumns = array($selectCriteria->getPrimaryTableName() => array());
		}

		// we also need the columns for the update SQL
		$updateTablesColumns = $updateValues->getTablesColumns();

		$affectedRows = 0; // initialize this in case the next loop has no iterations.

		foreach ($tablesColumns as $tableName => $columns) {

			$whereClause = array();
			$params = array();
			$stmt = null;
			try {
				$sql = 'UPDATE ';
				if ($queryComment = $selectCriteria->getComment()) {
					$sql .= '/* ' . $queryComment . ' */ ';
				}
				// is it a table alias?
				if ($tableName2 = $selectCriteria->getTableForAlias($tableName)) {
					$udpateTable = $tableName2 . ' ' . $tableName;
					$tableName = $tableName2;
				} else {
					$udpateTable = $tableName;
				}
				if ($db->useQuoteIdentifier()) {
					$sql .= $db->quoteIdentifierTable($udpateTable); 
				} else { 
					$sql .= $udpateTable;
				}
				$sql .= " SET ";
				$p = 1;
				foreach ($updateTablesColumns[$tableName] as $col) {
					$updateColumnName = substr($col, strrpos($col, '.') + 1);
					// add identifiers for the actual database?
					if ($db->useQuoteIdentifier()) {
						$updateColumnName = $db->quoteIdentifier($updateColumnName);
					}
					if ($updateValues->getComparison($col) != Criteria::CUSTOM_EQUAL) {
						$sql .= $updateColumnName . '=:p'.$p++.', ';
					} else {
						$param = $updateValues->get($col);
						$sql .= $updateColumnName . ' = ';
						if (is_array($param)) {
							if (isset($param['raw'])) {
								$raw = $param['raw'];
								$rawcvt = '';
								// parse the $params['raw'] for ? chars
								for($r=0,$len=strlen($raw); $r < $len; $r++) {
									if ($raw{$r} == '?') {
										$rawcvt .= ':p'.$p++;
									} else {
										$rawcvt .= $raw{$r};
									}
								}
								$sql .= $rawcvt . ', ';
							} else {
								$sql .= ':p'.$p++.', ';
							}
							if (isset($param['value'])) {
								$updateValues->put($col, $param['value']);
							}
						} else {
							$updateValues->remove($col);
							$sql .= $param . ', ';
						}
					}
				}
				
				$params = self::buildParams($updateTablesColumns[$tableName], $updateValues);

				$sql = substr($sql, 0, -2);
				if (!empty($columns)) {
					foreach ($columns as $colName) {
						$sb = "";
						$selectCriteria->getCriterion($colName)->appendPsTo($sb, $params);
						$whereClause[] = $sb;
					}
					$sql .= " WHERE " .  implode(" AND ", $whereClause);
				}

				$stmt = $con->prepare($sql);

				// Replace ':p?' with the actual values
				self::populateStmtValues($stmt, $params, $dbMap, $db);

				$stmt->execute();

				$affectedRows = $stmt->rowCount();

				$stmt = null; // close

			} catch (Exception $e) {
				if ($stmt) $stmt = null; // close
				Propel::log($e->getMessage(), Propel::LOG_ERR);
				throw new PropelException(sprintf('Unable to execute UPDATE statement [%s]', $sql), $e);
			}

		} // foreach table in the criteria

		return $affectedRows;
	}

	/**
	 * Executes query build by createSelectSql() and returns the resultset statement.
	 *
	 * @param      Criteria $criteria A Criteria.
	 * @param      PropelPDO $con A PropelPDO connection to use.
	 * @return     PDOStatement The resultset.
	 * @throws     PropelException
	 * @see        createSelectSql()
	 */
	public static function doSelect(Criteria $criteria, PropelPDO $con = null)
	{
		$dbMap = Propel::getDatabaseMap($criteria->getDbName());
		$db = Propel::getDB($criteria->getDbName());
		$stmt = null;
		
		if ($con === null) {
			$con = Propel::getConnection($criteria->getDbName(), Propel::CONNECTION_READ);
		}

		if ($criteria->isUseTransaction()) {
			$con->beginTransaction();
		} 

		try {

			$params = array();
			$sql = self::createSelectSql($criteria, $params);

			$stmt = $con->prepare($sql);

			self::populateStmtValues($stmt, $params, $dbMap, $db);

			$stmt->execute();

			if ($criteria->isUseTransaction()) {
				$con->commit();
			}

		} catch (Exception $e) {
			if ($stmt) {
				$stmt = null; // close
			}
			if ($criteria->isUseTransaction()) {
				$con->rollBack();
			}
			Propel::log($e->getMessage(), Propel::LOG_ERR);
			throw new PropelException(sprintf('Unable to execute SELECT statement [%s]', $sql), $e);
		}

		return $stmt;
	}

	/**
	 * Executes a COUNT query using either a simple SQL rewrite or, for more complex queries, a
	 * sub-select of the SQL created by createSelectSql() and returns the statement.
	 *
	 * @param      Criteria $criteria A Criteria.
	 * @param      PropelPDO $con A PropelPDO connection to use.
	 * @return     PDOStatement The resultset statement.
	 * @throws     PropelException
	 * @see        createSelectSql()
	 */
	public static function doCount(Criteria $criteria, PropelPDO $con = null)
	{
		$dbMap = Propel::getDatabaseMap($criteria->getDbName());
		$db = Propel::getDB($criteria->getDbName());

		if ($con === null) {
			$con = Propel::getConnection($criteria->getDbName(), Propel::CONNECTION_READ);
		}

		$stmt = null;

		if ($criteria->isUseTransaction()) {
			$con->beginTransaction();
		}

		$needsComplexCount = $criteria->getGroupByColumns() 
			|| $criteria->getOffset()
			|| $criteria->getLimit() 
			|| $criteria->getHaving() 
			|| in_array(Criteria::DISTINCT, $criteria->getSelectModifiers());

		try {

			$params = array();

			if ($needsComplexCount) {
				if (self::needsSelectAliases($criteria)) {
					if ($criteria->getHaving()) {
						throw new PropelException('Propel cannot create a COUNT query when using HAVING and  duplicate column names in the SELECT part');
					}
					self::turnSelectColumnsToAliases($criteria);
				}
				$selectSql = self::createSelectSql($criteria, $params);
				$sql = 'SELECT COUNT(*) FROM (' . $selectSql . ') propelmatch4cnt';
			} else {
				// Replace SELECT columns with COUNT(*)
				$criteria->clearSelectColumns()->addSelectColumn('COUNT(*)');
				$sql = self::createSelectSql($criteria, $params);
			}

			$stmt = $con->prepare($sql);
			self::populateStmtValues($stmt, $params, $dbMap, $db);
			$stmt->execute();

			if ($criteria->isUseTransaction()) {
				$con->commit();
			}

		} catch (Exception $e) {
			if ($stmt !== null) {
				$stmt = null;
			}
			if ($criteria->isUseTransaction()) {
				$con->rollBack();
			}
			Propel::log($e->getMessage(), Propel::LOG_ERR);
			throw new PropelException(sprintf('Unable to execute COUNT statement [%s]', $sql), $e);
		}

		return $stmt;
	}

	/**
	 * Populates values in a prepared statement.
	 *
	 * This method is designed to work with the createSelectSql() method, which creates
	 * both the SELECT SQL statement and populates a passed-in array of parameter
	 * values that should be substituted.
	 *
	 * <code>
	 * $params = array();
	 * $sql = BasePeer::createSelectSql($criteria, $params);
	 * BasePeer::populateStmtValues($stmt, $params, Propel::getDatabaseMap($critera->getDbName()), Propel::getDB($criteria->getDbName()));
	 * </code>
	 *
	 * @param      PDOStatement $stmt
	 * @param      array $params array('column' => ..., 'table' => ..., 'value' => ...)
	 * @param      DatabaseMap $dbMap
	 * @return     int The number of params replaced.
	 * @see        createSelectSql()
	 * @see        doSelect()
	 */
	public static function populateStmtValues(PDOStatement $stmt, array $params, DatabaseMap $dbMap, DBAdapter $db)
	{
		$i = 1;
		foreach ($params as $param) {
			$tableName = $param['table'];
			$columnName = $param['column'];
			$value = $param['value'];

			if (null === $value) {

				$stmt->bindValue(':p'.$i++, null, PDO::PARAM_NULL);

			} elseif (null !== $tableName) {

				$cMap = $dbMap->getTable($tableName)->getColumn($columnName);
				$type = $cMap->getType();
				$pdoType = $cMap->getPdoType();

				// FIXME - This is a temporary hack to get around apparent bugs w/ PDO+MYSQL
				// See http://pecl.php.net/bugs/bug.php?id=9919
				if ($pdoType == PDO::PARAM_BOOL && $db instanceof DBMySQL) {
					$value = (int) $value;
					$pdoType = PDO::PARAM_INT;
				} elseif (is_numeric($value) && $cMap->isEpochTemporal()) { // it's a timestamp that needs to be formatted
					if ($type == PropelColumnTypes::TIMESTAMP) {
						$value = date($db->getTimestampFormatter(), $value);
					} else if ($type == PropelColumnTypes::DATE) {
						$value = date($db->getDateFormatter(), $value);
					} else if ($type == PropelColumnTypes::TIME) {
						$value = date($db->getTimeFormatter(), $value);
					}
				} elseif ($value instanceof DateTime && $cMap->isTemporal()) { // it's a timestamp that needs to be formatted
					if ($type == PropelColumnTypes::TIMESTAMP || $type == PropelColumnTypes::BU_TIMESTAMP) {
						$value = $value->format($db->getTimestampFormatter());
					} else if ($type == PropelColumnTypes::DATE || $type == PropelColumnTypes::BU_DATE) {
						$value = $value->format($db->getDateFormatter());
					} else if ($type == PropelColumnTypes::TIME) {
						$value = $value->format($db->getTimeFormatter());
					}
				} elseif (is_resource($value) && $cMap->isLob()) {
					// we always need to make sure that the stream is rewound, otherwise nothing will
					// get written to database.
					rewind($value);
				}

				$stmt->bindValue(':p'.$i++, $value, $pdoType);
			} else {
				$stmt->bindValue(':p'.$i++, $value);
			}
		} // foreach
	}

	/**
	 * Applies any validators that were defined in the schema to the specified columns.
	 *
	 * @param      string $dbName The name of the database
	 * @param      string $tableName The name of the table
	 * @param      array $columns Array of column names as key and column values as value.
	 */
	public static function doValidate($dbName, $tableName, $columns)
	{
		$dbMap = Propel::getDatabaseMap($dbName);
		$tableMap = $dbMap->getTable($tableName);
		$failureMap = array(); // map of ValidationFailed objects
		foreach ($columns as $colName => $colValue) {
			if ($tableMap->containsColumn($colName)) {
				$col = $tableMap->getColumn($colName);
				foreach ($col->getValidators() as $validatorMap) {
					$validator = BasePeer::getValidator($validatorMap->getClass());
					if ($validator && ($col->isNotNull() || $colValue !== null) && $validator->isValid($validatorMap, $colValue) === false) {
						if (!isset($failureMap[$colName])) { // for now we do one ValidationFailed per column, not per rule
							$failureMap[$colName] = new ValidationFailed($colName, $validatorMap->getMessage(), $validator);
						}
					}
				}
			}
		}
		return (!empty($failureMap) ? $failureMap : true);
	}

	/**
	 * Helper method which returns the primary key contained
	 * in the given Criteria object.
	 *
	 * @param      Criteria $criteria A Criteria.
	 * @return     ColumnMap If the Criteria object contains a primary
	 *		  key, or null if it doesn't.
	 * @throws     PropelException
	 */
	private static function getPrimaryKey(Criteria $criteria)
	{
		// Assume all the keys are for the same table.
		$keys = $criteria->keys();
		$key = $keys[0];
		$table = $criteria->getTableName($key);

		$pk = null;

		if (!empty($table)) {

			$dbMap = Propel::getDatabaseMap($criteria->getDbName());

			$pks = $dbMap->getTable($table)->getPrimaryKeys();
			if (!empty($pks)) {
				$pk = array_shift($pks);
			}
		}
		return $pk;
	}

	/**
	 * Checks whether the Criteria needs to use column aliasing
	 * This is implemented in a service class rather than in Criteria itself
	 * in order to avoid doing the tests when it's not necessary (e.g. for SELECTs)
	 */
	public static function needsSelectAliases(Criteria $criteria)
	{
		$columnNames = array();
		foreach ($criteria->getSelectColumns() as $fullyQualifiedColumnName) {
			if ($pos = strrpos($fullyQualifiedColumnName, '.')) {
				$columnName = substr($fullyQualifiedColumnName, $pos);
				if (isset($columnNames[$columnName])) {
					// more than one column with the same name, so aliasing is required
					return true;
				}
				$columnNames[$columnName] = true;
			}
		}
		return false;
	}
	
	/**
	 * Ensures uniqueness of select column names by turning them all into aliases
	 * This is necessary for queries on more than one table when the tables share a column name
	 * @see http://propel.phpdb.org/trac/ticket/795
	 *
	 * @param Criteria $criteria
	 * 
	 * @return Criteria The input, with Select columns replaced by aliases
	 */
	public static function turnSelectColumnsToAliases(Criteria $criteria)
	{
		$selectColumns = $criteria->getSelectColumns();
		// clearSelectColumns also clears the aliases, so get them too
		$asColumns = $criteria->getAsColumns();
		$criteria->clearSelectColumns();
		$columnAliases = $asColumns;
		// add the select columns back
		foreach ($selectColumns as $clause) {
			// Generate a unique alias
			$baseAlias = preg_replace('/\W/', '_', $clause);
			$alias = $baseAlias;
			// If it already exists, add a unique suffix
			$i = 0;
			while (isset($columnAliases[$alias])) {
				$i++;
				$alias = $baseAlias . '_' . $i;
			}
			// Add it as an alias
			$criteria->addAsColumn($alias, $clause);
			$columnAliases[$alias] = $clause;
		}
		// Add the aliases back, don't modify them
		foreach ($asColumns as $name => $clause) {
			$criteria->addAsColumn($name, $clause);
		}
		
		return $criteria;
	}
	
	/**
	 * Method to create an SQL query based on values in a Criteria.
	 *
	 * This method creates only prepared statement SQL (using ? where values
	 * will go).  The second parameter ($params) stores the values that need
	 * to be set before the statement is executed.  The reason we do it this way
	 * is to let the PDO layer handle all escaping & value formatting.
	 *
	 * @param      Criteria $criteria Criteria for the SELECT query.
	 * @param      array &$params Parameters that are to be replaced in prepared statement.
	 * @return     string
	 * @throws     PropelException Trouble creating the query string.
	 */
	public static function createSelectSql(Criteria $criteria, &$params)
	{
		$db = Propel::getDB($criteria->getDbName());
		$dbMap = Propel::getDatabaseMap($criteria->getDbName());

		$fromClause = array();
		$joinClause = array();
		$joinTables = array();
		$whereClause = array();
		$orderByClause = array();

		$orderBy = $criteria->getOrderByColumns();
		$groupBy = $criteria->getGroupByColumns();
		$ignoreCase = $criteria->isIgnoreCase();

		// get the first part of the SQL statement, the SELECT part
		$selectSql = self::createSelectSqlPart($criteria, $fromClause);

		// add the criteria to WHERE clause
		// this will also add the table names to the FROM clause if they are not already
		// included via a LEFT JOIN
		foreach ($criteria->keys() as $key) {

			$criterion = $criteria->getCriterion($key);
			$table = null;
			foreach ($criterion->getAttachedCriterion() as $attachedCriterion) {
				$tableName = $attachedCriterion->getTable();

				$table = $criteria->getTableForAlias($tableName);
				if ($table !== null) {
					$fromClause[] = $table . ' ' . $tableName;
				} else {
					$fromClause[] = $tableName;
					$table = $tableName;
				}

				if (($criteria->isIgnoreCase() || $attachedCriterion->isIgnoreCase())
				&& $dbMap->getTable($table)->getColumn($attachedCriterion->getColumn())->isText()) {
					$attachedCriterion->setIgnoreCase(true);
				}
			}

			$criterion->setDB($db);

			$sb = '';
			$criterion->appendPsTo($sb, $params);
			$whereClause[] = $sb;
		}

		// Handle joins
		// joins with a null join type will be added to the FROM clause and the condition added to the WHERE clause.
		// joins of a specified type: the LEFT side will be added to the fromClause and the RIGHT to the joinClause
		foreach ($criteria->getJoins() as $join) { 
			// The join might have been established using an alias name
			$leftTable = $join->getLeftTableName();
			if ($realTable = $criteria->getTableForAlias($leftTable)) {
				$leftTableForFrom = $realTable . ' ' . $leftTable;
				$leftTable = $realTable;
			} else {
				$leftTableForFrom = $leftTable;
			}

			$rightTable = $join->getRightTableName();
			if ($realTable = $criteria->getTableForAlias($rightTable)) {
				$rightTableForFrom =  $realTable . ' ' . $rightTable;
				$rightTable = $realTable;
			} else {
				$rightTableForFrom = $rightTable;
			}

			// determine if casing is relevant.
			if ($ignoreCase = $criteria->isIgnoreCase()) {
				$leftColType = $dbMap->getTable($leftTable)->getColumn($join->getLeftColumnName())->getType();
				$rightColType = $dbMap->getTable($rightTable)->getColumn($join->getRightColumnName())->getType();
				$ignoreCase = ($leftColType == 'string' || $rightColType == 'string');
			}

			// build the condition
			$condition = '';
			foreach ($join->getConditions() as $index => $conditionDesc) {
				if ($ignoreCase) {
					$condition .= $db->ignoreCase($conditionDesc['left']) . $conditionDesc['operator'] . $db->ignoreCase($conditionDesc['right']);
				} else {
					$condition .= implode($conditionDesc);
				}
				if ($index + 1 < $join->countConditions()) {
					$condition .= ' AND ';
				}
			}

			// add 'em to the queues..
			if ($joinType = $join->getJoinType()) {
			  // real join
				if (!$fromClause) {
					$fromClause[] = $leftTableForFrom;
				}
				$joinTables[] = $rightTableForFrom;
				$joinClause[] = $join->getJoinType() . ' ' . $rightTableForFrom . " ON ($condition)";
			} else {
			  // implicit join, translates to a where
				$fromClause[] = $leftTableForFrom;
				$fromClause[] = $rightTableForFrom;
				$whereClause[] = $condition;
			}
		}

		// Unique from clause elements
		$fromClause = array_unique($fromClause);
		$fromClause = array_diff($fromClause, array(''));
		
		// tables should not exist in both the from and join clauses
		if ($joinTables && $fromClause) {
			foreach ($fromClause as $fi => $ftable) {
				if (in_array($ftable, $joinTables)) {
					unset($fromClause[$fi]);
				}
			}
		}

		// Add the GROUP BY columns
		$groupByClause = $groupBy;

		$having = $criteria->getHaving();
		$havingString = null;
		if ($having !== null) {
			$sb = '';
			$having->appendPsTo($sb, $params);
			$havingString = $sb;
		}

		if (!empty($orderBy)) {

			foreach ($orderBy as $orderByColumn) {

				// Add function expression as-is.

				if (strpos($orderByColumn, '(') !== false) {
					$orderByClause[] = $orderByColumn;
					continue;
				}

				// Split orderByColumn (i.e. "table.column DESC")

				$dotPos = strrpos($orderByColumn, '.');

				if ($dotPos !== false) {
					$tableName = substr($orderByColumn, 0, $dotPos);
					$columnName = substr($orderByColumn, $dotPos + 1);
				} else {
					$tableName = '';
					$columnName = $orderByColumn;
				}

				$spacePos = strpos($columnName, ' ');

				if ($spacePos !== false) {
					$direction = substr($columnName, $spacePos);
					$columnName = substr($columnName, 0, $spacePos);
				}	else {
					$direction = '';
				}

				$tableAlias = $tableName;
				if ($aliasTableName = $criteria->getTableForAlias($tableName)) {
					$tableName = $aliasTableName;
				}

				$columnAlias = $columnName;
				if ($asColumnName = $criteria->getColumnForAs($columnName)) {
					$columnName = $asColumnName;
				}

				$column = $tableName ? $dbMap->getTable($tableName)->getColumn($columnName) : null;

				if ($criteria->isIgnoreCase() && $column && $column->isText()) {
					$ignoreCaseColumn = $db->ignoreCaseInOrderBy("$tableAlias.$columnAlias");
					$orderByClause[] =  $ignoreCaseColumn . $direction;
					$selectSql .= ', ' . $ignoreCaseColumn;
				} else {
					$orderByClause[] = $orderByColumn;
				}
			}
		}

		if (empty($fromClause) && $criteria->getPrimaryTableName()) {
			$fromClause[] = $criteria->getPrimaryTableName();
		}

		// from / join tables quoted if it is necessary
		if ($db->useQuoteIdentifier()) {
			$fromClause = array_map(array($db, 'quoteIdentifierTable'), $fromClause);
			$joinClause = $joinClause ? $joinClause : array_map(array($db, 'quoteIdentifierTable'), $joinClause);
		}

		// build from-clause
		$from = '';
		if (!empty($joinClause) && count($fromClause) > 1) {
			$from .= implode(" CROSS JOIN ", $fromClause);
		} else {
			$from .= implode(", ", $fromClause);
		}
		
		$from .= $joinClause ? ' ' . implode(' ', $joinClause) : '';

		// Build the SQL from the arrays we compiled
		$sql =  $selectSql
		." FROM "  . $from
		.($whereClause ? " WHERE ".implode(" AND ", $whereClause) : "")
		.($groupByClause ? " GROUP BY ".implode(",", $groupByClause) : "")
		.($havingString ? " HAVING ".$havingString : "")
		.($orderByClause ? " ORDER BY ".implode(",", $orderByClause) : "");

		// APPLY OFFSET & LIMIT to the query.
		if ($criteria->getLimit() || $criteria->getOffset()) {
			$db->applyLimit($sql, $criteria->getOffset(), $criteria->getLimit(), $criteria);
		}

		return $sql;
	}

	/**
	 * Builds the SELECT part of a SQL statement based on a Criteria
	 * taking into account select columns and 'as' columns (i.e. columns aliases)
	 */
	public static function createSelectSqlPart(Criteria $criteria, &$fromClause, $aliasAll = false)
	{
		$selectClause = array();
		
		if ($aliasAll) {
			self::turnSelectColumnsToAliases($criteria);
			// no select columns after that, they are all aliases
		} else {
			foreach ($criteria->getSelectColumns() as $columnName) {

				// expect every column to be of "table.column" formation
				// it could be a function:  e.g. MAX(books.price)

				$tableName = null;

				$selectClause[] = $columnName; // the full column name: e.g. MAX(books.price)

				$parenPos = strrpos($columnName, '(');
				$dotPos = strrpos($columnName, '.', ($parenPos !== false ? $parenPos : 0));

				if ($dotPos !== false) {
					if ($parenPos === false) { // table.column
						$tableName = substr($columnName, 0, $dotPos);
					} else { // FUNC(table.column)
						// functions may contain qualifiers so only take the last
						// word as the table name.
						// COUNT(DISTINCT books.price)
						$lastSpace = strpos($tableName, ' ');
						if ($lastSpace !== false) { // COUNT(DISTINCT books.price)
							$tableName = substr($tableName, $lastSpace + 1);
						} else {
							$tableName = substr($columnName, $parenPos + 1, $dotPos - ($parenPos + 1));
						}
					}
					// is it a table alias?
					$tableName2 = $criteria->getTableForAlias($tableName);
					if ($tableName2 !== null) {
						$fromClause[] = $tableName2 . ' ' . $tableName;
					} else {
						$fromClause[] = $tableName;
					}
				} // if $dotPost !== false
			}
		}
		
		// set the aliases
		foreach ($criteria->getAsColumns() as $alias => $col) {
			$selectClause[] = $col . ' AS ' . $alias;
		}

		$selectModifiers = $criteria->getSelectModifiers();
		$queryComment = $criteria->getComment();
		
		// Build the SQL from the arrays we compiled
		$sql =  "SELECT " 
		. ($queryComment ? '/* ' . $queryComment . ' */ ' : '')
		. ($selectModifiers ? (implode(' ', $selectModifiers) . ' ') : '')
		. implode(", ", $selectClause);

		return $sql;
	}

	/**
	 * Builds a params array, like the kind populated by Criterion::appendPsTo().
	 * This is useful for building an array even when it is not using the appendPsTo() method.
	 * @param      array $columns
	 * @param      Criteria $values
	 * @return     array params array('column' => ..., 'table' => ..., 'value' => ...)
	 */
	private static function buildParams($columns, Criteria $values)
	{
		$params = array();
		foreach ($columns as $key) {
			if ($values->containsKey($key)) {
				$crit = $values->getCriterion($key);
				$params[] = array('column' => $crit->getColumn(), 'table' => $crit->getTable(), 'value' => $crit->getValue());
			}
		}
		return $params;
	}

	/**
	 * This function searches for the given validator $name under propel/validator/$name.php,
	 * imports and caches it.
	 *
	 * @param      string $classname The dot-path name of class (e.g. myapp.propel.MyValidator)
	 * @return     Validator object or null if not able to instantiate validator class (and error will be logged in this case)
	 */
	public static function getValidator($classname)
	{
		try {
			$v = isset(self::$validatorMap[$classname]) ? self::$validatorMap[$classname] : null;
			if ($v === null) {
				$cls = Propel::importClass($classname);
				$v = new $cls();
				self::$validatorMap[$classname] = $v;
			}
			return $v;
		} catch (Exception $e) {
			Propel::log("BasePeer::getValidator(): failed trying to instantiate " . $classname . ": ".$e->getMessage(), Propel::LOG_ERR);
		}
	}

}