<?php
/**
 * This file contains the TDbCommand class.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright Copyright &copy; 2008-2009 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * TDbCommand represents an SQL statement to execute against a database.
 *
 * It is usually created by calling {@link TDbConnection::createCommand}.
 * The SQL statement to be executed may be set via {@link setText Text}.
 *
 * To execute a non-query SQL (such as insert, delete, update), call
 * {@link execute}. To execute an SQL statement that returns result data set
 * (such as SELECT), use {@link query} or its convenient versions {@link queryRow},
 * {@link queryColumn}, or {@link queryScalar}.
 *
 * If an SQL statement returns results (such as a SELECT SQL), the results
 * can be accessed via the returned {@link TDbDataReader}.
 *
 * TDbCommand supports SQL statment preparation and parameter binding.
 * Call {@link bindParam} to bind a PHP variable to a parameter in SQL.
 * Call {@link bindValue} to bind a value to an SQL parameter.
 * When binding a parameter, the SQL statement is automatically prepared.
 * You may also call {@link prepare} to explicitly prepare an SQL statement.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id$
 * @package System.Testing.Data
 * @since 1.0
 */
class TDbCommand extends TComponent
{
	private $_connection;
	private $_text='';
	private $_statement=null;
	private $_params;

	/**
	 * Constructor.
	 * @param TDbConnection the database connection
	 * @param string the SQL statement to be executed
	 */
	public function __construct(TDbConnection $connection,$text)
	{
		$this->_connection=$connection;
		$this->setText($text);
	}

	/**
	 * Set the statement to null when serializing.
	 */
	public function __sleep()
	{
		$this->_statement=null;
		return array_keys(get_object_vars($this));
	}

	/**
	 * @return string the SQL statement to be executed
	 */
	public function getText()
	{
		return $this->_text;
	}

	/**
	 * Specifies the SQL statement to be executed.
	 * Any previous execution will be terminated or cancel.
	 * @param string the SQL statement to be executed
	 */
	public function setText($value)
	{
		$this->_text=$value;
		$this->cancel();
	}

	/**
	 * @return TDbConnection the connection associated with this command
	 */
	public function getConnection()
	{
		return $this->_connection;
	}

	/**
	 * @return PDOStatement the underlying PDOStatement for this command
	 * It could be null if the statement is not prepared yet.
	 */
	public function getPdoStatement()
	{
		return $this->_statement;
	}

	/**
	 * Prepares the SQL statement to be executed.
	 * For complex SQL statement that is to be executed multiple times,
	 * this may improve performance.
	 * For SQL statement with binding parameters, this method is invoked
	 * automatically.
	 */
	public function prepare()
	{
		if($this->_statement==null)
		{
			try
			{
				$this->_statement=$this->getConnection()->getPdoInstance()->prepare($this->getText());
				$this->_params=array();
			}
			catch(Exception $e)
			{
				Prado::log('Error in preparing SQL: '.$this->getText(),TLogger::ERROR,'System.Testing.Data.TDbCommand');
				throw new TDbException('TDbCommand failed to prepare the SQL statement: {0}',
					$e->getMessage());
			}
		}
	}

	/**
	 * Cancels the execution of the SQL statement.
	 */
	public function cancel()
	{
		$this->_statement=null;
	}

	/**
	 * Binds a parameter to the SQL statement to be executed.
	 * @param mixed Parameter identifier. For a prepared statement
	 * using named placeholders, this will be a parameter name of
	 * the form :name. For a prepared statement using question mark
	 * placeholders, this will be the 1-indexed position of the parameter.
	 * @param mixed Name of the PHP variable to bind to the SQL statement parameter
	 * @param int SQL data type of the parameter. If null, the type is determined by the PHP type of the value.
	 * @param int length of the data type
	 * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php
	 */
	public function bindParameter($name, &$value, $dataType=null, $length=null)
	{
		$this->prepare();
		if($dataType===null)
			$this->_statement->bindParam($name,$value,$this->_connection->getPdoType(gettype($value)));
		else if($length===null)
			$this->_statement->bindParam($name,$value,$dataType);
		else
			$this->_statement->bindParam($name,$value,$dataType,$length);
		if($this->_connection->enableParamLogging)
			$this->_params[]=$name.'=['.gettype($value).']';
	}


	/**
	 * Binds a value to a parameter.
	 * @param mixed Parameter identifier. For a prepared statement
	 * using named placeholders, this will be a parameter name of
	 * the form :name. For a prepared statement using question mark
	 * placeholders, this will be the 1-indexed position of the parameter.
	 * @param mixed The value to bind to the parameter
	 * @param int SQL data type of the parameter. If null, the type is determined by the PHP type of the value.
	 * @see http://www.php.net/manual/en/function.PDOStatement-bindValue.php
	 */
	public function bindValue($name, $value, $dataType=null)
	{
		$this->prepare();
		if($dataType===null)
			$this->_statement->bindValue($name,$value,$this->_connection->getPdoType(gettype($value)));
		else
			$this->_statement->bindValue($name,$value,$dataType);
		if($this->_connection->enableParamLogging)
			$this->_params[]=$name.'='.var_export($value,true);
	}

	/**
	 * Executes the SQL statement.
	 * This method is meant only for executing non-query SQL statement.
	 * No result set will be returned.
	 * @return integer number of rows affected by the execution.
	 * @throws CException execution failed
	 */
	public function execute()
	{
		$params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : '';
		Prado::trace('Executing SQL: '.$this->getText().$params,'System.Testing.Data.TDbCommand');
		try
		{
			/*
			if($this->_connection->enableProfiling)
				Prado::beginProfile('System.Testing.Data.TDbCommand.execute('.$this->getText().')','System.Testing.Data.TDbCommand.execute');
			*/
			if($this->_statement instanceof PDOStatement)
			{
				$this->_statement->execute();
				$n=$this->_statement->rowCount();
			}
			else
				$n=$this->getConnection()->getPdoInstance()->exec($this->getText());
			/*
			if($this->_connection->enableProfiling)
				Prado::endProfile('System.Testing.Data.TDbCommand.execute('.$this->getText().')','System.Testing.Data.TDbCommand.execute');
			*/
			return $n;
		}
		catch(Exception $e)
		{
			/*
			if($this->_connection->enableProfiling)
				Prado::endProfile('System.Testing.Data.TDbCommand.execute('.$this->getText().')','System.Testing.Data.TDbCommand.execute');
			*/
			Prado::log('Error in executing SQL: '.$this->getText().$params,TLogger::ERROR,'System.Testing.Data.TDbCommand');
			throw new TDbException('TDbCommand failed to execute the SQL statement: {0}',
				$e->getMessage());
		}
	}

	/**
	 * @return String prepared SQL text for debugging purposes.
	 */
	public function getDebugStatementText()
	{
		if(Prado::getApplication()->getMode() === TApplicationMode::Debug)
		{
			$params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : '';
			return $this->_statement instanceof PDOStatement ?
				$this->_statement->queryString.$params
				: $this->getText().$params;

		}
	}

	/**
	 * Executes the SQL statement and returns query result.
	 * This method is for executing an SQL query that returns result set.
	 * @return TDbDataReader the reader object for fetching the query result
	 * @throws CException execution failed
	 */
	public function query()
	{
		return $this->queryInternal('',0);
	}

	/**
	 * Executes the SQL statement and returns all rows.
	 * @param boolean whether each row should be returned as an associated array with
	 * column names as the keys or the array keys are column indexes (0-based).
	 * @return array all rows of the query result. Each array element is an array representing a row.
	 * An empty array is returned if the query results in nothing.
	 * @throws CException execution failed
	 */
	public function queryAll($fetchAssociative=true)
	{
		return $this->queryInternal('fetchAll',$fetchAssociative ? PDO::FETCH_ASSOC : PDO::FETCH_NUM);
	}

	/**
	 * Executes the SQL statement and returns the first row of the result.
	 * This is a convenient method of {@link query} when only the first row of data is needed.
	 * @param boolean whether the row should be returned as an associated array with
	 * column names as the keys or the array keys are column indexes (0-based).
	 * @return array the first row of the query result, false if no result.
	 * @throws CException execution failed
	 */
	public function queryRow($fetchAssociative=true)
	{
		return $this->queryInternal('fetch',$fetchAssociative ? PDO::FETCH_ASSOC : PDO::FETCH_NUM);
	}

	/**
	 * Executes the SQL statement and returns the value of the first column in the first row of data.
	 * This is a convenient method of {@link query} when only a single scalar
	 * value is needed (e.g. obtaining the count of the records).
	 * @return mixed the value of the first column in the first row of the query result. False is returned if there is no value.
	 * @throws CException execution failed
	 */
	public function queryScalar()
	{
		$result=$this->queryInternal('fetchColumn',0);
		if(is_resource($result) && get_resource_type($result)==='stream')
			return stream_get_contents($result);
		else
			return $result;
	}

	/**
	 * Executes the SQL statement and returns the first column of the result.
	 * This is a convenient method of {@link query} when only the first column of data is needed.
	 * Note, the column returned will contain the first element in each row of result.
	 * @return array the first column of the query result. Empty array if no result.
	 * @throws CException execution failed
	 */
	public function queryColumn()
	{
		return $this->queryInternal('fetchAll',PDO::FETCH_COLUMN);
	}

	/**
	 * @param string method of PDOStatement to be called
	 * @param mixed the first parameter to be passed to the method
	 * @return mixed the method execution result
	 */
	private function queryInternal($method,$mode)
	{
		$params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : '';
		Prado::trace('Querying SQL: '.$this->getText().$params,'System.Testing.Data.TDbCommand');
		try
		{
			/*
			if($this->_connection->enableProfiling)
				Prado::beginProfile('System.Testing.Data.TDbCommand.query('.$this->getText().')','System.Testing.Data.TDbCommand.query');
			*/
			if($this->_statement instanceof PDOStatement)
				$this->_statement->execute();
			else
				$this->_statement=$this->getConnection()->getPdoInstance()->query($this->getText());
			
			if($method==='')
				$result=new TDbDataReader($this);
			else
			{
				$result=$this->_statement->{$method}($mode);
				$this->_statement->closeCursor();
			}
			/*
			if($this->_connection->enableProfiling)
				Prado::endProfile('System.Testing.Data.TDbCommand.query('.$this->getText().')','System.Testing.Data.TDbCommand.query');
			*/
			return $result;
		}
		catch(Exception $e)
		{
			/*
			if($this->_connection->enableProfiling)
				prado::endProfile('System.Testing.Data.TDbCommand.query('.$this->getText().')','System.Testing.Data.TDbCommand.query');
			*/
			Prado::log('Error in querying SQL: '.$this->getText().$params,TLogger::ERROR,'System.Testing.Data..TDbCommand');
			throw new TDbException('TDbCommand failed to execute the SQL statement: {0}',
				$e->getMessage());
		}
	}
}