diff options
Diffstat (limited to 'framework/db')
33 files changed, 12172 insertions, 0 deletions
diff --git a/framework/db/CDbCommand.php b/framework/db/CDbCommand.php new file mode 100644 index 0000000..caf51d1 --- /dev/null +++ b/framework/db/CDbCommand.php @@ -0,0 +1,1511 @@ +<?php +/** + * This file contains the CDbCommand class. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbCommand represents an SQL statement to execute against a database. + * + * It is usually created by calling {@link CDbConnection::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 CDbDataReader}. + * + * CDbCommand 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. + * + * Starting from version 1.1.6, CDbCommand can also be used as a query builder + * that builds a SQL statement from code fragments. For example, + * <pre> + * $user = Yii::app()->db->createCommand() + * ->select('username, password') + * ->from('tbl_user') + * ->where('id=:id', array(':id'=>1)) + * ->queryRow(); + * </pre> + * + * @property string $text The SQL statement to be executed. + * @property CDbConnection $connection The connection associated with this command. + * @property PDOStatement $pdoStatement The underlying PDOStatement for this command + * It could be null if the statement is not prepared yet. + * @property string $select The SELECT part (without 'SELECT') in the query. + * @property boolean $distinct A value indicating whether SELECT DISTINCT should be used. + * @property string $from The FROM part (without 'FROM' ) in the query. + * @property string $where The WHERE part (without 'WHERE' ) in the query. + * @property mixed $join The join part in the query. This can be an array representing + * multiple join fragments, or a string representing a single jojin fragment. + * Each join fragment will contain the proper join operator (e.g. LEFT JOIN). + * @property string $group The GROUP BY part (without 'GROUP BY' ) in the query. + * @property string $having The HAVING part (without 'HAVING' ) in the query. + * @property string $order The ORDER BY part (without 'ORDER BY' ) in the query. + * @property string $limit The LIMIT part (without 'LIMIT' ) in the query. + * @property string $offset The OFFSET part (without 'OFFSET' ) in the query. + * @property mixed $union The UNION part (without 'UNION' ) in the query. + * This can be either a string or an array representing multiple union parts. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbCommand.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db + * @since 1.0 + */ +class CDbCommand extends CComponent +{ + /** + * @var array the parameters (name=>value) to be bound to the current query. + * @since 1.1.6 + */ + public $params=array(); + + private $_connection; + private $_text; + private $_statement; + private $_paramLog=array(); + private $_query; + private $_fetchMode = array(PDO::FETCH_ASSOC); + + /** + * Constructor. + * @param CDbConnection $connection the database connection + * @param mixed $query the DB query to be executed. This can be either + * a string representing a SQL statement, or an array whose name-value pairs + * will be used to set the corresponding properties of the created command object. + * + * For example, you can pass in either <code>'SELECT * FROM tbl_user'</code> + * or <code>array('select'=>'*', 'from'=>'tbl_user')</code>. They are equivalent + * in terms of the final query result. + * + * When passing the query as an array, the following properties are commonly set: + * {@link select}, {@link distinct}, {@link from}, {@link where}, {@link join}, + * {@link group}, {@link having}, {@link order}, {@link limit}, {@link offset} and + * {@link union}. Please refer to the setter of each of these properties for details + * about valid property values. This feature has been available since version 1.1.6. + * + * Since 1.1.7 it is possible to use a specific mode of data fetching by setting + * {@link setFetchMode FetchMode}. See {@link http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php} + * for more details. + */ + public function __construct(CDbConnection $connection,$query=null) + { + $this->_connection=$connection; + if(is_array($query)) + { + foreach($query as $name=>$value) + $this->$name=$value; + } + else + $this->setText($query); + } + + /** + * Set the statement to null when serializing. + * @return array + */ + public function __sleep() + { + $this->_statement=null; + return array_keys(get_object_vars($this)); + } + + /** + * Set the default fetch mode for this statement + * @param mixed $mode fetch mode + * @return CDbCommand + * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php + * @since 1.1.7 + */ + public function setFetchMode($mode) + { + $params=func_get_args(); + $this->_fetchMode = $params; + return $this; + } + + /** + * Cleans up the command and prepares for building a new query. + * This method is mainly used when a command object is being reused + * multiple times for building different queries. + * Calling this method will clean up all internal states of the command object. + * @return CDbCommand this command instance + * @since 1.1.6 + */ + public function reset() + { + $this->_text=null; + $this->_query=null; + $this->_statement=null; + $this->_paramLog=array(); + $this->params=array(); + return $this; + } + + /** + * @return string the SQL statement to be executed + */ + public function getText() + { + if($this->_text=='' && !empty($this->_query)) + $this->setText($this->buildQuery($this->_query)); + return $this->_text; + } + + /** + * Specifies the SQL statement to be executed. + * Any previous execution will be terminated or cancel. + * @param string $value the SQL statement to be executed + * @return CDbCommand this command instance + */ + public function setText($value) + { + if($this->_connection->tablePrefix!==null && $value!='') + $this->_text=preg_replace('/{{(.*?)}}/',$this->_connection->tablePrefix.'\1',$value); + else + $this->_text=$value; + $this->cancel(); + return $this; + } + + /** + * @return CDbConnection 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->_paramLog=array(); + } + catch(Exception $e) + { + Yii::log('Error in preparing SQL: '.$this->getText(),CLogger::LEVEL_ERROR,'system.db.CDbCommand'); + $errorInfo = $e instanceof PDOException ? $e->errorInfo : null; + throw new CDbException(Yii::t('yii','CDbCommand failed to prepare the SQL statement: {error}', + array('{error}'=>$e->getMessage())),(int)$e->getCode(),$errorInfo); + } + } + } + + /** + * 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 $name 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 $value Name of the PHP variable to bind to the SQL statement parameter + * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @param integer $length length of the data type + * @param mixed $driverOptions the driver-specific options (this is available since version 1.1.6) + * @return CDbCommand the current command being executed + * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php + */ + public function bindParam($name, &$value, $dataType=null, $length=null, $driverOptions=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 if($driverOptions===null) + $this->_statement->bindParam($name,$value,$dataType,$length); + else + $this->_statement->bindParam($name,$value,$dataType,$length,$driverOptions); + $this->_paramLog[$name]=&$value; + return $this; + } + + /** + * Binds a value to a parameter. + * @param mixed $name 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 $value The value to bind to the parameter + * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @return CDbCommand the current command being executed + * @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); + $this->_paramLog[$name]=$value; + return $this; + } + + /** + * Binds a list of values to the corresponding parameters. + * This is similar to {@link bindValue} except that it binds multiple values. + * Note that the SQL data type of each value is determined by its PHP type. + * @param array $values the values to be bound. This must be given in terms of an associative + * array with array keys being the parameter names, and array values the corresponding parameter values. + * For example, <code>array(':name'=>'John', ':age'=>25)</code>. + * @return CDbCommand the current command being executed + * @since 1.1.5 + */ + public function bindValues($values) + { + $this->prepare(); + foreach($values as $name=>$value) + { + $this->_statement->bindValue($name,$value,$this->_connection->getPdoType(gettype($value))); + $this->_paramLog[$name]=$value; + } + return $this; + } + + /** + * Executes the SQL statement. + * This method is meant only for executing non-query SQL statement. + * No result set will be returned. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * @return integer number of rows affected by the execution. + * @throws CException execution failed + */ + public function execute($params=array()) + { + if($this->_connection->enableParamLogging && ($pars=array_merge($this->_paramLog,$params))!==array()) + { + $p=array(); + foreach($pars as $name=>$value) + $p[$name]=$name.'='.var_export($value,true); + $par='. Bound with ' .implode(', ',$p); + } + else + $par=''; + Yii::trace('Executing SQL: '.$this->getText().$par,'system.db.CDbCommand'); + try + { + if($this->_connection->enableProfiling) + Yii::beginProfile('system.db.CDbCommand.execute('.$this->getText().')','system.db.CDbCommand.execute'); + + $this->prepare(); + if($params===array()) + $this->_statement->execute(); + else + $this->_statement->execute($params); + $n=$this->_statement->rowCount(); + + if($this->_connection->enableProfiling) + Yii::endProfile('system.db.CDbCommand.execute('.$this->getText().')','system.db.CDbCommand.execute'); + + return $n; + } + catch(Exception $e) + { + if($this->_connection->enableProfiling) + Yii::endProfile('system.db.CDbCommand.execute('.$this->getText().')','system.db.CDbCommand.execute'); + $errorInfo = $e instanceof PDOException ? $e->errorInfo : null; + $message = $e->getMessage(); + Yii::log(Yii::t('yii','CDbCommand::execute() failed: {error}. The SQL statement executed was: {sql}.', + array('{error}'=>$message, '{sql}'=>$this->getText().$par)),CLogger::LEVEL_ERROR,'system.db.CDbCommand'); + if(YII_DEBUG) + $message .= '. The SQL statement executed was: '.$this->getText().$par; + throw new CDbException(Yii::t('yii','CDbCommand failed to execute the SQL statement: {error}', + array('{error}'=>$message)),(int)$e->getCode(),$errorInfo); + } + } + + /** + * Executes the SQL statement and returns query result. + * This method is for executing an SQL query that returns result set. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * @return CDbDataReader the reader object for fetching the query result + * @throws CException execution failed + */ + public function query($params=array()) + { + return $this->queryInternal('',0,$params); + } + + /** + * Executes the SQL statement and returns all rows. + * @param boolean $fetchAssociative 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). + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * @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,$params=array()) + { + return $this->queryInternal('fetchAll',$fetchAssociative ? $this->_fetchMode : PDO::FETCH_NUM, $params); + } + + /** + * 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 $fetchAssociative 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). + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * @return mixed the first row (in terms of an array) of the query result, false if no result. + * @throws CException execution failed + */ + public function queryRow($fetchAssociative=true,$params=array()) + { + return $this->queryInternal('fetch',$fetchAssociative ? $this->_fetchMode : PDO::FETCH_NUM, $params); + } + + /** + * 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). + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * @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($params=array()) + { + $result=$this->queryInternal('fetchColumn',0,$params); + 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. + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that if you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * @return array the first column of the query result. Empty array if no result. + * @throws CException execution failed + */ + public function queryColumn($params=array()) + { + return $this->queryInternal('fetchAll',PDO::FETCH_COLUMN,$params); + } + + /** + * @param string $method method of PDOStatement to be called + * @param mixed $mode parameters to be passed to the method + * @param array $params input parameters (name=>value) for the SQL execution. This is an alternative + * to {@link bindParam} and {@link bindValue}. If you have multiple input parameters, passing + * them in this way can improve the performance. Note that you pass parameters in this way, + * you cannot bind parameters or values using {@link bindParam} or {@link bindValue}, and vice versa. + * binding methods and the input parameters this way can improve the performance. + * @return mixed the method execution result + */ + private function queryInternal($method,$mode,$params=array()) + { + $params=array_merge($this->params,$params); + + if($this->_connection->enableParamLogging && ($pars=array_merge($this->_paramLog,$params))!==array()) + { + $p=array(); + foreach($pars as $name=>$value) + $p[$name]=$name.'='.var_export($value,true); + $par='. Bound with '.implode(', ',$p); + } + else + $par=''; + + Yii::trace('Querying SQL: '.$this->getText().$par,'system.db.CDbCommand'); + + if($this->_connection->queryCachingCount>0 && $method!=='' + && $this->_connection->queryCachingDuration>0 + && $this->_connection->queryCacheID!==false + && ($cache=Yii::app()->getComponent($this->_connection->queryCacheID))!==null) + { + $this->_connection->queryCachingCount--; + $cacheKey='yii:dbquery'.$this->_connection->connectionString.':'.$this->_connection->username; + $cacheKey.=':'.$this->getText().':'.serialize(array_merge($this->_paramLog,$params)); + if(($result=$cache->get($cacheKey))!==false) + { + Yii::trace('Query result found in cache','system.db.CDbCommand'); + return $result; + } + } + + try + { + if($this->_connection->enableProfiling) + Yii::beginProfile('system.db.CDbCommand.query('.$this->getText().$par.')','system.db.CDbCommand.query'); + + $this->prepare(); + if($params===array()) + $this->_statement->execute(); + else + $this->_statement->execute($params); + + if($method==='') + $result=new CDbDataReader($this); + else + { + $mode=(array)$mode; + $result=call_user_func_array(array($this->_statement, $method), $mode); + $this->_statement->closeCursor(); + } + + if($this->_connection->enableProfiling) + Yii::endProfile('system.db.CDbCommand.query('.$this->getText().$par.')','system.db.CDbCommand.query'); + + if(isset($cache,$cacheKey)) + $cache->set($cacheKey, $result, $this->_connection->queryCachingDuration, $this->_connection->queryCachingDependency); + + return $result; + } + catch(Exception $e) + { + if($this->_connection->enableProfiling) + Yii::endProfile('system.db.CDbCommand.query('.$this->getText().$par.')','system.db.CDbCommand.query'); + $errorInfo = $e instanceof PDOException ? $e->errorInfo : null; + $message = $e->getMessage(); + Yii::log(Yii::t('yii','CDbCommand::{method}() failed: {error}. The SQL statement executed was: {sql}.', + array('{method}'=>$method, '{error}'=>$message, '{sql}'=>$this->getText().$par)),CLogger::LEVEL_ERROR,'system.db.CDbCommand'); + if(YII_DEBUG) + $message .= '. The SQL statement executed was: '.$this->getText().$par; + throw new CDbException(Yii::t('yii','CDbCommand failed to execute the SQL statement: {error}', + array('{error}'=>$message)),(int)$e->getCode(),$errorInfo); + } + } + + /** + * Builds a SQL SELECT statement from the given query specification. + * @param array $query the query specification in name-value pairs. The following + * query options are supported: {@link select}, {@link distinct}, {@link from}, + * {@link where}, {@link join}, {@link group}, {@link having}, {@link order}, + * {@link limit}, {@link offset} and {@link union}. + * @return string the SQL statement + * @since 1.1.6 + */ + public function buildQuery($query) + { + $sql=isset($query['distinct']) && $query['distinct'] ? 'SELECT DISTINCT' : 'SELECT'; + $sql.=' '.(isset($query['select']) ? $query['select'] : '*'); + + if(isset($query['from'])) + $sql.="\nFROM ".$query['from']; + else + throw new CDbException(Yii::t('yii','The DB query must contain the "from" portion.')); + + if(isset($query['join'])) + $sql.="\n".(is_array($query['join']) ? implode("\n",$query['join']) : $query['join']); + + if(isset($query['where'])) + $sql.="\nWHERE ".$query['where']; + + if(isset($query['group'])) + $sql.="\nGROUP BY ".$query['group']; + + if(isset($query['having'])) + $sql.="\nHAVING ".$query['having']; + + if(isset($query['order'])) + $sql.="\nORDER BY ".$query['order']; + + $limit=isset($query['limit']) ? (int)$query['limit'] : -1; + $offset=isset($query['offset']) ? (int)$query['offset'] : -1; + if($limit>=0 || $offset>0) + $sql=$this->_connection->getCommandBuilder()->applyLimit($sql,$limit,$offset); + + if(isset($query['union'])) + $sql.="\nUNION (\n".(is_array($query['union']) ? implode("\n) UNION (\n",$query['union']) : $query['union']) . ')'; + + return $sql; + } + + /** + * Sets the SELECT part of the query. + * @param mixed $columns the columns to be selected. Defaults to '*', meaning all columns. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @param string $option additional option that should be appended to the 'SELECT' keyword. For example, + * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. This parameter is supported since version 1.1.8. + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function select($columns='*', $option='') + { + if(is_string($columns) && strpos($columns,'(')!==false) + $this->_query['select']=$columns; + else + { + if(!is_array($columns)) + $columns=preg_split('/\s*,\s*/',trim($columns),-1,PREG_SPLIT_NO_EMPTY); + + foreach($columns as $i=>$column) + { + if(is_object($column)) + $columns[$i]=(string)$column; + else if(strpos($column,'(')===false) + { + if(preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/',$column,$matches)) + $columns[$i]=$this->_connection->quoteColumnName($matches[1]).' AS '.$this->_connection->quoteColumnName($matches[2]); + else + $columns[$i]=$this->_connection->quoteColumnName($column); + } + } + $this->_query['select']=implode(', ',$columns); + } + if($option!='') + $this->_query['select']=$option.' '.$this->_query['select']; + return $this; + } + + /** + * Returns the SELECT part in the query. + * @return string the SELECT part (without 'SELECT') in the query. + * @since 1.1.6 + */ + public function getSelect() + { + return isset($this->_query['select']) ? $this->_query['select'] : ''; + } + + /** + * Sets the SELECT part in the query. + * @param mixed $value the data to be selected. Please refer to {@link select()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setSelect($value) + { + $this->select($value); + } + + /** + * Sets the SELECT part of the query with the DISTINCT flag turned on. + * This is the same as {@link select} except that the DISTINCT flag is turned on. + * @param mixed $columns the columns to be selected. See {@link select} for more details. + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function selectDistinct($columns='*') + { + $this->_query['distinct']=true; + return $this->select($columns); + } + + /** + * Returns a value indicating whether SELECT DISTINCT should be used. + * @return boolean a value indicating whether SELECT DISTINCT should be used. + * @since 1.1.6 + */ + public function getDistinct() + { + return isset($this->_query['distinct']) ? $this->_query['distinct'] : false; + } + + /** + * Sets a value indicating whether SELECT DISTINCT should be used. + * @param boolean $value a value indicating whether SELECT DISTINCT should be used. + * @since 1.1.6 + */ + public function setDistinct($value) + { + $this->_query['distinct']=$value; + } + + /** + * Sets the FROM part of the query. + * @param mixed $tables the table(s) to be selected from. This can be either a string (e.g. 'tbl_user') + * or an array (e.g. array('tbl_user', 'tbl_profile')) specifying one or several table names. + * Table names can contain schema prefixes (e.g. 'public.tbl_user') and/or table aliases (e.g. 'tbl_user u'). + * The method will automatically quote the table names unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function from($tables) + { + if(is_string($tables) && strpos($tables,'(')!==false) + $this->_query['from']=$tables; + else + { + if(!is_array($tables)) + $tables=preg_split('/\s*,\s*/',trim($tables),-1,PREG_SPLIT_NO_EMPTY); + foreach($tables as $i=>$table) + { + if(strpos($table,'(')===false) + { + if(preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/',$table,$matches)) // with alias + $tables[$i]=$this->_connection->quoteTableName($matches[1]).' '.$this->_connection->quoteTableName($matches[2]); + else + $tables[$i]=$this->_connection->quoteTableName($table); + } + } + $this->_query['from']=implode(', ',$tables); + } + return $this; + } + + /** + * Returns the FROM part in the query. + * @return string the FROM part (without 'FROM' ) in the query. + * @since 1.1.6 + */ + public function getFrom() + { + return isset($this->_query['from']) ? $this->_query['from'] : ''; + } + + /** + * Sets the FROM part in the query. + * @param mixed $value the tables to be selected from. Please refer to {@link from()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setFrom($value) + { + $this->from($value); + } + + /** + * Sets the WHERE part of the query. + * + * The method requires a $conditions parameter, and optionally a $params parameter + * specifying the values to be bound to the query. + * + * The $conditions parameter should be either a string (e.g. 'id=1') or an array. + * If the latter, it must be of the format <code>array(operator, operand1, operand2, ...)</code>, + * where the operator can be one of the followings, and the possible operands depend on the corresponding + * operator: + * <ul> + * <li><code>and</code>: the operands should be concatenated together using AND. For example, + * array('and', 'id=1', 'id=2') will generate 'id=1 AND id=2'. If an operand is an array, + * it will be converted into a string using the same rules described here. For example, + * array('and', 'type=1', array('or', 'id=1', 'id=2')) will generate 'type=1 AND (id=1 OR id=2)'. + * The method will NOT do any quoting or escaping.</li> + * <li><code>or</code>: similar as the <code>and</code> operator except that the operands are concatenated using OR.</li> + * <li><code>in</code>: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * array('in', 'id', array(1,2,3)) will generate 'id IN (1,2,3)'. + * The method will properly quote the column name and escape values in the range.</li> + * <li><code>not in</code>: similar as the <code>in</code> operator except that IN is replaced with NOT IN in the generated condition.</li> + * <li><code>like</code>: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, array('like', 'name', '%tester%') will generate "name LIKE '%tester%'". + * When the value range is given as an array, multiple LIKE predicates will be generated and concatenated using AND. + * For example, array('like', 'name', array('%test%', '%sample%')) will generate + * "name LIKE '%test%' AND name LIKE '%sample%'". + * The method will properly quote the column name and escape values in the range.</li> + * <li><code>not like</code>: similar as the <code>like</code> operator except that LIKE is replaced with NOT LIKE in the generated condition.</li> + * <li><code>or like</code>: similar as the <code>like</code> operator except that OR is used to concatenated the LIKE predicates.</li> + * <li><code>or not like</code>: similar as the <code>not like</code> operator except that OR is used to concatenated the NOT LIKE predicates.</li> + * </ul> + * @param mixed $conditions the conditions that should be put in the WHERE part. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function where($conditions, $params=array()) + { + $this->_query['where']=$this->processConditions($conditions); + foreach($params as $name=>$value) + $this->params[$name]=$value; + return $this; + } + + /** + * Returns the WHERE part in the query. + * @return string the WHERE part (without 'WHERE' ) in the query. + * @since 1.1.6 + */ + public function getWhere() + { + return isset($this->_query['where']) ? $this->_query['where'] : ''; + } + + /** + * Sets the WHERE part in the query. + * @param mixed $value the where part. Please refer to {@link where()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setWhere($value) + { + $this->where($value); + } + + /** + * Appends an INNER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param mixed $conditions the join condition that should appear in the ON part. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function join($table, $conditions, $params=array()) + { + return $this->joinInternal('join', $table, $conditions, $params); + } + + /** + * Returns the join part in the query. + * @return mixed the join part in the query. This can be an array representing + * multiple join fragments, or a string representing a single jojin fragment. + * Each join fragment will contain the proper join operator (e.g. LEFT JOIN). + * @since 1.1.6 + */ + public function getJoin() + { + return isset($this->_query['join']) ? $this->_query['join'] : ''; + } + + /** + * Sets the join part in the query. + * @param mixed $value the join part in the query. This can be either a string or + * an array representing multiple join parts in the query. Each part must contain + * the proper join operator (e.g. 'LEFT JOIN tbl_profile ON tbl_user.id=tbl_profile.id') + * @since 1.1.6 + */ + public function setJoin($value) + { + $this->_query['join']=$value; + } + + /** + * Appends a LEFT OUTER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param mixed $conditions the join condition that should appear in the ON part. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function leftJoin($table, $conditions, $params=array()) + { + return $this->joinInternal('left join', $table, $conditions, $params); + } + + /** + * Appends a RIGHT OUTER JOIN part to the query. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param mixed $conditions the join condition that should appear in the ON part. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function rightJoin($table, $conditions, $params=array()) + { + return $this->joinInternal('right join', $table, $conditions, $params); + } + + /** + * Appends a CROSS JOIN part to the query. + * Note that not all DBMS support CROSS JOIN. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function crossJoin($table) + { + return $this->joinInternal('cross join', $table); + } + + /** + * Appends a NATURAL JOIN part to the query. + * Note that not all DBMS support NATURAL JOIN. + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function naturalJoin($table) + { + return $this->joinInternal('natural join', $table); + } + + /** + * Sets the GROUP BY part of the query. + * @param mixed $columns the columns to be grouped by. + * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function group($columns) + { + if(is_string($columns) && strpos($columns,'(')!==false) + $this->_query['group']=$columns; + else + { + if(!is_array($columns)) + $columns=preg_split('/\s*,\s*/',trim($columns),-1,PREG_SPLIT_NO_EMPTY); + foreach($columns as $i=>$column) + { + if(is_object($column)) + $columns[$i]=(string)$column; + else if(strpos($column,'(')===false) + $columns[$i]=$this->_connection->quoteColumnName($column); + } + $this->_query['group']=implode(', ',$columns); + } + return $this; + } + + /** + * Returns the GROUP BY part in the query. + * @return string the GROUP BY part (without 'GROUP BY' ) in the query. + * @since 1.1.6 + */ + public function getGroup() + { + return isset($this->_query['group']) ? $this->_query['group'] : ''; + } + + /** + * Sets the GROUP BY part in the query. + * @param mixed $value the GROUP BY part. Please refer to {@link group()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setGroup($value) + { + $this->group($value); + } + + /** + * Sets the HAVING part of the query. + * @param mixed $conditions the conditions to be put after HAVING. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function having($conditions, $params=array()) + { + $this->_query['having']=$this->processConditions($conditions); + foreach($params as $name=>$value) + $this->params[$name]=$value; + return $this; + } + + /** + * Returns the HAVING part in the query. + * @return string the HAVING part (without 'HAVING' ) in the query. + * @since 1.1.6 + */ + public function getHaving() + { + return isset($this->_query['having']) ? $this->_query['having'] : ''; + } + + /** + * Sets the HAVING part in the query. + * @param mixed $value the HAVING part. Please refer to {@link having()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setHaving($value) + { + $this->having($value); + } + + /** + * Sets the ORDER BY part of the query. + * @param mixed $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array (e.g. array('id ASC', 'name DESC')). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function order($columns) + { + if(is_string($columns) && strpos($columns,'(')!==false) + $this->_query['order']=$columns; + else + { + if(!is_array($columns)) + $columns=preg_split('/\s*,\s*/',trim($columns),-1,PREG_SPLIT_NO_EMPTY); + foreach($columns as $i=>$column) + { + if(is_object($column)) + $columns[$i]=(string)$column; + else if(strpos($column,'(')===false) + { + if(preg_match('/^(.*?)\s+(asc|desc)$/i',$column,$matches)) + $columns[$i]=$this->_connection->quoteColumnName($matches[1]).' '.strtoupper($matches[2]); + else + $columns[$i]=$this->_connection->quoteColumnName($column); + } + } + $this->_query['order']=implode(', ',$columns); + } + return $this; + } + + /** + * Returns the ORDER BY part in the query. + * @return string the ORDER BY part (without 'ORDER BY' ) in the query. + * @since 1.1.6 + */ + public function getOrder() + { + return isset($this->_query['order']) ? $this->_query['order'] : ''; + } + + /** + * Sets the ORDER BY part in the query. + * @param mixed $value the ORDER BY part. Please refer to {@link order()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setOrder($value) + { + $this->order($value); + } + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit + * @param integer $offset the offset + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function limit($limit, $offset=null) + { + $this->_query['limit']=(int)$limit; + if($offset!==null) + $this->offset($offset); + return $this; + } + + /** + * Returns the LIMIT part in the query. + * @return string the LIMIT part (without 'LIMIT' ) in the query. + * @since 1.1.6 + */ + public function getLimit() + { + return isset($this->_query['limit']) ? $this->_query['limit'] : -1; + } + + /** + * Sets the LIMIT part in the query. + * @param integer $value the LIMIT part. Please refer to {@link limit()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setLimit($value) + { + $this->limit($value); + } + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function offset($offset) + { + $this->_query['offset']=(int)$offset; + return $this; + } + + /** + * Returns the OFFSET part in the query. + * @return string the OFFSET part (without 'OFFSET' ) in the query. + * @since 1.1.6 + */ + public function getOffset() + { + return isset($this->_query['offset']) ? $this->_query['offset'] : -1; + } + + /** + * Sets the OFFSET part in the query. + * @param integer $value the OFFSET part. Please refer to {@link offset()} for details + * on how to specify this parameter. + * @since 1.1.6 + */ + public function setOffset($value) + { + $this->offset($value); + } + + /** + * Appends a SQL statement using UNION operator. + * @param string $sql the SQL statement to be appended using UNION + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + public function union($sql) + { + if(isset($this->_query['union']) && is_string($this->_query['union'])) + $this->_query['union']=array($this->_query['union']); + + $this->_query['union'][]=$sql; + + return $this; + } + + /** + * Returns the UNION part in the query. + * @return mixed the UNION part (without 'UNION' ) in the query. + * This can be either a string or an array representing multiple union parts. + * @since 1.1.6 + */ + public function getUnion() + { + return isset($this->_query['union']) ? $this->_query['union'] : ''; + } + + /** + * Sets the UNION part in the query. + * @param mixed $value the UNION part. This can be either a string or an array + * representing multiple SQL statements to be unioned together. + * @since 1.1.6 + */ + public function setUnion($value) + { + $this->_query['union']=$value; + } + + /** + * Creates and executes an INSERT SQL statement. + * The method will properly escape the column names, and bind the values to be inserted. + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column data (name=>value) to be inserted into the table. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function insert($table, $columns) + { + $params=array(); + $names=array(); + $placeholders=array(); + foreach($columns as $name=>$value) + { + $names[]=$this->_connection->quoteColumnName($name); + if($value instanceof CDbExpression) + { + $placeholders[] = $value->expression; + foreach($value->params as $n => $v) + $params[$n] = $v; + } + else + { + $placeholders[] = ':' . $name; + $params[':' . $name] = $value; + } + } + $sql='INSERT INTO ' . $this->_connection->quoteTableName($table) + . ' (' . implode(', ',$names) . ') VALUES (' + . implode(', ', $placeholders) . ')'; + return $this->setText($sql)->execute($params); + } + + /** + * Creates and executes an UPDATE SQL statement. + * The method will properly escape the column names and bind the values to be updated. + * @param string $table the table to be updated. + * @param array $columns the column data (name=>value) to be updated. + * @param mixed $conditions the conditions that will be put in the WHERE part. Please + * refer to {@link where} on how to specify conditions. + * @param array $params the parameters to be bound to the query. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function update($table, $columns, $conditions='', $params=array()) + { + $lines=array(); + foreach($columns as $name=>$value) + { + if($value instanceof CDbExpression) + { + $lines[]=$this->_connection->quoteColumnName($name) . '=' . $value->expression; + foreach($value->params as $n => $v) + $params[$n] = $v; + } + else + { + $lines[]=$this->_connection->quoteColumnName($name) . '=:' . $name; + $params[':' . $name]=$value; + } + } + $sql='UPDATE ' . $this->_connection->quoteTableName($table) . ' SET ' . implode(', ', $lines); + if(($where=$this->processConditions($conditions))!='') + $sql.=' WHERE '.$where; + return $this->setText($sql)->execute($params); + } + + /** + * Creates and executes a DELETE SQL statement. + * @param string $table the table where the data will be deleted from. + * @param mixed $conditions the conditions that will be put in the WHERE part. Please + * refer to {@link where} on how to specify conditions. + * @param array $params the parameters to be bound to the query. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function delete($table, $conditions='', $params=array()) + { + $sql='DELETE FROM ' . $this->_connection->quoteTableName($table); + if(($where=$this->processConditions($conditions))!='') + $sql.=' WHERE '.$where; + return $this->setText($sql)->execute($params); + } + + /** + * Builds and executes a SQL statement for creating a new DB table. + * + * The columns in the new table should be specified as name-definition pairs (e.g. 'name'=>'string'), + * where name stands for a column name which will be properly quoted by the method, and definition + * stands for the column type which can contain an abstract DB type. + * The {@link getColumnType} method will be invoked to convert any abstract type into a physical one. + * + * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly + * inserted into the generated SQL. + * + * @param string $table the name of the table to be created. The name will be properly quoted by the method. + * @param array $columns the columns (name=>definition) in the new table. + * @param string $options additional SQL fragment that will be appended to the generated SQL. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function createTable($table, $columns, $options=null) + { + return $this->setText($this->getConnection()->getSchema()->createTable($table, $columns, $options))->execute(); + } + + /** + * Builds and executes a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function renameTable($table, $newName) + { + return $this->setText($this->getConnection()->getSchema()->renameTable($table, $newName))->execute(); + } + + /** + * Builds and executes a SQL statement for dropping a DB table. + * @param string $table the table to be dropped. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function dropTable($table) + { + return $this->setText($this->getConnection()->getSchema()->dropTable($table))->execute(); + } + + /** + * Builds and executes a SQL statement for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function truncateTable($table) + { + $schema=$this->getConnection()->getSchema(); + $n=$this->setText($schema->truncateTable($table))->execute(); + if(strncasecmp($this->getConnection()->getDriverName(),'sqlite',6)===0) + $schema->resetSequence($schema->getTable($table)); + return $n; + } + + /** + * Builds and executes a SQL statement for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function addColumn($table, $column, $type) + { + return $this->setText($this->getConnection()->getSchema()->addColumn($table, $column, $type))->execute(); + } + + /** + * Builds and executes a SQL statement for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function dropColumn($table, $column) + { + return $this->setText($this->getConnection()->getSchema()->dropColumn($table, $column))->execute(); + } + + /** + * Builds and executes a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function renameColumn($table, $name, $newName) + { + return $this->setText($this->getConnection()->getSchema()->renameColumn($table, $name, $newName))->execute(); + } + + /** + * Builds and executes a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function alterColumn($table, $column, $type) + { + return $this->setText($this->getConnection()->getSchema()->alterColumn($table, $column, $type))->execute(); + } + + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas. + * @param string $refTable the table that the foreign key references to. + * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete=null, $update=null) + { + return $this->setText($this->getConnection()->getSchema()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update))->execute(); + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function dropForeignKey($name, $table) + { + return $this->setText($this->getConnection()->getSchema()->dropForeignKey($name, $table))->execute(); + } + + /** + * Builds and executes a SQL statement for creating a new index. + * @param string $name the name of the index. The name will be properly quoted by the method. + * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. + * @param string $column the column(s) that should be included in the index. If there are multiple columns, please separate them + * by commas. The column names will be properly quoted by the method. + * @param boolean $unique whether to add UNIQUE constraint on the created index. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function createIndex($name, $table, $column, $unique=false) + { + return $this->setText($this->getConnection()->getSchema()->createIndex($name, $table, $column, $unique))->execute(); + } + + /** + * Builds and executes a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return integer number of rows affected by the execution. + * @since 1.1.6 + */ + public function dropIndex($name, $table) + { + return $this->setText($this->getConnection()->getSchema()->dropIndex($name, $table))->execute(); + } + + /** + * Generates the condition string that will be put in the WHERE part + * @param mixed $conditions the conditions that will be put in the WHERE part. + * @return string the condition string to put in the WHERE part + */ + private function processConditions($conditions) + { + if(!is_array($conditions)) + return $conditions; + else if($conditions===array()) + return ''; + $n=count($conditions); + $operator=strtoupper($conditions[0]); + if($operator==='OR' || $operator==='AND') + { + $parts=array(); + for($i=1;$i<$n;++$i) + { + $condition=$this->processConditions($conditions[$i]); + if($condition!=='') + $parts[]='('.$condition.')'; + } + return $parts===array() ? '' : implode(' '.$operator.' ', $parts); + } + + if(!isset($conditions[1],$conditions[2])) + return ''; + + $column=$conditions[1]; + if(strpos($column,'(')===false) + $column=$this->_connection->quoteColumnName($column); + + $values=$conditions[2]; + if(!is_array($values)) + $values=array($values); + + if($operator==='IN' || $operator==='NOT IN') + { + if($values===array()) + return $operator==='IN' ? '0=1' : ''; + foreach($values as $i=>$value) + { + if(is_string($value)) + $values[$i]=$this->_connection->quoteValue($value); + else + $values[$i]=(string)$value; + } + return $column.' '.$operator.' ('.implode(', ',$values).')'; + } + + if($operator==='LIKE' || $operator==='NOT LIKE' || $operator==='OR LIKE' || $operator==='OR NOT LIKE') + { + if($values===array()) + return $operator==='LIKE' || $operator==='OR LIKE' ? '0=1' : ''; + + if($operator==='LIKE' || $operator==='NOT LIKE') + $andor=' AND '; + else + { + $andor=' OR '; + $operator=$operator==='OR LIKE' ? 'LIKE' : 'NOT LIKE'; + } + $expressions=array(); + foreach($values as $value) + $expressions[]=$column.' '.$operator.' '.$this->_connection->quoteValue($value); + return implode($andor,$expressions); + } + + throw new CDbException(Yii::t('yii', 'Unknown operator "{operator}".', array('{operator}'=>$operator))); + } + + /** + * Appends an JOIN part to the query. + * @param string $type the join type ('join', 'left join', 'right join', 'cross join', 'natural join') + * @param string $table the table to be joined. + * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). + * The method will automatically quote the table name unless it contains some parenthesis + * (which means the table is given as a sub-query or DB expression). + * @param mixed $conditions the join condition that should appear in the ON part. + * Please refer to {@link where} on how to specify conditions. + * @param array $params the parameters (name=>value) to be bound to the query + * @return CDbCommand the command object itself + * @since 1.1.6 + */ + private function joinInternal($type, $table, $conditions='', $params=array()) + { + if(strpos($table,'(')===false) + { + if(preg_match('/^(.*?)(?i:\s+as\s+|\s+)(.*)$/',$table,$matches)) // with alias + $table=$this->_connection->quoteTableName($matches[1]).' '.$this->_connection->quoteTableName($matches[2]); + else + $table=$this->_connection->quoteTableName($table); + } + + $conditions=$this->processConditions($conditions); + if($conditions!='') + $conditions=' ON '.$conditions; + + if(isset($this->_query['join']) && is_string($this->_query['join'])) + $this->_query['join']=array($this->_query['join']); + + $this->_query['join'][]=strtoupper($type) . ' ' . $table . $conditions; + + foreach($params as $name=>$value) + $this->params[$name]=$value; + return $this; + } +} diff --git a/framework/db/CDbConnection.php b/framework/db/CDbConnection.php new file mode 100644 index 0000000..86b6733 --- /dev/null +++ b/framework/db/CDbConnection.php @@ -0,0 +1,809 @@ +<?php +/** + * CDbConnection class file + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbConnection represents a connection to a database. + * + * CDbConnection works together with {@link CDbCommand}, {@link CDbDataReader} + * and {@link CDbTransaction} to provide data access to various DBMS + * in a common set of APIs. They are a thin wrapper of the {@link http://www.php.net/manual/en/ref.pdo.php PDO} + * PHP extension. + * + * To establish a connection, set {@link setActive active} to true after + * specifying {@link connectionString}, {@link username} and {@link password}. + * + * The following example shows how to create a CDbConnection instance and establish + * the actual connection: + * <pre> + * $connection=new CDbConnection($dsn,$username,$password); + * $connection->active=true; + * </pre> + * + * After the DB connection is established, one can execute an SQL statement like the following: + * <pre> + * $command=$connection->createCommand($sqlStatement); + * $command->execute(); // a non-query SQL statement execution + * // or execute an SQL query and fetch the result set + * $reader=$command->query(); + * + * // each $row is an array representing a row of data + * foreach($reader as $row) ... + * </pre> + * + * One can do prepared SQL execution and bind parameters to the prepared SQL: + * <pre> + * $command=$connection->createCommand($sqlStatement); + * $command->bindParam($name1,$value1); + * $command->bindParam($name2,$value2); + * $command->execute(); + * </pre> + * + * To use transaction, do like the following: + * <pre> + * $transaction=$connection->beginTransaction(); + * try + * { + * $connection->createCommand($sql1)->execute(); + * $connection->createCommand($sql2)->execute(); + * //.... other SQL executions + * $transaction->commit(); + * } + * catch(Exception $e) + * { + * $transaction->rollBack(); + * } + * </pre> + * + * CDbConnection also provides a set of methods to support setting and querying + * of certain DBMS attributes, such as {@link getNullConversion nullConversion}. + * + * Since CDbConnection implements the interface IApplicationComponent, it can + * be used as an application component and be configured in application configuration, + * like the following, + * <pre> + * array( + * 'components'=>array( + * 'db'=>array( + * 'class'=>'CDbConnection', + * 'connectionString'=>'sqlite:path/to/dbfile', + * ), + * ), + * ) + * </pre> + * + * @property boolean $active Whether the DB connection is established. + * @property PDO $pdoInstance The PDO instance, null if the connection is not established yet. + * @property CDbTransaction $currentTransaction The currently active transaction. Null if no active transaction. + * @property CDbSchema $schema The database schema for the current connection. + * @property CDbCommandBuilder $commandBuilder The command builder. + * @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the sequence object. + * @property mixed $columnCase The case of the column names. + * @property mixed $nullConversion How the null and empty strings are converted. + * @property boolean $autoCommit Whether creating or updating a DB record will be automatically committed. + * @property boolean $persistent Whether the connection is persistent or not. + * @property string $driverName Name of the DB driver. + * @property string $clientVersion The version information of the DB driver. + * @property string $connectionStatus The status of the connection. + * @property boolean $prefetch Whether the connection performs data prefetching. + * @property string $serverInfo The information of DBMS server. + * @property string $serverVersion The version information of DBMS server. + * @property integer $timeout Timeout settings for the connection. + * @property array $attributes Attributes (name=>value) that are previously explicitly set for the DB connection. + * @property array $stats The first element indicates the number of SQL statements executed, + * and the second element the total time spent in SQL execution. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbConnection.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db + * @since 1.0 + */ +class CDbConnection extends CApplicationComponent +{ + /** + * @var string The Data Source Name, or DSN, contains the information required to connect to the database. + * @see http://www.php.net/manual/en/function.PDO-construct.php + * + * Note that if you're using GBK or BIG5 then it's highly recommended to + * update to PHP 5.3.6+ and to specify charset via DSN like + * 'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'. + */ + public $connectionString; + /** + * @var string the username for establishing DB connection. Defaults to empty string. + */ + public $username=''; + /** + * @var string the password for establishing DB connection. Defaults to empty string. + */ + public $password=''; + /** + * @var integer number of seconds that table metadata can remain valid in cache. + * Use 0 or negative value to indicate not caching schema. + * If greater than 0 and the primary cache is enabled, the table metadata will be cached. + * @see schemaCachingExclude + */ + public $schemaCachingDuration=0; + /** + * @var array list of tables whose metadata should NOT be cached. Defaults to empty array. + * @see schemaCachingDuration + */ + public $schemaCachingExclude=array(); + /** + * @var string the ID of the cache application component that is used to cache the table metadata. + * Defaults to 'cache' which refers to the primary cache application component. + * Set this property to false if you want to disable caching table metadata. + */ + public $schemaCacheID='cache'; + /** + * @var integer number of seconds that query results can remain valid in cache. + * Use 0 or negative value to indicate not caching query results (the default behavior). + * + * In order to enable query caching, this property must be a positive + * integer and {@link queryCacheID} must point to a valid cache component ID. + * + * The method {@link cache()} is provided as a convenient way of setting this property + * and {@link queryCachingDependency} on the fly. + * + * @see cache + * @see queryCachingDependency + * @see queryCacheID + * @since 1.1.7 + */ + public $queryCachingDuration=0; + /** + * @var CCacheDependency the dependency that will be used when saving query results into cache. + * @see queryCachingDuration + * @since 1.1.7 + */ + public $queryCachingDependency; + /** + * @var integer the number of SQL statements that need to be cached next. + * If this is 0, then even if query caching is enabled, no query will be cached. + * Note that each time after executing a SQL statement (whether executed on DB server or fetched from + * query cache), this property will be reduced by 1 until 0. + * @since 1.1.7 + */ + public $queryCachingCount=0; + /** + * @var string the ID of the cache application component that is used for query caching. + * Defaults to 'cache' which refers to the primary cache application component. + * Set this property to false if you want to disable query caching. + * @since 1.1.7 + */ + public $queryCacheID='cache'; + /** + * @var boolean whether the database connection should be automatically established + * the component is being initialized. Defaults to true. Note, this property is only + * effective when the CDbConnection object is used as an application component. + */ + public $autoConnect=true; + /** + * @var string the charset used for database connection. The property is only used + * for MySQL and PostgreSQL databases. Defaults to null, meaning using default charset + * as specified by the database. + * + * Note that if you're using GBK or BIG5 then it's highly recommended to + * update to PHP 5.3.6+ and to specify charset via DSN like + * 'mysql:dbname=mydatabase;host=127.0.0.1;charset=GBK;'. + */ + public $charset; + /** + * @var boolean whether to turn on prepare emulation. Defaults to false, meaning PDO + * will use the native prepare support if available. For some databases (such as MySQL), + * this may need to be set true so that PDO can emulate the prepare support to bypass + * the buggy native prepare support. Note, this property is only effective for PHP 5.1.3 or above. + * The default value is null, which will not change the ATTR_EMULATE_PREPARES value of PDO. + */ + public $emulatePrepare; + /** + * @var boolean whether to log the values that are bound to a prepare SQL statement. + * Defaults to false. During development, you may consider setting this property to true + * so that parameter values bound to SQL statements are logged for debugging purpose. + * You should be aware that logging parameter values could be expensive and have significant + * impact on the performance of your application. + */ + public $enableParamLogging=false; + /** + * @var boolean whether to enable profiling the SQL statements being executed. + * Defaults to false. This should be mainly enabled and used during development + * to find out the bottleneck of SQL executions. + */ + public $enableProfiling=false; + /** + * @var string the default prefix for table names. Defaults to null, meaning no table prefix. + * By setting this property, any token like '{{tableName}}' in {@link CDbCommand::text} will + * be replaced by 'prefixTableName', where 'prefix' refers to this property value. + * @since 1.1.0 + */ + public $tablePrefix; + /** + * @var array list of SQL statements that should be executed right after the DB connection is established. + * @since 1.1.1 + */ + public $initSQLs; + /** + * @var array mapping between PDO driver and schema class name. + * A schema class can be specified using path alias. + * @since 1.1.6 + */ + public $driverMap=array( + 'pgsql'=>'CPgsqlSchema', // PostgreSQL + 'mysqli'=>'CMysqlSchema', // MySQL + 'mysql'=>'CMysqlSchema', // MySQL + 'sqlite'=>'CSqliteSchema', // sqlite 3 + 'sqlite2'=>'CSqliteSchema', // sqlite 2 + 'mssql'=>'CMssqlSchema', // Mssql driver on windows hosts + 'dblib'=>'CMssqlSchema', // dblib drivers on linux (and maybe others os) hosts + 'sqlsrv'=>'CMssqlSchema', // Mssql + 'oci'=>'COciSchema', // Oracle driver + ); + + /** + * @var string Custom PDO wrapper class. + * @since 1.1.8 + */ + public $pdoClass = 'PDO'; + + private $_attributes=array(); + private $_active=false; + private $_pdo; + private $_transaction; + private $_schema; + + + /** + * Constructor. + * Note, the DB connection is not established when this connection + * instance is created. Set {@link setActive active} property to true + * to establish the connection. + * @param string $dsn The Data Source Name, or DSN, contains the information required to connect to the database. + * @param string $username The user name for the DSN string. + * @param string $password The password for the DSN string. + * @see http://www.php.net/manual/en/function.PDO-construct.php + */ + public function __construct($dsn='',$username='',$password='') + { + $this->connectionString=$dsn; + $this->username=$username; + $this->password=$password; + } + + /** + * Close the connection when serializing. + * @return array + */ + public function __sleep() + { + $this->close(); + return array_keys(get_object_vars($this)); + } + + /** + * Returns a list of available PDO drivers. + * @return array list of available PDO drivers + * @see http://www.php.net/manual/en/function.PDO-getAvailableDrivers.php + */ + public static function getAvailableDrivers() + { + return PDO::getAvailableDrivers(); + } + + /** + * Initializes the component. + * This method is required by {@link IApplicationComponent} and is invoked by application + * when the CDbConnection is used as an application component. + * If you override this method, make sure to call the parent implementation + * so that the component can be marked as initialized. + */ + public function init() + { + parent::init(); + if($this->autoConnect) + $this->setActive(true); + } + + /** + * Returns whether the DB connection is established. + * @return boolean whether the DB connection is established + */ + public function getActive() + { + return $this->_active; + } + + /** + * Open or close the DB connection. + * @param boolean $value whether to open or close DB connection + * @throws CException if connection fails + */ + public function setActive($value) + { + if($value!=$this->_active) + { + if($value) + $this->open(); + else + $this->close(); + } + } + + /** + * Sets the parameters about query caching. + * This method can be used to enable or disable query caching. + * By setting the $duration parameter to be 0, the query caching will be disabled. + * Otherwise, query results of the new SQL statements executed next will be saved in cache + * and remain valid for the specified duration. + * If the same query is executed again, the result may be fetched from cache directly + * without actually executing the SQL statement. + * @param integer $duration the number of seconds that query results may remain valid in cache. + * If this is 0, the caching will be disabled. + * @param CCacheDependency $dependency the dependency that will be used when saving the query results into cache. + * @param integer $queryCount number of SQL queries that need to be cached after calling this method. Defaults to 1, + * meaning that the next SQL query will be cached. + * @return CDbConnection the connection instance itself. + * @since 1.1.7 + */ + public function cache($duration, $dependency=null, $queryCount=1) + { + $this->queryCachingDuration=$duration; + $this->queryCachingDependency=$dependency; + $this->queryCachingCount=$queryCount; + return $this; + } + + /** + * Opens DB connection if it is currently not + * @throws CException if connection fails + */ + protected function open() + { + if($this->_pdo===null) + { + if(empty($this->connectionString)) + throw new CDbException(Yii::t('yii','CDbConnection.connectionString cannot be empty.')); + try + { + Yii::trace('Opening DB connection','system.db.CDbConnection'); + $this->_pdo=$this->createPdoInstance(); + $this->initConnection($this->_pdo); + $this->_active=true; + } + catch(PDOException $e) + { + if(YII_DEBUG) + { + throw new CDbException(Yii::t('yii','CDbConnection failed to open the DB connection: {error}', + array('{error}'=>$e->getMessage())),(int)$e->getCode(),$e->errorInfo); + } + else + { + Yii::log($e->getMessage(),CLogger::LEVEL_ERROR,'exception.CDbException'); + throw new CDbException(Yii::t('yii','CDbConnection failed to open the DB connection.'),(int)$e->getCode(),$e->errorInfo); + } + } + } + } + + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + protected function close() + { + Yii::trace('Closing DB connection','system.db.CDbConnection'); + $this->_pdo=null; + $this->_active=false; + $this->_schema=null; + } + + /** + * Creates the PDO instance. + * When some functionalities are missing in the pdo driver, we may use + * an adapter class to provides them. + * @return PDO the pdo instance + */ + protected function createPdoInstance() + { + $pdoClass=$this->pdoClass; + if(($pos=strpos($this->connectionString,':'))!==false) + { + $driver=strtolower(substr($this->connectionString,0,$pos)); + if($driver==='mssql' || $driver==='dblib' || $driver==='sqlsrv') + $pdoClass='CMssqlPdoAdapter'; + } + return new $pdoClass($this->connectionString,$this->username, + $this->password,$this->_attributes); + } + + /** + * Initializes the open db connection. + * This method is invoked right after the db connection is established. + * The default implementation is to set the charset for MySQL and PostgreSQL database connections. + * @param PDO $pdo the PDO instance + */ + protected function initConnection($pdo) + { + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + if($this->emulatePrepare!==null && constant('PDO::ATTR_EMULATE_PREPARES')) + $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,$this->emulatePrepare); + if($this->charset!==null) + { + $driver=strtolower($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)); + if(in_array($driver,array('pgsql','mysql','mysqli'))) + $pdo->exec('SET NAMES '.$pdo->quote($this->charset)); + } + if($this->initSQLs!==null) + { + foreach($this->initSQLs as $sql) + $pdo->exec($sql); + } + } + + /** + * Returns the PDO instance. + * @return PDO the PDO instance, null if the connection is not established yet + */ + public function getPdoInstance() + { + return $this->_pdo; + } + + /** + * Creates a command for execution. + * @param mixed $query the DB query to be executed. This can be either a string representing a SQL statement, + * or an array representing different fragments of a SQL statement. Please refer to {@link CDbCommand::__construct} + * for more details about how to pass an array as the query. If this parameter is not given, + * you will have to call query builder methods of {@link CDbCommand} to build the DB query. + * @return CDbCommand the DB command + */ + public function createCommand($query=null) + { + $this->setActive(true); + return new CDbCommand($this,$query); + } + + /** + * Returns the currently active transaction. + * @return CDbTransaction the currently active transaction. Null if no active transaction. + */ + public function getCurrentTransaction() + { + if($this->_transaction!==null) + { + if($this->_transaction->getActive()) + return $this->_transaction; + } + return null; + } + + /** + * Starts a transaction. + * @return CDbTransaction the transaction initiated + */ + public function beginTransaction() + { + Yii::trace('Starting transaction','system.db.CDbConnection'); + $this->setActive(true); + $this->_pdo->beginTransaction(); + return $this->_transaction=new CDbTransaction($this); + } + + /** + * Returns the database schema for the current connection + * @return CDbSchema the database schema for the current connection + */ + public function getSchema() + { + if($this->_schema!==null) + return $this->_schema; + else + { + $driver=$this->getDriverName(); + if(isset($this->driverMap[$driver])) + return $this->_schema=Yii::createComponent($this->driverMap[$driver], $this); + else + throw new CDbException(Yii::t('yii','CDbConnection does not support reading schema for {driver} database.', + array('{driver}'=>$driver))); + } + } + + /** + * Returns the SQL command builder for the current DB connection. + * @return CDbCommandBuilder the command builder + */ + public function getCommandBuilder() + { + return $this->getSchema()->getCommandBuilder(); + } + + /** + * Returns the ID of the last inserted row or sequence value. + * @param string $sequenceName name of the sequence object (required by some DBMS) + * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object + * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php + */ + public function getLastInsertID($sequenceName='') + { + $this->setActive(true); + return $this->_pdo->lastInsertId($sequenceName); + } + + /** + * Quotes a string value for use in a query. + * @param string $str string to be quoted + * @return string the properly quoted string + * @see http://www.php.net/manual/en/function.PDO-quote.php + */ + public function quoteValue($str) + { + if(is_int($str) || is_float($str)) + return $str; + + $this->setActive(true); + if(($value=$this->_pdo->quote($str))!==false) + return $value; + else // the driver doesn't support quote (e.g. oci) + return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; + } + + /** + * Quotes a table name for use in a query. + * If the table name contains schema prefix, the prefix will also be properly quoted. + * @param string $name table name + * @return string the properly quoted table name + */ + public function quoteTableName($name) + { + return $this->getSchema()->quoteTableName($name); + } + + /** + * Quotes a column name for use in a query. + * If the column name contains prefix, the prefix will also be properly quoted. + * @param string $name column name + * @return string the properly quoted column name + */ + public function quoteColumnName($name) + { + return $this->getSchema()->quoteColumnName($name); + } + + /** + * Determines the PDO type for the specified PHP type. + * @param string $type The PHP type (obtained by gettype() call). + * @return integer the corresponding PDO type + */ + public function getPdoType($type) + { + static $map=array + ( + 'boolean'=>PDO::PARAM_BOOL, + 'integer'=>PDO::PARAM_INT, + 'string'=>PDO::PARAM_STR, + 'NULL'=>PDO::PARAM_NULL, + ); + return isset($map[$type]) ? $map[$type] : PDO::PARAM_STR; + } + + /** + * Returns the case of the column names + * @return mixed the case of the column names + * @see http://www.php.net/manual/en/pdo.setattribute.php + */ + public function getColumnCase() + { + return $this->getAttribute(PDO::ATTR_CASE); + } + + /** + * Sets the case of the column names. + * @param mixed $value the case of the column names + * @see http://www.php.net/manual/en/pdo.setattribute.php + */ + public function setColumnCase($value) + { + $this->setAttribute(PDO::ATTR_CASE,$value); + } + + /** + * Returns how the null and empty strings are converted. + * @return mixed how the null and empty strings are converted + * @see http://www.php.net/manual/en/pdo.setattribute.php + */ + public function getNullConversion() + { + return $this->getAttribute(PDO::ATTR_ORACLE_NULLS); + } + + /** + * Sets how the null and empty strings are converted. + * @param mixed $value how the null and empty strings are converted + * @see http://www.php.net/manual/en/pdo.setattribute.php + */ + public function setNullConversion($value) + { + $this->setAttribute(PDO::ATTR_ORACLE_NULLS,$value); + } + + /** + * Returns whether creating or updating a DB record will be automatically committed. + * Some DBMS (such as sqlite) may not support this feature. + * @return boolean whether creating or updating a DB record will be automatically committed. + */ + public function getAutoCommit() + { + return $this->getAttribute(PDO::ATTR_AUTOCOMMIT); + } + + /** + * Sets whether creating or updating a DB record will be automatically committed. + * Some DBMS (such as sqlite) may not support this feature. + * @param boolean $value whether creating or updating a DB record will be automatically committed. + */ + public function setAutoCommit($value) + { + $this->setAttribute(PDO::ATTR_AUTOCOMMIT,$value); + } + + /** + * Returns whether the connection is persistent or not. + * Some DBMS (such as sqlite) may not support this feature. + * @return boolean whether the connection is persistent or not + */ + public function getPersistent() + { + return $this->getAttribute(PDO::ATTR_PERSISTENT); + } + + /** + * Sets whether the connection is persistent or not. + * Some DBMS (such as sqlite) may not support this feature. + * @param boolean $value whether the connection is persistent or not + */ + public function setPersistent($value) + { + return $this->setAttribute(PDO::ATTR_PERSISTENT,$value); + } + + /** + * Returns the name of the DB driver + * @return string name of the DB driver + */ + public function getDriverName() + { + if(($pos=strpos($this->connectionString, ':'))!==false) + return strtolower(substr($this->connectionString, 0, $pos)); + // return $this->getAttribute(PDO::ATTR_DRIVER_NAME); + } + + /** + * Returns the version information of the DB driver. + * @return string the version information of the DB driver + */ + public function getClientVersion() + { + return $this->getAttribute(PDO::ATTR_CLIENT_VERSION); + } + + /** + * Returns the status of the connection. + * Some DBMS (such as sqlite) may not support this feature. + * @return string the status of the connection + */ + public function getConnectionStatus() + { + return $this->getAttribute(PDO::ATTR_CONNECTION_STATUS); + } + + /** + * Returns whether the connection performs data prefetching. + * @return boolean whether the connection performs data prefetching + */ + public function getPrefetch() + { + return $this->getAttribute(PDO::ATTR_PREFETCH); + } + + /** + * Returns the information of DBMS server. + * @return string the information of DBMS server + */ + public function getServerInfo() + { + return $this->getAttribute(PDO::ATTR_SERVER_INFO); + } + + /** + * Returns the version information of DBMS server. + * @return string the version information of DBMS server + */ + public function getServerVersion() + { + return $this->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * Returns the timeout settings for the connection. + * @return integer timeout settings for the connection + */ + public function getTimeout() + { + return $this->getAttribute(PDO::ATTR_TIMEOUT); + } + + /** + * Obtains a specific DB connection attribute information. + * @param integer $name the attribute to be queried + * @return mixed the corresponding attribute information + * @see http://www.php.net/manual/en/function.PDO-getAttribute.php + */ + public function getAttribute($name) + { + $this->setActive(true); + return $this->_pdo->getAttribute($name); + } + + /** + * Sets an attribute on the database connection. + * @param integer $name the attribute to be set + * @param mixed $value the attribute value + * @see http://www.php.net/manual/en/function.PDO-setAttribute.php + */ + public function setAttribute($name,$value) + { + if($this->_pdo instanceof PDO) + $this->_pdo->setAttribute($name,$value); + else + $this->_attributes[$name]=$value; + } + + /** + * Returns the attributes that are previously explicitly set for the DB connection. + * @return array attributes (name=>value) that are previously explicitly set for the DB connection. + * @see setAttributes + * @since 1.1.7 + */ + public function getAttributes() + { + return $this->_attributes; + } + + /** + * Sets a set of attributes on the database connection. + * @param array $values attributes (name=>value) to be set. + * @see setAttribute + * @since 1.1.7 + */ + public function setAttributes($values) + { + foreach($values as $name=>$value) + $this->_attributes[$name]=$value; + } + + /** + * Returns the statistical results of SQL executions. + * The results returned include the number of SQL statements executed and + * the total time spent. + * In order to use this method, {@link enableProfiling} has to be set true. + * @return array the first element indicates the number of SQL statements executed, + * and the second element the total time spent in SQL execution. + */ + public function getStats() + { + $logger=Yii::getLogger(); + $timings=$logger->getProfilingResults(null,'system.db.CDbCommand.query'); + $count=count($timings); + $time=array_sum($timings); + $timings=$logger->getProfilingResults(null,'system.db.CDbCommand.execute'); + $count+=count($timings); + $time+=array_sum($timings); + return array($count,$time); + } +} diff --git a/framework/db/CDbDataReader.php b/framework/db/CDbDataReader.php new file mode 100644 index 0000000..9ffc62e --- /dev/null +++ b/framework/db/CDbDataReader.php @@ -0,0 +1,246 @@ +<?php +/** + * CDbDataReader class file + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbDataReader represents a forward-only stream of rows from a query result set. + * + * To read the current row of data, call {@link read}. The method {@link readAll} + * returns all the rows in a single array. + * + * One can also retrieve the rows of data in CDbDataReader by using foreach: + * <pre> + * foreach($reader as $row) + * // $row represents a row of data + * </pre> + * Since CDbDataReader is a forward-only stream, you can only traverse it once. + * + * It is possible to use a specific mode of data fetching by setting + * {@link setFetchMode FetchMode}. See {@link http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php} + * for more details. + * + * @property boolean $isClosed Whether the reader is closed or not. + * @property integer $rowCount Number of rows contained in the result. + * @property integer $columnCount The number of columns in the result set. + * @property mixed $fetchMode Fetch mode. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbDataReader.php 3426 2011-10-25 00:01:09Z alexander.makarow $ + * @package system.db + * @since 1.0 + */ +class CDbDataReader extends CComponent implements Iterator, Countable +{ + private $_statement; + private $_closed=false; + private $_row; + private $_index=-1; + + /** + * Constructor. + * @param CDbCommand $command the command generating the query result + */ + public function __construct(CDbCommand $command) + { + $this->_statement=$command->getPdoStatement(); + $this->_statement->setFetchMode(PDO::FETCH_ASSOC); + } + + /** + * Binds a column to a PHP variable. + * When rows of data are being fetched, the corresponding column value + * will be set in the variable. Note, the fetch mode must include PDO::FETCH_BOUND. + * @param mixed $column Number of the column (1-indexed) or name of the column + * in the result set. If using the column name, be aware that the name + * should match the case of the column, as returned by the driver. + * @param mixed $value Name of the PHP variable to which the column will be bound. + * @param integer $dataType Data type of the parameter + * @see http://www.php.net/manual/en/function.PDOStatement-bindColumn.php + */ + public function bindColumn($column, &$value, $dataType=null) + { + if($dataType===null) + $this->_statement->bindColumn($column,$value); + else + $this->_statement->bindColumn($column,$value,$dataType); + } + + /** + * Set the default fetch mode for this statement + * @param mixed $mode fetch mode + * @see http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php + */ + public function setFetchMode($mode) + { + $params=func_get_args(); + call_user_func_array(array($this->_statement,'setFetchMode'),$params); + } + + /** + * Advances the reader to the next row in a result set. + * @return array|false the current row, false if no more row available + */ + public function read() + { + return $this->_statement->fetch(); + } + + /** + * Returns a single column from the next row of a result set. + * @param integer $columnIndex zero-based column index + * @return mixed|false the column of the current row, false if no more row available + */ + public function readColumn($columnIndex) + { + return $this->_statement->fetchColumn($columnIndex); + } + + /** + * Returns an object populated with the next row of data. + * @param string $className class name of the object to be created and populated + * @param array $fields Elements of this array are passed to the constructor + * @return mixed|false the populated object, false if no more row of data available + */ + public function readObject($className,$fields) + { + return $this->_statement->fetchObject($className,$fields); + } + + /** + * Reads the whole result set into an array. + * @return array the result set (each array element represents a row of data). + * An empty array will be returned if the result contains no row. + */ + public function readAll() + { + return $this->_statement->fetchAll(); + } + + /** + * Advances the reader to the next result when reading the results of a batch of statements. + * This method is only useful when there are multiple result sets + * returned by the query. Not all DBMS support this feature. + * @return boolean Returns true on success or false on failure. + */ + public function nextResult() + { + if(($result=$this->_statement->nextRowset())!==false) + $this->_index=-1; + return $result; + } + + /** + * Closes the reader. + * This frees up the resources allocated for executing this SQL statement. + * Read attemps after this method call are unpredictable. + */ + public function close() + { + $this->_statement->closeCursor(); + $this->_closed=true; + } + + /** + * whether the reader is closed or not. + * @return boolean whether the reader is closed or not. + */ + public function getIsClosed() + { + return $this->_closed; + } + + /** + * Returns the number of rows in the result set. + * Note, most DBMS may not give a meaningful count. + * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. + * @return integer number of rows contained in the result. + */ + public function getRowCount() + { + return $this->_statement->rowCount(); + } + + /** + * Returns the number of rows in the result set. + * This method is required by the Countable interface. + * Note, most DBMS may not give a meaningful count. + * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. + * @return integer number of rows contained in the result. + */ + public function count() + { + return $this->getRowCount(); + } + + /** + * Returns the number of columns in the result set. + * Note, even there's no row in the reader, this still gives correct column number. + * @return integer the number of columns in the result set. + */ + public function getColumnCount() + { + return $this->_statement->columnCount(); + } + + /** + * Resets the iterator to the initial state. + * This method is required by the interface Iterator. + * @throws CException if this method is invoked twice + */ + public function rewind() + { + if($this->_index<0) + { + $this->_row=$this->_statement->fetch(); + $this->_index=0; + } + else + throw new CDbException(Yii::t('yii','CDbDataReader cannot rewind. It is a forward-only reader.')); + } + + /** + * Returns the index of the current row. + * This method is required by the interface Iterator. + * @return integer the index of the current row. + */ + public function key() + { + return $this->_index; + } + + /** + * Returns the current row. + * This method is required by the interface Iterator. + * @return mixed the current row. + */ + public function current() + { + return $this->_row; + } + + /** + * Moves the internal pointer to the next row. + * This method is required by the interface Iterator. + */ + public function next() + { + $this->_row=$this->_statement->fetch(); + $this->_index++; + } + + /** + * Returns whether there is a row of data at current position. + * This method is required by the interface Iterator. + * @return boolean whether there is a row of data at current position. + */ + public function valid() + { + return $this->_row!==false; + } +} diff --git a/framework/db/CDbException.php b/framework/db/CDbException.php new file mode 100644 index 0000000..7b22fb2 --- /dev/null +++ b/framework/db/CDbException.php @@ -0,0 +1,39 @@ +<?php +/** + * CDbException class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbException represents an exception that is caused by some DB-related operations. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbException.php 2799 2011-01-01 19:31:13Z qiang.xue $ + * @package system.db + * @since 1.0 + */ +class CDbException extends CException +{ + /** + * @var mixed the error info provided by a PDO exception. This is the same as returned + * by {@link http://www.php.net/manual/en/pdo.errorinfo.php PDO::errorInfo}. + * @since 1.1.4 + */ + public $errorInfo; + + /** + * Constructor. + * @param string $message PDO error message + * @param integer $code PDO error code + * @param mixed $errorInfo PDO error info + */ + public function __construct($message,$code=0,$errorInfo=null) + { + $this->errorInfo=$errorInfo; + parent::__construct($message,$code); + } +}
\ No newline at end of file diff --git a/framework/db/CDbMigration.php b/framework/db/CDbMigration.php new file mode 100644 index 0000000..4f3d078 --- /dev/null +++ b/framework/db/CDbMigration.php @@ -0,0 +1,402 @@ +<?php +/** + * CDbMigration class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbMigration is the base class for representing a database migration. + * + * CDbMigration is designed to be used together with the "yiic migrate" command. + * + * Each child class of CDbMigration represents an individual database migration which + * is identified by the child class name. + * + * Within each migration, the {@link up} method contains the logic for "upgrading" + * the database used in an application; while the {@link down} method contains "downgrading" + * logic. The "yiic migrate" command manages all available migrations in an application. + * + * CDbMigration provides a set of convenient methods for manipulating database data and schema. + * For example, the {@link insert} method can be used to easily insert a row of data into + * a database table; the {@link createTable} method can be used to create a database table. + * Compared with the same methods in {@link CDbCommand}, these methods will display extra + * information showing the method parameters and execution time, which may be useful when + * applying migrations. + * + * @property CDbConnection $dbConnection The currently active database connection. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbMigration.php 3514 2011-12-27 20:28:26Z alexander.makarow $ + * @package system.db + * @since 1.1.6 + */ +abstract class CDbMigration extends CComponent +{ + private $_db; + + /** + * This method contains the logic to be executed when applying this migration. + * Child classes may implement this method to provide actual migration logic. + * @return boolean + */ + public function up() + { + $transaction=$this->getDbConnection()->beginTransaction(); + try + { + if($this->safeUp()===false) + { + $transaction->rollBack(); + return false; + } + $transaction->commit(); + } + catch(Exception $e) + { + echo "Exception: ".$e->getMessage().' ('.$e->getFile().':'.$e->getLine().")\n"; + echo $e->getTraceAsString()."\n"; + $transaction->rollBack(); + return false; + } + } + + /** + * This method contains the logic to be executed when removing this migration. + * The default implementation throws an exception indicating the migration cannot be removed. + * Child classes may override this method if the corresponding migrations can be removed. + * @return boolean + */ + public function down() + { + $transaction=$this->getDbConnection()->beginTransaction(); + try + { + if($this->safeDown()===false) + { + $transaction->rollBack(); + return false; + } + $transaction->commit(); + } + catch(Exception $e) + { + echo "Exception: ".$e->getMessage().' ('.$e->getFile().':'.$e->getLine().")\n"; + echo $e->getTraceAsString()."\n"; + $transaction->rollBack(); + return false; + } + } + + /** + * This method contains the logic to be executed when applying this migration. + * This method differs from {@link up} in that the DB logic implemented here will + * be enclosed within a DB transaction. + * Child classes may implement this method instead of {@link up} if the DB logic + * needs to be within a transaction. + * @return boolean + * @since 1.1.7 + */ + public function safeUp() + { + } + + /** + * This method contains the logic to be executed when removing this migration. + * This method differs from {@link down} in that the DB logic implemented here will + * be enclosed within a DB transaction. + * Child classes may implement this method instead of {@link up} if the DB logic + * needs to be within a transaction. + * @return boolean + * @since 1.1.7 + */ + public function safeDown() + { + } + + /** + * Returns the currently active database connection. + * By default, the 'db' application component will be returned and activated. + * You can call {@link setDbConnection} to switch to a different database connection. + * Methods such as {@link insert}, {@link createTable} will use this database connection + * to perform DB queries. + * @return CDbConnection the currently active database connection + */ + public function getDbConnection() + { + if($this->_db===null) + { + $this->_db=Yii::app()->getComponent('db'); + if(!$this->_db instanceof CDbConnection) + throw new CException(Yii::t('yii', 'The "db" application component must be configured to be a CDbConnection object.')); + } + return $this->_db; + } + + /** + * Sets the currently active database connection. + * The database connection will be used by the methods such as {@link insert}, {@link createTable}. + * @param CDbConnection $db the database connection component + */ + public function setDbConnection($db) + { + $this->_db=$db; + } + + /** + * Executes a SQL statement. + * This method executes the specified SQL statement using {@link dbConnection}. + * @param string $sql the SQL statement to be executed + * @param array $params input parameters (name=>value) for the SQL execution. See {@link CDbCommand::execute} for more details. + * @since 1.1.7 + */ + public function execute($sql, $params=array()) + { + echo " > execute SQL: $sql ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand($sql)->execute($params); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Creates and executes an INSERT SQL statement. + * The method will properly escape the column names, and bind the values to be inserted. + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column data (name=>value) to be inserted into the table. + */ + public function insert($table, $columns) + { + echo " > insert into $table ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->insert($table, $columns); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Creates and executes an UPDATE SQL statement. + * The method will properly escape the column names and bind the values to be updated. + * @param string $table the table to be updated. + * @param array $columns the column data (name=>value) to be updated. + * @param mixed $conditions the conditions that will be put in the WHERE part. Please + * refer to {@link CDbCommand::where} on how to specify conditions. + * @param array $params the parameters to be bound to the query. + */ + public function update($table, $columns, $conditions='', $params=array()) + { + echo " > update $table ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->update($table, $columns, $conditions, $params); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Creates and executes a DELETE SQL statement. + * @param string $table the table where the data will be deleted from. + * @param mixed $conditions the conditions that will be put in the WHERE part. Please + * refer to {@link CDbCommand::where} on how to specify conditions. + * @param array $params the parameters to be bound to the query. + */ + public function delete($table, $conditions='', $params=array()) + { + echo " > delete from $table ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->delete($table, $conditions, $params); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for creating a new DB table. + * + * The columns in the new table should be specified as name-definition pairs (e.g. 'name'=>'string'), + * where name stands for a column name which will be properly quoted by the method, and definition + * stands for the column type which can contain an abstract DB type. + * The {@link getColumnType} method will be invoked to convert any abstract type into a physical one. + * + * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly + * inserted into the generated SQL. + * + * @param string $table the name of the table to be created. The name will be properly quoted by the method. + * @param array $columns the columns (name=>definition) in the new table. + * @param string $options additional SQL fragment that will be appended to the generated SQL. + */ + public function createTable($table, $columns, $options=null) + { + echo " > create table $table ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->createTable($table, $columns, $options); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + */ + public function renameTable($table, $newName) + { + echo " > rename table $table to $newName ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->renameTable($table, $newName); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping a DB table. + * @param string $table the table to be dropped. The name will be properly quoted by the method. + */ + public function dropTable($table) + { + echo " > drop table $table ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->dropTable($table); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + */ + public function truncateTable($table) + { + echo " > truncate table $table ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->truncateTable($table); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + */ + public function addColumn($table, $column, $type) + { + echo " > add column $column $type to table $table ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->addColumn($table, $column, $type); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + */ + public function dropColumn($table, $column) + { + echo " > drop column $column from table $table ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->dropColumn($table, $column); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + */ + public function renameColumn($table, $name, $newName) + { + echo " > rename column $name in table $table to $newName ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->renameColumn($table, $name, $newName); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + */ + public function alterColumn($table, $column, $type) + { + echo " > alter column $column in table $table to $type ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->alterColumn($table, $column, $type); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas. + * @param string $refTable the table that the foreign key references to. + * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete=null, $update=null) + { + echo " > add foreign key $name: $table ($columns) references $refTable ($refColumns) ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + */ + public function dropForeignKey($name, $table) + { + echo " > drop foreign key $name from table $table ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->dropForeignKey($name, $table); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for creating a new index. + * @param string $name the name of the index. The name will be properly quoted by the method. + * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. + * @param string $column the column(s) that should be included in the index. If there are multiple columns, please separate them + * by commas. The column names will be properly quoted by the method. + * @param boolean $unique whether to add UNIQUE constraint on the created index. + */ + public function createIndex($name, $table, $column, $unique=false) + { + echo " > create".($unique ? ' unique':'')." index $name on $table ($column) ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->createIndex($name, $table, $column, $unique); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Builds and executes a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + */ + public function dropIndex($name, $table) + { + echo " > drop index $name ..."; + $time=microtime(true); + $this->getDbConnection()->createCommand()->dropIndex($name, $table); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } + + /** + * Refreshed schema cache for a table + * @param string $table name of the table to refresh + * @since 1.1.9 + */ + public function refreshTableSchema($table) + { + echo " > refresh table $table schema cache ..."; + $time=microtime(true); + $this->getDbConnection()->getSchema()->getTable($table,true); + echo " done (time: ".sprintf('%.3f', microtime(true)-$time)."s)\n"; + } +}
\ No newline at end of file diff --git a/framework/db/CDbTransaction.php b/framework/db/CDbTransaction.php new file mode 100644 index 0000000..dd27921 --- /dev/null +++ b/framework/db/CDbTransaction.php @@ -0,0 +1,111 @@ +<?php +/** + * CDbTransaction class file + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbTransaction represents a DB transaction. + * + * It is usually created by calling {@link CDbConnection::beginTransaction}. + * + * The following code is a common scenario of using transactions: + * <pre> + * $transaction=$connection->beginTransaction(); + * try + * { + * $connection->createCommand($sql1)->execute(); + * $connection->createCommand($sql2)->execute(); + * //.... other SQL executions + * $transaction->commit(); + * } + * catch(Exception $e) + * { + * $transaction->rollBack(); + * } + * </pre> + * + * @property CDbConnection $connection The DB connection for this transaction. + * @property boolean $active Whether this transaction is active. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbTransaction.php 3426 2011-10-25 00:01:09Z alexander.makarow $ + * @package system.db + * @since 1.0 + */ +class CDbTransaction extends CComponent +{ + private $_connection=null; + private $_active; + + /** + * Constructor. + * @param CDbConnection $connection the connection associated with this transaction + * @see CDbConnection::beginTransaction + */ + public function __construct(CDbConnection $connection) + { + $this->_connection=$connection; + $this->_active=true; + } + + /** + * Commits a transaction. + * @throws CException if the transaction or the DB connection is not active. + */ + public function commit() + { + if($this->_active && $this->_connection->getActive()) + { + Yii::trace('Committing transaction','system.db.CDbTransaction'); + $this->_connection->getPdoInstance()->commit(); + $this->_active=false; + } + else + throw new CDbException(Yii::t('yii','CDbTransaction is inactive and cannot perform commit or roll back operations.')); + } + + /** + * Rolls back a transaction. + * @throws CException if the transaction or the DB connection is not active. + */ + public function rollback() + { + if($this->_active && $this->_connection->getActive()) + { + Yii::trace('Rolling back transaction','system.db.CDbTransaction'); + $this->_connection->getPdoInstance()->rollBack(); + $this->_active=false; + } + else + throw new CDbException(Yii::t('yii','CDbTransaction is inactive and cannot perform commit or roll back operations.')); + } + + /** + * @return CDbConnection the DB connection for this transaction + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @return boolean whether this transaction is active + */ + public function getActive() + { + return $this->_active; + } + + /** + * @param boolean $value whether this transaction is active + */ + protected function setActive($value) + { + $this->_active=$value; + } +} diff --git a/framework/db/ar/CActiveFinder.php b/framework/db/ar/CActiveFinder.php new file mode 100644 index 0000000..2331100 --- /dev/null +++ b/framework/db/ar/CActiveFinder.php @@ -0,0 +1,1647 @@ +<?php +/** + * CActiveRecord class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CActiveFinder implements eager loading and lazy loading of related active records. + * + * When used in eager loading, this class provides the same set of find methods as + * {@link CActiveRecord}. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveFinder.php 3562 2012-02-13 01:27:06Z qiang.xue $ + * @package system.db.ar + * @since 1.0 + */ +class CActiveFinder extends CComponent +{ + /** + * @var boolean join all tables all at once. Defaults to false. + * This property is internally used. + */ + public $joinAll=false; + /** + * @var boolean whether the base model has limit or offset. + * This property is internally used. + */ + public $baseLimited=false; + + private $_joinCount=0; + private $_joinTree; + private $_builder; + + /** + * Constructor. + * A join tree is built up based on the declared relationships between active record classes. + * @param CActiveRecord $model the model that initiates the active finding process + * @param mixed $with the relation names to be actively looked for + */ + public function __construct($model,$with) + { + $this->_builder=$model->getCommandBuilder(); + $this->_joinTree=new CJoinElement($this,$model); + $this->buildJoinTree($this->_joinTree,$with); + } + + /** + * Do not call this method. This method is used internally to perform the relational query + * based on the given DB criteria. + * @param CDbCriteria $criteria the DB criteria + * @param boolean $all whether to bring back all records + * @return mixed the query result + */ + public function query($criteria,$all=false) + { + $this->joinAll=$criteria->together===true; + $this->_joinTree->beforeFind(false); + + if($criteria->alias!='') + { + $this->_joinTree->tableAlias=$criteria->alias; + $this->_joinTree->rawTableAlias=$this->_builder->getSchema()->quoteTableName($criteria->alias); + } + + $this->_joinTree->find($criteria); + $this->_joinTree->afterFind(); + + if($all) + { + $result = array_values($this->_joinTree->records); + if ($criteria->index!==null) + { + $index=$criteria->index; + $array=array(); + foreach($result as $object) + $array[$object->$index]=$object; + $result=$array; + } + } + else if(count($this->_joinTree->records)) + $result = reset($this->_joinTree->records); + else + $result = null; + + $this->destroyJoinTree(); + return $result; + } + + /** + * This method is internally called. + * @param string $sql the SQL statement + * @param array $params parameters to be bound to the SQL statement + * @return CActiveRecord + */ + public function findBySql($sql,$params=array()) + { + Yii::trace(get_class($this->_joinTree->model).'.findBySql() eagerly','system.db.ar.CActiveRecord'); + if(($row=$this->_builder->createSqlCommand($sql,$params)->queryRow())!==false) + { + $baseRecord=$this->_joinTree->model->populateRecord($row,false); + $this->_joinTree->beforeFind(false); + $this->_joinTree->findWithBase($baseRecord); + $this->_joinTree->afterFind(); + $this->destroyJoinTree(); + return $baseRecord; + } + else + $this->destroyJoinTree(); + } + + /** + * This method is internally called. + * @param string $sql the SQL statement + * @param array $params parameters to be bound to the SQL statement + * @return CActiveRecord[] + */ + public function findAllBySql($sql,$params=array()) + { + Yii::trace(get_class($this->_joinTree->model).'.findAllBySql() eagerly','system.db.ar.CActiveRecord'); + if(($rows=$this->_builder->createSqlCommand($sql,$params)->queryAll())!==array()) + { + $baseRecords=$this->_joinTree->model->populateRecords($rows,false); + $this->_joinTree->beforeFind(false); + $this->_joinTree->findWithBase($baseRecords); + $this->_joinTree->afterFind(); + $this->destroyJoinTree(); + return $baseRecords; + } + else + { + $this->destroyJoinTree(); + return array(); + } + } + + /** + * This method is internally called. + * @param CDbCriteria $criteria the query criteria + * @return string + */ + public function count($criteria) + { + Yii::trace(get_class($this->_joinTree->model).'.count() eagerly','system.db.ar.CActiveRecord'); + $this->joinAll=$criteria->together!==true; + + $alias=$criteria->alias===null ? 't' : $criteria->alias; + $this->_joinTree->tableAlias=$alias; + $this->_joinTree->rawTableAlias=$this->_builder->getSchema()->quoteTableName($alias); + + $n=$this->_joinTree->count($criteria); + $this->destroyJoinTree(); + return $n; + } + + /** + * Finds the related objects for the specified active record. + * This method is internally invoked by {@link CActiveRecord} to support lazy loading. + * @param CActiveRecord $baseRecord the base record whose related objects are to be loaded + */ + public function lazyFind($baseRecord) + { + $this->_joinTree->lazyFind($baseRecord); + if(!empty($this->_joinTree->children)) + { + $child=reset($this->_joinTree->children); + $child->afterFind(); + } + $this->destroyJoinTree(); + } + + private function destroyJoinTree() + { + if($this->_joinTree!==null) + $this->_joinTree->destroy(); + $this->_joinTree=null; + } + + /** + * Builds up the join tree representing the relationships involved in this query. + * @param CJoinElement $parent the parent tree node + * @param mixed $with the names of the related objects relative to the parent tree node + * @param array $options additional query options to be merged with the relation + */ + private function buildJoinTree($parent,$with,$options=null) + { + if($parent instanceof CStatElement) + throw new CDbException(Yii::t('yii','The STAT relation "{name}" cannot have child relations.', + array('{name}'=>$parent->relation->name))); + + if(is_string($with)) + { + if(($pos=strrpos($with,'.'))!==false) + { + $parent=$this->buildJoinTree($parent,substr($with,0,$pos)); + $with=substr($with,$pos+1); + } + + // named scope + $scopes=array(); + if(($pos=strpos($with,':'))!==false) + { + $scopes=explode(':',substr($with,$pos+1)); + $with=substr($with,0,$pos); + } + + if(isset($parent->children[$with]) && $parent->children[$with]->master===null) + return $parent->children[$with]; + + if(($relation=$parent->model->getActiveRelation($with))===null) + throw new CDbException(Yii::t('yii','Relation "{name}" is not defined in active record class "{class}".', + array('{class}'=>get_class($parent->model), '{name}'=>$with))); + + $relation=clone $relation; + $model=CActiveRecord::model($relation->className); + if($relation instanceof CActiveRelation) + { + $oldAlias=$model->getTableAlias(false,false); + if(isset($options['alias'])) + $model->setTableAlias($options['alias']); + else if($relation->alias===null) + $model->setTableAlias($relation->name); + else + $model->setTableAlias($relation->alias); + } + + if(($scope=$model->defaultScope())!==array()) + $relation->mergeWith($scope,true); + + if(!empty($relation->scopes)) + $scopes=array_merge($scopes,(array)$relation->scopes); // no need for complex merging + + if(!empty($options['scopes'])) + $scopes=array_merge($scopes,(array)$options['scopes']); // no need for complex merging + + if($scopes!==array()) + { + $scs=$model->scopes(); + foreach($scopes as $k=>$v) + { + if(is_integer($k)) + { + if(is_string($v)) + { + if(isset($scs[$v])) + { + $relation->mergeWith($scs[$v],true); + continue; + } + $scope=$v; + $params=array(); + } + else if(is_array($v)) + { + $scope=key($v); + $params=current($v); + } + } + else if(is_string($k)) + { + $scope=$k; + $params=$v; + } + + $model->resetScope(); + call_user_func_array(array($model,$scope),(array)$params); + $relation->mergeWith($model->getDbCriteria(),true); + } + } + + // dynamic options + if($options!==null) + $relation->mergeWith($options); + + if($relation instanceof CActiveRelation) + $model->setTableAlias($oldAlias); + + if($relation instanceof CStatRelation) + return new CStatElement($this,$relation,$parent); + else + { + if(isset($parent->children[$with])) + { + $element=$parent->children[$with]; + $element->relation=$relation; + } + else + $element=new CJoinElement($this,$relation,$parent,++$this->_joinCount); + if(!empty($relation->through)) + { + $slave=$this->buildJoinTree($parent,$relation->through,array('select'=>false)); + $slave->master=$element; + $element->slave=$slave; + } + $parent->children[$with]=$element; + if(!empty($relation->with)) + $this->buildJoinTree($element,$relation->with); + return $element; + } + } + + // $with is an array, keys are relation name, values are relation spec + foreach($with as $key=>$value) + { + if(is_string($value)) // the value is a relation name + $this->buildJoinTree($parent,$value); + else if(is_string($key) && is_array($value)) + $this->buildJoinTree($parent,$key,$value); + } + } +} + + +/** + * CJoinElement represents a tree node in the join tree created by {@link CActiveFinder}. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveFinder.php 3562 2012-02-13 01:27:06Z qiang.xue $ + * @package system.db.ar + * @since 1.0 + */ +class CJoinElement +{ + /** + * @var integer the unique ID of this tree node + */ + public $id; + /** + * @var CActiveRelation the relation represented by this tree node + */ + public $relation; + /** + * @var CActiveRelation the master relation + */ + public $master; + /** + * @var CActiveRelation the slave relation + */ + public $slave; + /** + * @var CActiveRecord the model associated with this tree node + */ + public $model; + /** + * @var array list of active records found by the queries. They are indexed by primary key values. + */ + public $records=array(); + /** + * @var array list of child join elements + */ + public $children=array(); + /** + * @var array list of stat elements + */ + public $stats=array(); + /** + * @var string table alias for this join element + */ + public $tableAlias; + /** + * @var string the quoted table alias for this element + */ + public $rawTableAlias; + + private $_finder; + private $_builder; + private $_parent; + private $_pkAlias; // string or name=>alias + private $_columnAliases=array(); // name=>alias + private $_joined=false; + private $_table; + private $_related=array(); // PK, relation name, related PK => true + + /** + * Constructor. + * @param CActiveFinder $finder the finder + * @param mixed $relation the relation (if the third parameter is not null) + * or the model (if the third parameter is null) associated with this tree node. + * @param CJoinElement $parent the parent tree node + * @param integer $id the ID of this tree node that is unique among all the tree nodes + */ + public function __construct($finder,$relation,$parent=null,$id=0) + { + $this->_finder=$finder; + $this->id=$id; + if($parent!==null) + { + $this->relation=$relation; + $this->_parent=$parent; + $this->model=CActiveRecord::model($relation->className); + $this->_builder=$this->model->getCommandBuilder(); + $this->tableAlias=$relation->alias===null?$relation->name:$relation->alias; + $this->rawTableAlias=$this->_builder->getSchema()->quoteTableName($this->tableAlias); + $this->_table=$this->model->getTableSchema(); + } + else // root element, the first parameter is the model. + { + $this->model=$relation; + $this->_builder=$relation->getCommandBuilder(); + $this->_table=$relation->getTableSchema(); + $this->tableAlias=$this->model->getTableAlias(); + $this->rawTableAlias=$this->_builder->getSchema()->quoteTableName($this->tableAlias); + } + + // set up column aliases, such as t1_c2 + $table=$this->_table; + if($this->model->getDbConnection()->getDriverName()==='oci') // Issue 482 + $prefix='T'.$id.'_C'; + else + $prefix='t'.$id.'_c'; + foreach($table->getColumnNames() as $key=>$name) + { + $alias=$prefix.$key; + $this->_columnAliases[$name]=$alias; + if($table->primaryKey===$name) + $this->_pkAlias=$alias; + else if(is_array($table->primaryKey) && in_array($name,$table->primaryKey)) + $this->_pkAlias[$name]=$alias; + } + } + + /** + * Removes references to child elements and finder to avoid circular references. + * This is internally used. + */ + public function destroy() + { + if(!empty($this->children)) + { + foreach($this->children as $child) + $child->destroy(); + } + unset($this->_finder, $this->_parent, $this->model, $this->relation, $this->master, $this->slave, $this->records, $this->children, $this->stats); + } + + /** + * Performs the recursive finding with the criteria. + * @param CDbCriteria $criteria the query criteria + */ + public function find($criteria=null) + { + if($this->_parent===null) // root element + { + $query=new CJoinQuery($this,$criteria); + $this->_finder->baseLimited=($criteria->offset>=0 || $criteria->limit>=0); + $this->buildQuery($query); + $this->_finder->baseLimited=false; + $this->runQuery($query); + } + else if(!$this->_joined && !empty($this->_parent->records)) // not joined before + { + $query=new CJoinQuery($this->_parent); + $this->_joined=true; + $query->join($this); + $this->buildQuery($query); + $this->_parent->runQuery($query); + } + + foreach($this->children as $child) // find recursively + $child->find(); + + foreach($this->stats as $stat) + $stat->query(); + } + + /** + * Performs lazy find with the specified base record. + * @param CActiveRecord $baseRecord the active record whose related object is to be fetched. + */ + public function lazyFind($baseRecord) + { + if(is_string($this->_table->primaryKey)) + $this->records[$baseRecord->{$this->_table->primaryKey}]=$baseRecord; + else + { + $pk=array(); + foreach($this->_table->primaryKey as $name) + $pk[$name]=$baseRecord->$name; + $this->records[serialize($pk)]=$baseRecord; + } + + foreach($this->stats as $stat) + $stat->query(); + + switch(count($this->children)) + { + case 0: + return; + break; + case 1: + $child=reset($this->children); + break; + default: // bridge(s) inside + $child=end($this->children); + break; + } + + $query=new CJoinQuery($child); + $query->selects=array(); + $query->selects[]=$child->getColumnSelect($child->relation->select); + $query->conditions=array(); + $query->conditions[]=$child->relation->condition; + $query->conditions[]=$child->relation->on; + $query->groups[]=$child->relation->group; + $query->joins[]=$child->relation->join; + $query->havings[]=$child->relation->having; + $query->orders[]=$child->relation->order; + if(is_array($child->relation->params)) + $query->params=$child->relation->params; + $query->elements[$child->id]=true; + if($child->relation instanceof CHasManyRelation) + { + $query->limit=$child->relation->limit; + $query->offset=$child->relation->offset; + } + + $child->beforeFind(); + $child->applyLazyCondition($query,$baseRecord); + + $this->_joined=true; + $child->_joined=true; + + $this->_finder->baseLimited=false; + $child->buildQuery($query); + $child->runQuery($query); + foreach($child->children as $c) + $c->find(); + + if(empty($child->records)) + return; + if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation) + $baseRecord->addRelatedRecord($child->relation->name,reset($child->records),false); + else // has_many and many_many + { + foreach($child->records as $record) + { + if($child->relation->index!==null) + $index=$record->{$child->relation->index}; + else + $index=true; + $baseRecord->addRelatedRecord($child->relation->name,$record,$index); + } + } + } + + /** + * Apply Lazy Condition + * @param CJoinQuery $query represents a JOIN SQL statements + * @param CActiveRecord $record the active record whose related object is to be fetched. + */ + private function applyLazyCondition($query,$record) + { + $schema=$this->_builder->getSchema(); + $parent=$this->_parent; + if($this->relation instanceof CManyManyRelation) + { + if(!preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches)) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The format of the foreign key must be "joinTable(fk1,fk2,...)".', + array('{class}'=>get_class($parent->model),'{relation}'=>$this->relation->name))); + + if(($joinTable=$schema->getTable($matches[1]))===null) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.', + array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{joinTable}'=>$matches[1]))); + $fks=preg_split('/\s*,\s*/',$matches[2],-1,PREG_SPLIT_NO_EMPTY); + + + $joinAlias=$schema->quoteTableName($this->relation->name.'_'.$this->tableAlias); + $parentCondition=array(); + $childCondition=array(); + $count=0; + $params=array(); + + $fkDefined=true; + foreach($fks as $i=>$fk) + { + if(isset($joinTable->foreignKeys[$fk])) // FK defined + { + list($tableName,$pk)=$joinTable->foreignKeys[$fk]; + if(!isset($parentCondition[$pk]) && $schema->compareTableNames($parent->_table->rawName,$tableName)) + { + $parentCondition[$pk]=$joinAlias.'.'.$schema->quoteColumnName($fk).'=:ypl'.$count; + $params[':ypl'.$count]=$record->$pk; + $count++; + } + else if(!isset($childCondition[$pk]) && $schema->compareTableNames($this->_table->rawName,$tableName)) + $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk); + else + { + $fkDefined=false; + break; + } + } + else + { + $fkDefined=false; + break; + } + } + + if(!$fkDefined) + { + $parentCondition=array(); + $childCondition=array(); + $count=0; + $params=array(); + foreach($fks as $i=>$fk) + { + if($i<count($parent->_table->primaryKey)) + { + $pk=is_array($parent->_table->primaryKey) ? $parent->_table->primaryKey[$i] : $parent->_table->primaryKey; + $parentCondition[$pk]=$joinAlias.'.'.$schema->quoteColumnName($fk).'=:ypl'.$count; + $params[':ypl'.$count]=$record->$pk; + $count++; + } + else + { + $j=$i-count($parent->_table->primaryKey); + $pk=is_array($this->_table->primaryKey) ? $this->_table->primaryKey[$j] : $this->_table->primaryKey; + $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk); + } + } + } + + if($parentCondition!==array() && $childCondition!==array()) + { + $join='INNER JOIN '.$joinTable->rawName.' '.$joinAlias.' ON '; + $join.='('.implode(') AND (',$parentCondition).') AND ('.implode(') AND (',$childCondition).')'; + if(!empty($this->relation->on)) + $join.=' AND ('.$this->relation->on.')'; + $query->joins[]=$join; + foreach($params as $name=>$value) + $query->params[$name]=$value; + } + else + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.', + array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name))); + } + else + { + $element=$this; + while($element->slave!==null) + { + $query->joins[]=$element->slave->joinOneMany($element->slave,$element->relation->foreignKey,$element,$parent); + $element=$element->slave; + } + $fks=is_array($element->relation->foreignKey) ? $element->relation->foreignKey : preg_split('/\s*,\s*/',$element->relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY); + $prefix=$element->getColumnPrefix(); + $params=array(); + foreach($fks as $i=>$fk) + { + if(!is_int($i)) + { + $pk=$fk; + $fk=$i; + } + + if($this->relation instanceof CBelongsToRelation) + { + if(is_int($i)) + { + if(isset($parent->_table->foreignKeys[$fk])) // FK defined + $pk=$parent->_table->foreignKeys[$fk][1]; + else if(is_array($this->_table->primaryKey)) // composite PK + $pk=$this->_table->primaryKey[$i]; + else + $pk=$this->_table->primaryKey; + } + $params[$pk]=$record->$fk; + } + else + { + if(is_int($i)) + { + if(isset($this->_table->foreignKeys[$fk])) // FK defined + $pk=$this->_table->foreignKeys[$fk][1]; + else if(is_array($parent->_table->primaryKey)) // composite PK + $pk=$parent->_table->primaryKey[$i]; + else + $pk=$parent->_table->primaryKey; + } + $params[$fk]=$record->$pk; + } + } + $count=0; + foreach($params as $name=>$value) + { + $query->conditions[]=$prefix.$schema->quoteColumnName($name).'=:ypl'.$count; + $query->params[':ypl'.$count]=$value; + $count++; + } + } + } + + /** + * Performs the eager loading with the base records ready. + * @param mixed $baseRecords the available base record(s). + */ + public function findWithBase($baseRecords) + { + if(!is_array($baseRecords)) + $baseRecords=array($baseRecords); + if(is_string($this->_table->primaryKey)) + { + foreach($baseRecords as $baseRecord) + $this->records[$baseRecord->{$this->_table->primaryKey}]=$baseRecord; + } + else + { + foreach($baseRecords as $baseRecord) + { + $pk=array(); + foreach($this->_table->primaryKey as $name) + $pk[$name]=$baseRecord->$name; + $this->records[serialize($pk)]=$baseRecord; + } + } + + $query=new CJoinQuery($this); + $this->buildQuery($query); + if(count($query->joins)>1) + $this->runQuery($query); + foreach($this->children as $child) + $child->find(); + + foreach($this->stats as $stat) + $stat->query(); + } + + /** + * Count the number of primary records returned by the join statement. + * @param CDbCriteria $criteria the query criteria + * @return string number of primary records. Note: type is string to keep max. precision. + */ + public function count($criteria=null) + { + $query=new CJoinQuery($this,$criteria); + // ensure only one big join statement is used + $this->_finder->baseLimited=false; + $this->_finder->joinAll=true; + $this->buildQuery($query); + + $select=is_array($criteria->select) ? implode(',',$criteria->select) : $criteria->select; + if($select!=='*' && !strncasecmp($select,'count',5)) + $query->selects=array($select); + else if(is_string($this->_table->primaryKey)) + { + $prefix=$this->getColumnPrefix(); + $schema=$this->_builder->getSchema(); + $column=$prefix.$schema->quoteColumnName($this->_table->primaryKey); + $query->selects=array("COUNT(DISTINCT $column)"); + } + else + $query->selects=array("COUNT(*)"); + + $query->orders=$query->groups=$query->havings=array(); + $query->limit=$query->offset=-1; + $command=$query->createCommand($this->_builder); + return $command->queryScalar(); + } + + /** + * Calls {@link CActiveRecord::beforeFind}. + * @param boolean $isChild whether is called for a child + */ + public function beforeFind($isChild=true) + { + if($isChild) + $this->model->beforeFindInternal(); + + foreach($this->children as $child) + $child->beforeFind(true); + } + + /** + * Calls {@link CActiveRecord::afterFind} of all the records. + */ + public function afterFind() + { + foreach($this->records as $record) + $record->afterFindInternal(); + foreach($this->children as $child) + $child->afterFind(); + + $this->children = null; + } + + /** + * Builds the join query with all descendant HAS_ONE and BELONGS_TO nodes. + * @param CJoinQuery $query the query being built up + */ + public function buildQuery($query) + { + foreach($this->children as $child) + { + if($child->master!==null) + $child->_joined=true; + else if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation + || $this->_finder->joinAll || $child->relation->together || (!$this->_finder->baseLimited && $child->relation->together===null)) + { + $child->_joined=true; + $query->join($child); + $child->buildQuery($query); + } + } + } + + /** + * Executes the join query and populates the query results. + * @param CJoinQuery $query the query to be executed. + */ + public function runQuery($query) + { + $command=$query->createCommand($this->_builder); + foreach($command->queryAll() as $row) + $this->populateRecord($query,$row); + } + + /** + * Populates the active records with the query data. + * @param CJoinQuery $query the query executed + * @param array $row a row of data + * @return CActiveRecord the populated record + */ + private function populateRecord($query,$row) + { + // determine the primary key value + if(is_string($this->_pkAlias)) // single key + { + if(isset($row[$this->_pkAlias])) + $pk=$row[$this->_pkAlias]; + else // no matching related objects + return null; + } + else // is_array, composite key + { + $pk=array(); + foreach($this->_pkAlias as $name=>$alias) + { + if(isset($row[$alias])) + $pk[$name]=$row[$alias]; + else // no matching related objects + return null; + } + $pk=serialize($pk); + } + + // retrieve or populate the record according to the primary key value + if(isset($this->records[$pk])) + $record=$this->records[$pk]; + else + { + $attributes=array(); + $aliases=array_flip($this->_columnAliases); + foreach($row as $alias=>$value) + { + if(isset($aliases[$alias])) + $attributes[$aliases[$alias]]=$value; + } + $record=$this->model->populateRecord($attributes,false); + foreach($this->children as $child) + { + if(!empty($child->relation->select)) + $record->addRelatedRecord($child->relation->name,null,$child->relation instanceof CHasManyRelation); + } + $this->records[$pk]=$record; + } + + // populate child records recursively + foreach($this->children as $child) + { + if(!isset($query->elements[$child->id]) || empty($child->relation->select)) + continue; + $childRecord=$child->populateRecord($query,$row); + if($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation) + $record->addRelatedRecord($child->relation->name,$childRecord,false); + else // has_many and many_many + { + // need to double check to avoid adding duplicated related objects + if($childRecord instanceof CActiveRecord) + $fpk=serialize($childRecord->getPrimaryKey()); + else + $fpk=0; + if(!isset($this->_related[$pk][$child->relation->name][$fpk])) + { + if($childRecord instanceof CActiveRecord && $child->relation->index!==null) + $index=$childRecord->{$child->relation->index}; + else + $index=true; + $record->addRelatedRecord($child->relation->name,$childRecord,$index); + $this->_related[$pk][$child->relation->name][$fpk]=true; + } + } + } + + return $record; + } + + /** + * @return string the table name and the table alias (if any). This can be used directly in SQL query without escaping. + */ + public function getTableNameWithAlias() + { + if($this->tableAlias!==null) + return $this->_table->rawName . ' ' . $this->rawTableAlias; + else + return $this->_table->rawName; + } + + /** + * Generates the list of columns to be selected. + * Columns will be properly aliased and primary keys will be added to selection if they are not specified. + * @param mixed $select columns to be selected. Defaults to '*', indicating all columns. + * @return string the column selection + */ + public function getColumnSelect($select='*') + { + $schema=$this->_builder->getSchema(); + $prefix=$this->getColumnPrefix(); + $columns=array(); + if($select==='*') + { + foreach($this->_table->getColumnNames() as $name) + $columns[]=$prefix.$schema->quoteColumnName($name).' AS '.$schema->quoteColumnName($this->_columnAliases[$name]); + } + else + { + if(is_string($select)) + $select=explode(',',$select); + $selected=array(); + foreach($select as $name) + { + $name=trim($name); + $matches=array(); + if(($pos=strrpos($name,'.'))!==false) + $key=substr($name,$pos+1); + else + $key=$name; + $key=trim($key,'\'"`'); + + if($key==='*') + { + foreach($this->_table->columns as $name=>$column) + { + $alias=$this->_columnAliases[$name]; + if(!isset($selected[$alias])) + { + $columns[]=$prefix.$column->rawName.' AS '.$schema->quoteColumnName($alias); + $selected[$alias]=1; + } + } + continue; + } + + if(isset($this->_columnAliases[$key])) // simple column names + { + $columns[]=$prefix.$schema->quoteColumnName($key).' AS '.$schema->quoteColumnName($this->_columnAliases[$key]); + $selected[$this->_columnAliases[$key]]=1; + } + else if(preg_match('/^(.*?)\s+AS\s+(\w+)$/im',$name,$matches)) // if the column is already aliased + { + $alias=$matches[2]; + if(!isset($this->_columnAliases[$alias]) || $this->_columnAliases[$alias]!==$alias) + { + $this->_columnAliases[$alias]=$alias; + $columns[]=$name; + $selected[$alias]=1; + } + } + else + throw new CDbException(Yii::t('yii','Active record "{class}" is trying to select an invalid column "{column}". Note, the column must exist in the table or be an expression with alias.', + array('{class}'=>get_class($this->model), '{column}'=>$name))); + } + // add primary key selection if they are not selected + if(is_string($this->_pkAlias) && !isset($selected[$this->_pkAlias])) + $columns[]=$prefix.$schema->quoteColumnName($this->_table->primaryKey).' AS '.$schema->quoteColumnName($this->_pkAlias); + else if(is_array($this->_pkAlias)) + { + foreach($this->_table->primaryKey as $name) + if(!isset($selected[$name])) + $columns[]=$prefix.$schema->quoteColumnName($name).' AS '.$schema->quoteColumnName($this->_pkAlias[$name]); + } + } + + return implode(', ',$columns); + } + + /** + * @return string the primary key selection + */ + public function getPrimaryKeySelect() + { + $schema=$this->_builder->getSchema(); + $prefix=$this->getColumnPrefix(); + $columns=array(); + if(is_string($this->_pkAlias)) + $columns[]=$prefix.$schema->quoteColumnName($this->_table->primaryKey).' AS '.$schema->quoteColumnName($this->_pkAlias); + else if(is_array($this->_pkAlias)) + { + foreach($this->_pkAlias as $name=>$alias) + $columns[]=$prefix.$schema->quoteColumnName($name).' AS '.$schema->quoteColumnName($alias); + } + return implode(', ',$columns); + } + + /** + * @return string the condition that specifies only the rows with the selected primary key values. + */ + public function getPrimaryKeyRange() + { + if(empty($this->records)) + return ''; + $values=array_keys($this->records); + if(is_array($this->_table->primaryKey)) + { + foreach($values as &$value) + $value=unserialize($value); + } + return $this->_builder->createInCondition($this->_table,$this->_table->primaryKey,$values,$this->getColumnPrefix()); + } + + /** + * @return string the column prefix for column reference disambiguation + */ + public function getColumnPrefix() + { + if($this->tableAlias!==null) + return $this->rawTableAlias.'.'; + else + return $this->_table->rawName.'.'; + } + + /** + * @return string the join statement (this node joins with its parent) + */ + public function getJoinCondition() + { + $parent=$this->_parent; + if($this->relation instanceof CManyManyRelation) + { + if(!preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches)) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The format of the foreign key must be "joinTable(fk1,fk2,...)".', + array('{class}'=>get_class($parent->model),'{relation}'=>$this->relation->name))); + + $schema=$this->_builder->getSchema(); + if(($joinTable=$schema->getTable($matches[1]))===null) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.', + array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{joinTable}'=>$matches[1]))); + $fks=preg_split('/\s*,\s*/',$matches[2],-1,PREG_SPLIT_NO_EMPTY); + + return $this->joinManyMany($joinTable,$fks,$parent); + } + else + { + $fks=is_array($this->relation->foreignKey) ? $this->relation->foreignKey : preg_split('/\s*,\s*/',$this->relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY); + if($this->relation instanceof CBelongsToRelation) + { + $pke=$this; + $fke=$parent; + } + else if($this->slave===null) + { + $pke=$parent; + $fke=$this; + } + else + { + $pke=$this; + $fke=$this->slave; + } + return $this->joinOneMany($fke,$fks,$pke,$parent); + } + } + + /** + * Generates the join statement for one-many relationship. + * This works for HAS_ONE, HAS_MANY and BELONGS_TO. + * @param CJoinElement $fke the join element containing foreign keys + * @param array $fks the foreign keys + * @param CJoinElement $pke the join element containg primary keys + * @param CJoinElement $parent the parent join element + * @return string the join statement + * @throws CDbException if a foreign key is invalid + */ + private function joinOneMany($fke,$fks,$pke,$parent) + { + $schema=$this->_builder->getSchema(); + $joins=array(); + if(is_string($fks)) + $fks=preg_split('/\s*,\s*/',$fks,-1,PREG_SPLIT_NO_EMPTY); + foreach($fks as $i=>$fk) + { + if(!is_int($i)) + { + $pk=$fk; + $fk=$i; + } + + if(!isset($fke->_table->columns[$fk])) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".', + array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{key}'=>$fk, '{table}'=>$fke->_table->name))); + + if(is_int($i)) + { + if(isset($fke->_table->foreignKeys[$fk]) && $schema->compareTableNames($pke->_table->rawName, $fke->_table->foreignKeys[$fk][0])) + $pk=$fke->_table->foreignKeys[$fk][1]; + else // FK constraints undefined + { + if(is_array($pke->_table->primaryKey)) // composite PK + $pk=$pke->_table->primaryKey[$i]; + else + $pk=$pke->_table->primaryKey; + } + } + + $joins[]=$fke->getColumnPrefix().$schema->quoteColumnName($fk) . '=' . $pke->getColumnPrefix().$schema->quoteColumnName($pk); + } + if(!empty($this->relation->on)) + $joins[]=$this->relation->on; + return $this->relation->joinType . ' ' . $this->getTableNameWithAlias() . ' ON (' . implode(') AND (',$joins).')'; + } + + /** + * Generates the join statement for many-many relationship. + * @param CDbTableSchema $joinTable the join table + * @param array $fks the foreign keys + * @param CJoinElement $parent the parent join element + * @return string the join statement + * @throws CDbException if a foreign key is invalid + */ + private function joinManyMany($joinTable,$fks,$parent) + { + $schema=$this->_builder->getSchema(); + $joinAlias=$schema->quoteTableName($this->relation->name.'_'.$this->tableAlias); + $parentCondition=array(); + $childCondition=array(); + + $fkDefined=true; + foreach($fks as $i=>$fk) + { + if(!isset($joinTable->columns[$fk])) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".', + array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name, '{key}'=>$fk, '{table}'=>$joinTable->name))); + + if(isset($joinTable->foreignKeys[$fk])) + { + list($tableName,$pk)=$joinTable->foreignKeys[$fk]; + if(!isset($parentCondition[$pk]) && $schema->compareTableNames($parent->_table->rawName,$tableName)) + $parentCondition[$pk]=$parent->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk); + else if(!isset($childCondition[$pk]) && $schema->compareTableNames($this->_table->rawName,$tableName)) + $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk); + else + { + $fkDefined=false; + break; + } + } + else + { + $fkDefined=false; + break; + } + } + + if(!$fkDefined) + { + $parentCondition=array(); + $childCondition=array(); + foreach($fks as $i=>$fk) + { + if($i<count($parent->_table->primaryKey)) + { + $pk=is_array($parent->_table->primaryKey) ? $parent->_table->primaryKey[$i] : $parent->_table->primaryKey; + $parentCondition[$pk]=$parent->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk); + } + else + { + $j=$i-count($parent->_table->primaryKey); + $pk=is_array($this->_table->primaryKey) ? $this->_table->primaryKey[$j] : $this->_table->primaryKey; + $childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk); + } + } + } + + if($parentCondition!==array() && $childCondition!==array()) + { + $join=$this->relation->joinType.' '.$joinTable->rawName.' '.$joinAlias; + $join.=' ON ('.implode(') AND (',$parentCondition).')'; + $join.=' '.$this->relation->joinType.' '.$this->getTableNameWithAlias(); + $join.=' ON ('.implode(') AND (',$childCondition).')'; + if(!empty($this->relation->on)) + $join.=' AND ('.$this->relation->on.')'; + return $join; + } + else + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.', + array('{class}'=>get_class($parent->model), '{relation}'=>$this->relation->name))); + } +} + + +/** + * CJoinQuery represents a JOIN SQL statement. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveFinder.php 3562 2012-02-13 01:27:06Z qiang.xue $ + * @package system.db.ar + * @since 1.0 + */ +class CJoinQuery +{ + /** + * @var array list of column selections + */ + public $selects=array(); + /** + * @var boolean whether to select distinct result set + */ + public $distinct=false; + /** + * @var array list of join statement + */ + public $joins=array(); + /** + * @var array list of WHERE clauses + */ + public $conditions=array(); + /** + * @var array list of ORDER BY clauses + */ + public $orders=array(); + /** + * @var array list of GROUP BY clauses + */ + public $groups=array(); + /** + * @var array list of HAVING clauses + */ + public $havings=array(); + /** + * @var integer row limit + */ + public $limit=-1; + /** + * @var integer row offset + */ + public $offset=-1; + /** + * @var array list of query parameters + */ + public $params=array(); + /** + * @var array list of join element IDs (id=>true) + */ + public $elements=array(); + + /** + * Constructor. + * @param CJoinElement $joinElement The root join tree. + * @param CDbCriteria $criteria the query criteria + */ + public function __construct($joinElement,$criteria=null) + { + if($criteria!==null) + { + $this->selects[]=$joinElement->getColumnSelect($criteria->select); + $this->joins[]=$joinElement->getTableNameWithAlias(); + $this->joins[]=$criteria->join; + $this->conditions[]=$criteria->condition; + $this->orders[]=$criteria->order; + $this->groups[]=$criteria->group; + $this->havings[]=$criteria->having; + $this->limit=$criteria->limit; + $this->offset=$criteria->offset; + $this->params=$criteria->params; + if(!$this->distinct && $criteria->distinct) + $this->distinct=true; + } + else + { + $this->selects[]=$joinElement->getPrimaryKeySelect(); + $this->joins[]=$joinElement->getTableNameWithAlias(); + $this->conditions[]=$joinElement->getPrimaryKeyRange(); + } + $this->elements[$joinElement->id]=true; + } + + /** + * Joins with another join element + * @param CJoinElement $element the element to be joined + */ + public function join($element) + { + if($element->slave!==null) + $this->join($element->slave); + if(!empty($element->relation->select)) + $this->selects[]=$element->getColumnSelect($element->relation->select); + $this->conditions[]=$element->relation->condition; + $this->orders[]=$element->relation->order; + $this->joins[]=$element->getJoinCondition(); + $this->joins[]=$element->relation->join; + $this->groups[]=$element->relation->group; + $this->havings[]=$element->relation->having; + + if(is_array($element->relation->params)) + { + if(is_array($this->params)) + $this->params=array_merge($this->params,$element->relation->params); + else + $this->params=$element->relation->params; + } + $this->elements[$element->id]=true; + } + + /** + * Creates the SQL statement. + * @param CDbCommandBuilder $builder the command builder + * @return string the SQL statement + */ + public function createCommand($builder) + { + $sql=($this->distinct ? 'SELECT DISTINCT ':'SELECT ') . implode(', ',$this->selects); + $sql.=' FROM ' . implode(' ',$this->joins); + + $conditions=array(); + foreach($this->conditions as $condition) + if($condition!=='') + $conditions[]=$condition; + if($conditions!==array()) + $sql.=' WHERE (' . implode(') AND (',$conditions).')'; + + $groups=array(); + foreach($this->groups as $group) + if($group!=='') + $groups[]=$group; + if($groups!==array()) + $sql.=' GROUP BY ' . implode(', ',$groups); + + $havings=array(); + foreach($this->havings as $having) + if($having!=='') + $havings[]=$having; + if($havings!==array()) + $sql.=' HAVING (' . implode(') AND (',$havings).')'; + + $orders=array(); + foreach($this->orders as $order) + if($order!=='') + $orders[]=$order; + if($orders!==array()) + $sql.=' ORDER BY ' . implode(', ',$orders); + + $sql=$builder->applyLimit($sql,$this->limit,$this->offset); + $command=$builder->getDbConnection()->createCommand($sql); + $builder->bindValues($command,$this->params); + return $command; + } +} + + +/** + * CStatElement represents STAT join element for {@link CActiveFinder}. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveFinder.php 3562 2012-02-13 01:27:06Z qiang.xue $ + * @package system.db.ar + */ +class CStatElement +{ + /** + * @var CActiveRelation the relation represented by this tree node + */ + public $relation; + + private $_finder; + private $_parent; + + /** + * Constructor. + * @param CActiveFinder $finder the finder + * @param CStatRelation $relation the STAT relation + * @param CJoinElement $parent the join element owning this STAT element + */ + public function __construct($finder,$relation,$parent) + { + $this->_finder=$finder; + $this->_parent=$parent; + $this->relation=$relation; + $parent->stats[]=$this; + } + + /** + * Performs the STAT query. + */ + public function query() + { + if(preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches)) + $this->queryManyMany($matches[1],$matches[2]); + else + $this->queryOneMany(); + } + + private function queryOneMany() + { + $relation=$this->relation; + $model=CActiveRecord::model($relation->className); + $builder=$model->getCommandBuilder(); + $schema=$builder->getSchema(); + $table=$model->getTableSchema(); + $parent=$this->_parent; + $pkTable=$parent->model->getTableSchema(); + + $fks=preg_split('/\s*,\s*/',$relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY); + if(count($fks)!==count($pkTable->primaryKey)) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The columns in the key must match the primary keys of the table "{table}".', + array('{class}'=>get_class($parent->model), '{relation}'=>$relation->name, '{table}'=>$pkTable->name))); + + // set up mapping between fk and pk columns + $map=array(); // pk=>fk + foreach($fks as $i=>$fk) + { + if(!isset($table->columns[$fk])) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".', + array('{class}'=>get_class($parent->model), '{relation}'=>$relation->name, '{key}'=>$fk, '{table}'=>$table->name))); + + if(isset($table->foreignKeys[$fk])) + { + list($tableName,$pk)=$table->foreignKeys[$fk]; + if($schema->compareTableNames($pkTable->rawName,$tableName)) + $map[$pk]=$fk; + else + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with a foreign key "{key}" that does not point to the parent table "{table}".', + array('{class}'=>get_class($parent->model), '{relation}'=>$relation->name, '{key}'=>$fk, '{table}'=>$pkTable->name))); + } + else // FK constraints undefined + { + if(is_array($pkTable->primaryKey)) // composite PK + $map[$pkTable->primaryKey[$i]]=$fk; + else + $map[$pkTable->primaryKey]=$fk; + } + } + + $records=$this->_parent->records; + + $join=empty($relation->join)?'' : ' '.$relation->join; + $where=empty($relation->condition)?' WHERE ' : ' WHERE ('.$relation->condition.') AND '; + $group=empty($relation->group)?'' : ', '.$relation->group; + $having=empty($relation->having)?'' : ' HAVING ('.$relation->having.')'; + $order=empty($relation->order)?'' : ' ORDER BY '.$relation->order; + + $c=$schema->quoteColumnName('c'); + $s=$schema->quoteColumnName('s'); + + $tableAlias=$model->getTableAlias(true); + + // generate and perform query + if(count($fks)===1) // single column FK + { + $col=$table->columns[$fks[0]]->rawName; + $sql="SELECT $col AS $c, {$relation->select} AS $s FROM {$table->rawName} ".$tableAlias.$join + .$where.'('.$builder->createInCondition($table,$fks[0],array_keys($records),$tableAlias.'.').')' + ." GROUP BY $col".$group + .$having.$order; + $command=$builder->getDbConnection()->createCommand($sql); + if(is_array($relation->params)) + $builder->bindValues($command,$relation->params); + $stats=array(); + foreach($command->queryAll() as $row) + $stats[$row['c']]=$row['s']; + } + else // composite FK + { + $keys=array_keys($records); + foreach($keys as &$key) + { + $key2=unserialize($key); + $key=array(); + foreach($pkTable->primaryKey as $pk) + $key[$map[$pk]]=$key2[$pk]; + } + $cols=array(); + foreach($pkTable->primaryKey as $n=>$pk) + { + $name=$table->columns[$map[$pk]]->rawName; + $cols[$name]=$name.' AS '.$schema->quoteColumnName('c'.$n); + } + $sql='SELECT '.implode(', ',$cols).", {$relation->select} AS $s FROM {$table->rawName} ".$tableAlias.$join + .$where.'('.$builder->createInCondition($table,$fks,$keys,$tableAlias.'.').')' + .' GROUP BY '.implode(', ',array_keys($cols)).$group + .$having.$order; + $command=$builder->getDbConnection()->createCommand($sql); + if(is_array($relation->params)) + $builder->bindValues($command,$relation->params); + $stats=array(); + foreach($command->queryAll() as $row) + { + $key=array(); + foreach($pkTable->primaryKey as $n=>$pk) + $key[$pk]=$row['c'.$n]; + $stats[serialize($key)]=$row['s']; + } + } + + // populate the results into existing records + foreach($records as $pk=>$record) + $record->addRelatedRecord($relation->name,isset($stats[$pk])?$stats[$pk]:$relation->defaultValue,false); + } + + /* + * @param string $joinTableName jointablename + * @param string $keys keys + */ + private function queryManyMany($joinTableName,$keys) + { + $relation=$this->relation; + $model=CActiveRecord::model($relation->className); + $table=$model->getTableSchema(); + $builder=$model->getCommandBuilder(); + $schema=$builder->getSchema(); + $pkTable=$this->_parent->model->getTableSchema(); + + $tableAlias=$model->getTableAlias(true); + + if(($joinTable=$builder->getSchema()->getTable($joinTableName))===null) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is not specified correctly. The join table "{joinTable}" given in the foreign key cannot be found in the database.', + array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name, '{joinTable}'=>$joinTableName))); + + $fks=preg_split('/\s*,\s*/',$keys,-1,PREG_SPLIT_NO_EMPTY); + if(count($fks)!==count($table->primaryKey)+count($pkTable->primaryKey)) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.', + array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name))); + + $joinCondition=array(); + $map=array(); + + $fkDefined=true; + foreach($fks as $i=>$fk) + { + if(!isset($joinTable->columns[$fk])) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".', + array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name, '{key}'=>$fk, '{table}'=>$joinTable->name))); + + if(isset($joinTable->foreignKeys[$fk])) + { + list($tableName,$pk)=$joinTable->foreignKeys[$fk]; + if(!isset($joinCondition[$pk]) && $schema->compareTableNames($table->rawName,$tableName)) + $joinCondition[$pk]=$tableAlias.'.'.$schema->quoteColumnName($pk).'='.$joinTable->rawName.'.'.$schema->quoteColumnName($fk); + else if(!isset($map[$pk]) && $schema->compareTableNames($pkTable->rawName,$tableName)) + $map[$pk]=$fk; + else + { + $fkDefined=false; + break; + } + } + else + { + $fkDefined=false; + break; + } + } + + if(!$fkDefined) + { + $joinCondition=array(); + $map=array(); + foreach($fks as $i=>$fk) + { + if($i<count($pkTable->primaryKey)) + { + $pk=is_array($pkTable->primaryKey) ? $pkTable->primaryKey[$i] : $pkTable->primaryKey; + $map[$pk]=$fk; + } + else + { + $j=$i-count($pkTable->primaryKey); + $pk=is_array($table->primaryKey) ? $table->primaryKey[$j] : $table->primaryKey; + $joinCondition[$pk]=$tableAlias.'.'.$schema->quoteColumnName($pk).'='.$joinTable->rawName.'.'.$schema->quoteColumnName($fk); + } + } + } + + if($joinCondition===array() || $map===array()) + throw new CDbException(Yii::t('yii','The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.', + array('{class}'=>get_class($this->_parent->model), '{relation}'=>$relation->name))); + + $records=$this->_parent->records; + + $cols=array(); + foreach(is_string($pkTable->primaryKey)?array($pkTable->primaryKey):$pkTable->primaryKey as $n=>$pk) + { + $name=$joinTable->rawName.'.'.$schema->quoteColumnName($map[$pk]); + $cols[$name]=$name.' AS '.$schema->quoteColumnName('c'.$n); + } + + $keys=array_keys($records); + if(is_array($pkTable->primaryKey)) + { + foreach($keys as &$key) + { + $key2=unserialize($key); + $key=array(); + foreach($pkTable->primaryKey as $pk) + $key[$map[$pk]]=$key2[$pk]; + } + } + + $join=empty($relation->join)?'' : ' '.$relation->join; + $where=empty($relation->condition)?'' : ' WHERE ('.$relation->condition.')'; + $group=empty($relation->group)?'' : ', '.$relation->group; + $having=empty($relation->having)?'' : ' AND ('.$relation->having.')'; + $order=empty($relation->order)?'' : ' ORDER BY '.$relation->order; + + $sql='SELECT '.$this->relation->select.' AS '.$schema->quoteColumnName('s').', '.implode(', ',$cols) + .' FROM '.$table->rawName.' '.$tableAlias.' INNER JOIN '.$joinTable->rawName + .' ON ('.implode(') AND (',$joinCondition).')'.$join + .$where + .' GROUP BY '.implode(', ',array_keys($cols)).$group + .' HAVING ('.$builder->createInCondition($joinTable,$map,$keys).')' + .$having.$order; + + $command=$builder->getDbConnection()->createCommand($sql); + if(is_array($relation->params)) + $builder->bindValues($command,$relation->params); + + $stats=array(); + foreach($command->queryAll() as $row) + { + if(is_array($pkTable->primaryKey)) + { + $key=array(); + foreach($pkTable->primaryKey as $n=>$k) + $key[$k]=$row['c'.$n]; + $stats[serialize($key)]=$row['s']; + } + else + $stats[$row['c0']]=$row['s']; + } + + foreach($records as $pk=>$record) + $record->addRelatedRecord($relation->name,isset($stats[$pk])?$stats[$pk]:$this->relation->defaultValue,false); + } +}
\ No newline at end of file diff --git a/framework/db/ar/CActiveRecord.php b/framework/db/ar/CActiveRecord.php new file mode 100644 index 0000000..282e523 --- /dev/null +++ b/framework/db/ar/CActiveRecord.php @@ -0,0 +1,2338 @@ +<?php +/** + * CActiveRecord class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CActiveRecord is the base class for classes representing relational data. + * + * It implements the active record design pattern, a popular Object-Relational Mapping (ORM) technique. + * Please check {@link http://www.yiiframework.com/doc/guide/database.ar the Guide} for more details + * about this class. + * + * @property CDbCriteria $dbCriteria The query criteria that is associated with this model. + * This criteria is mainly used by {@link scopes named scope} feature to accumulate + * different criteria specifications. + * @property CActiveRecordMetaData $metaData The meta for this AR class. + * @property CDbConnection $dbConnection The database connection used by active record. + * @property CDbTableSchema $tableSchema The metadata of the table that this AR belongs to. + * @property CDbCommandBuilder $commandBuilder The command builder used by this AR. + * @property array $attributes Attribute values indexed by attribute names. + * @property boolean $isNewRecord Whether the record is new and should be inserted when calling {@link save}. + * This property is automatically set in constructor and {@link populateRecord}. + * Defaults to false, but it will be set to true if the instance is created using + * the new operator. + * @property mixed $primaryKey The primary key value. An array (column name=>column value) is returned if the primary key is composite. + * If primary key is not defined, null will be returned. + * @property mixed $oldPrimaryKey The old primary key value. An array (column name=>column value) is returned if the primary key is composite. + * If primary key is not defined, null will be returned. + * @property string $tableAlias The default table alias. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecord.php 3533 2012-01-08 22:07:55Z mdomba $ + * @package system.db.ar + * @since 1.0 + */ +abstract class CActiveRecord extends CModel +{ + const BELONGS_TO='CBelongsToRelation'; + const HAS_ONE='CHasOneRelation'; + const HAS_MANY='CHasManyRelation'; + const MANY_MANY='CManyManyRelation'; + const STAT='CStatRelation'; + + /** + * @var CDbConnection the default database connection for all active record classes. + * By default, this is the 'db' application component. + * @see getDbConnection + */ + public static $db; + + private static $_models=array(); // class name => model + + private $_md; // meta data + private $_new=false; // whether this instance is new or not + private $_attributes=array(); // attribute name => attribute value + private $_related=array(); // attribute name => related objects + private $_c; // query criteria (used by finder only) + private $_pk; // old primary key value + private $_alias='t'; // the table alias being used for query + + + /** + * Constructor. + * @param string $scenario scenario name. See {@link CModel::scenario} for more details about this parameter. + */ + public function __construct($scenario='insert') + { + if($scenario===null) // internally used by populateRecord() and model() + return; + + $this->setScenario($scenario); + $this->setIsNewRecord(true); + $this->_attributes=$this->getMetaData()->attributeDefaults; + + $this->init(); + + $this->attachBehaviors($this->behaviors()); + $this->afterConstruct(); + } + + /** + * Initializes this model. + * This method is invoked when an AR instance is newly created and has + * its {@link scenario} set. + * You may override this method to provide code that is needed to initialize the model (e.g. setting + * initial property values.) + */ + public function init() + { + } + + /** + * Sets the parameters about query caching. + * This is a shortcut method to {@link CDbConnection::cache()}. + * It changes the query caching parameter of the {@link dbConnection} instance. + * @param integer $duration the number of seconds that query results may remain valid in cache. + * If this is 0, the caching will be disabled. + * @param CCacheDependency $dependency the dependency that will be used when saving the query results into cache. + * @param integer $queryCount number of SQL queries that need to be cached after calling this method. Defaults to 1, + * meaning that the next SQL query will be cached. + * @return CActiveRecord the active record instance itself. + * @since 1.1.7 + */ + public function cache($duration, $dependency=null, $queryCount=1) + { + $this->getDbConnection()->cache($duration, $dependency, $queryCount); + return $this; + } + + /** + * PHP sleep magic method. + * This method ensures that the model meta data reference is set to null. + * @return array + */ + public function __sleep() + { + $this->_md=null; + return array_keys((array)$this); + } + + /** + * PHP getter magic method. + * This method is overridden so that AR attributes can be accessed like properties. + * @param string $name property name + * @return mixed property value + * @see getAttribute + */ + public function __get($name) + { + if(isset($this->_attributes[$name])) + return $this->_attributes[$name]; + else if(isset($this->getMetaData()->columns[$name])) + return null; + else if(isset($this->_related[$name])) + return $this->_related[$name]; + else if(isset($this->getMetaData()->relations[$name])) + return $this->getRelated($name); + else + return parent::__get($name); + } + + /** + * PHP setter magic method. + * This method is overridden so that AR attributes can be accessed like properties. + * @param string $name property name + * @param mixed $value property value + */ + public function __set($name,$value) + { + if($this->setAttribute($name,$value)===false) + { + if(isset($this->getMetaData()->relations[$name])) + $this->_related[$name]=$value; + else + parent::__set($name,$value); + } + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking + * if the named attribute is null or not. + * @param string $name the property name or the event name + * @return boolean whether the property value is null + */ + public function __isset($name) + { + if(isset($this->_attributes[$name])) + return true; + else if(isset($this->getMetaData()->columns[$name])) + return false; + else if(isset($this->_related[$name])) + return true; + else if(isset($this->getMetaData()->relations[$name])) + return $this->getRelated($name)!==null; + else + return parent::__isset($name); + } + + /** + * Sets a component property to be null. + * This method overrides the parent implementation by clearing + * the specified attribute value. + * @param string $name the property name or the event name + */ + public function __unset($name) + { + if(isset($this->getMetaData()->columns[$name])) + unset($this->_attributes[$name]); + else if(isset($this->getMetaData()->relations[$name])) + unset($this->_related[$name]); + else + parent::__unset($name); + } + + /** + * Calls the named method which is not a class method. + * Do not call this method. This is a PHP magic method that we override + * to implement the named scope feature. + * @param string $name the method name + * @param array $parameters method parameters + * @return mixed the method return value + */ + public function __call($name,$parameters) + { + if(isset($this->getMetaData()->relations[$name])) + { + if(empty($parameters)) + return $this->getRelated($name,false); + else + return $this->getRelated($name,false,$parameters[0]); + } + + $scopes=$this->scopes(); + if(isset($scopes[$name])) + { + $this->getDbCriteria()->mergeWith($scopes[$name]); + return $this; + } + + return parent::__call($name,$parameters); + } + + /** + * Returns the related record(s). + * This method will return the related record(s) of the current record. + * If the relation is HAS_ONE or BELONGS_TO, it will return a single object + * or null if the object does not exist. + * If the relation is HAS_MANY or MANY_MANY, it will return an array of objects + * or an empty array. + * @param string $name the relation name (see {@link relations}) + * @param boolean $refresh whether to reload the related objects from database. Defaults to false. + * @param array $params additional parameters that customize the query conditions as specified in the relation declaration. + * @return mixed the related object(s). + * @throws CDbException if the relation is not specified in {@link relations}. + */ + public function getRelated($name,$refresh=false,$params=array()) + { + if(!$refresh && $params===array() && (isset($this->_related[$name]) || array_key_exists($name,$this->_related))) + return $this->_related[$name]; + + $md=$this->getMetaData(); + if(!isset($md->relations[$name])) + throw new CDbException(Yii::t('yii','{class} does not have relation "{name}".', + array('{class}'=>get_class($this), '{name}'=>$name))); + + Yii::trace('lazy loading '.get_class($this).'.'.$name,'system.db.ar.CActiveRecord'); + $relation=$md->relations[$name]; + if($this->getIsNewRecord() && !$refresh && ($relation instanceof CHasOneRelation || $relation instanceof CHasManyRelation)) + return $relation instanceof CHasOneRelation ? null : array(); + + if($params!==array()) // dynamic query + { + $exists=isset($this->_related[$name]) || array_key_exists($name,$this->_related); + if($exists) + $save=$this->_related[$name]; + $r=array($name=>$params); + } + else + $r=$name; + unset($this->_related[$name]); + + $finder=new CActiveFinder($this,$r); + $finder->lazyFind($this); + + if(!isset($this->_related[$name])) + { + if($relation instanceof CHasManyRelation) + $this->_related[$name]=array(); + else if($relation instanceof CStatRelation) + $this->_related[$name]=$relation->defaultValue; + else + $this->_related[$name]=null; + } + + if($params!==array()) + { + $results=$this->_related[$name]; + if($exists) + $this->_related[$name]=$save; + else + unset($this->_related[$name]); + return $results; + } + else + return $this->_related[$name]; + } + + /** + * Returns a value indicating whether the named related object(s) has been loaded. + * @param string $name the relation name + * @return boolean a value indicating whether the named related object(s) has been loaded. + */ + public function hasRelated($name) + { + return isset($this->_related[$name]) || array_key_exists($name,$this->_related); + } + + /** + * Returns the query criteria associated with this model. + * @param boolean $createIfNull whether to create a criteria instance if it does not exist. Defaults to true. + * @return CDbCriteria the query criteria that is associated with this model. + * This criteria is mainly used by {@link scopes named scope} feature to accumulate + * different criteria specifications. + */ + public function getDbCriteria($createIfNull=true) + { + if($this->_c===null) + { + if(($c=$this->defaultScope())!==array() || $createIfNull) + $this->_c=new CDbCriteria($c); + } + return $this->_c; + } + + /** + * Sets the query criteria for the current model. + * @param CDbCriteria $criteria the query criteria + * @since 1.1.3 + */ + public function setDbCriteria($criteria) + { + $this->_c=$criteria; + } + + /** + * Returns the default named scope that should be implicitly applied to all queries for this model. + * Note, default scope only applies to SELECT queries. It is ignored for INSERT, UPDATE and DELETE queries. + * The default implementation simply returns an empty array. You may override this method + * if the model needs to be queried with some default criteria (e.g. only active records should be returned). + * @return array the query criteria. This will be used as the parameter to the constructor + * of {@link CDbCriteria}. + */ + public function defaultScope() + { + return array(); + } + + /** + * Resets all scopes and criterias applied including default scope. + * + * @return CActiveRecord + * @since 1.1.2 + */ + public function resetScope() + { + $this->_c=new CDbCriteria(); + return $this; + } + + /** + * Returns the static model of the specified AR class. + * The model returned is a static instance of the AR class. + * It is provided for invoking class-level methods (something similar to static class methods.) + * + * EVERY derived AR class must override this method as follows, + * <pre> + * public static function model($className=__CLASS__) + * { + * return parent::model($className); + * } + * </pre> + * + * @param string $className active record class name. + * @return CActiveRecord active record model instance. + */ + public static function model($className=__CLASS__) + { + if(isset(self::$_models[$className])) + return self::$_models[$className]; + else + { + $model=self::$_models[$className]=new $className(null); + $model->_md=new CActiveRecordMetaData($model); + $model->attachBehaviors($model->behaviors()); + return $model; + } + } + + /** + * Returns the meta-data for this AR + * @return CActiveRecordMetaData the meta for this AR class. + */ + public function getMetaData() + { + if($this->_md!==null) + return $this->_md; + else + return $this->_md=self::model(get_class($this))->_md; + } + + /** + * Refreshes the meta data for this AR class. + * By calling this method, this AR class will regenerate the meta data needed. + * This is useful if the table schema has been changed and you want to use the latest + * available table schema. Make sure you have called {@link CDbSchema::refresh} + * before you call this method. Otherwise, old table schema data will still be used. + */ + public function refreshMetaData() + { + $finder=self::model(get_class($this)); + $finder->_md=new CActiveRecordMetaData($finder); + if($this!==$finder) + $this->_md=$finder->_md; + } + + /** + * Returns the name of the associated database table. + * By default this method returns the class name as the table name. + * You may override this method if the table is not named after this convention. + * @return string the table name + */ + public function tableName() + { + return get_class($this); + } + + /** + * Returns the primary key of the associated database table. + * This method is meant to be overridden in case when the table is not defined with a primary key + * (for some legency database). If the table is already defined with a primary key, + * you do not need to override this method. The default implementation simply returns null, + * meaning using the primary key defined in the database. + * @return mixed the primary key of the associated database table. + * If the key is a single column, it should return the column name; + * If the key is a composite one consisting of several columns, it should + * return the array of the key column names. + */ + public function primaryKey() + { + } + + /** + * This method should be overridden to declare related objects. + * + * There are four types of relations that may exist between two active record objects: + * <ul> + * <li>BELONGS_TO: e.g. a member belongs to a team;</li> + * <li>HAS_ONE: e.g. a member has at most one profile;</li> + * <li>HAS_MANY: e.g. a team has many members;</li> + * <li>MANY_MANY: e.g. a member has many skills and a skill belongs to a member.</li> + * </ul> + * + * Besides the above relation types, a special relation called STAT is also supported + * that can be used to perform statistical query (or aggregational query). + * It retrieves the aggregational information about the related objects, such as the number + * of comments for each post, the average rating for each product, etc. + * + * Each kind of related objects is defined in this method as an array with the following elements: + * <pre> + * 'varName'=>array('relationType', 'className', 'foreign_key', ...additional options) + * </pre> + * where 'varName' refers to the name of the variable/property that the related object(s) can + * be accessed through; 'relationType' refers to the type of the relation, which can be one of the + * following four constants: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY and self::MANY_MANY; + * 'className' refers to the name of the active record class that the related object(s) is of; + * and 'foreign_key' states the foreign key that relates the two kinds of active record. + * Note, for composite foreign keys, they can be either listed together, separated by commas or specified as an array + * in format of array('key1','key2'). In case you need to specify custom PK->FK association you can define it as + * array('fk'=>'pk'). For composite keys it will be array('fk_c1'=>'pk_с1','fk_c2'=>'pk_c2'). + * For foreign keys used in MANY_MANY relation, the joining table must be declared as well + * (e.g. 'join_table(fk1, fk2)'). + * + * Additional options may be specified as name-value pairs in the rest array elements: + * <ul> + * <li>'select': string|array, a list of columns to be selected. Defaults to '*', meaning all columns. + * Column names should be disambiguated if they appear in an expression (e.g. COUNT(relationName.name) AS name_count).</li> + * <li>'condition': string, the WHERE clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix 'relationName.' (e.g. relationName.age>20)</li> + * <li>'order': string, the ORDER BY clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix 'relationName.' (e.g. relationName.age DESC)</li> + * <li>'with': string|array, a list of child related objects that should be loaded together with this object. + * Note, this is only honored by lazy loading, not eager loading.</li> + * <li>'joinType': type of join. Defaults to 'LEFT OUTER JOIN'.</li> + * <li>'alias': the alias for the table associated with this relationship. + * It defaults to null, + * meaning the table alias is the same as the relation name.</li> + * <li>'params': the parameters to be bound to the generated SQL statement. + * This should be given as an array of name-value pairs.</li> + * <li>'on': the ON clause. The condition specified here will be appended + * to the joining condition using the AND operator.</li> + * <li>'index': the name of the column whose values should be used as keys + * of the array that stores related objects. This option is only available to + * HAS_MANY and MANY_MANY relations.</li> + * <li>'scopes': scopes to apply. In case of a single scope can be used like 'scopes'=>'scopeName', + * in case of multiple scopes can be used like 'scopes'=>array('scopeName1','scopeName2'). + * This option has been available since version 1.1.9.</li> + * </ul> + * + * The following options are available for certain relations when lazy loading: + * <ul> + * <li>'group': string, the GROUP BY clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix 'relationName.' (e.g. relationName.age). This option only applies to HAS_MANY and MANY_MANY relations.</li> + * <li>'having': string, the HAVING clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix 'relationName.' (e.g. relationName.age). This option only applies to HAS_MANY and MANY_MANY relations.</li> + * <li>'limit': limit of the rows to be selected. This option does not apply to BELONGS_TO relation.</li> + * <li>'offset': offset of the rows to be selected. This option does not apply to BELONGS_TO relation.</li> + * <li>'through': name of the model's relation that will be used as a bridge when getting related data. Can be set only for HAS_ONE and HAS_MANY. This option has been available since version 1.1.7.</li> + * </ul> + * + * Below is an example declaring related objects for 'Post' active record class: + * <pre> + * return array( + * 'author'=>array(self::BELONGS_TO, 'User', 'author_id'), + * 'comments'=>array(self::HAS_MANY, 'Comment', 'post_id', 'with'=>'author', 'order'=>'create_time DESC'), + * 'tags'=>array(self::MANY_MANY, 'Tag', 'post_tag(post_id, tag_id)', 'order'=>'name'), + * ); + * </pre> + * + * @return array list of related object declarations. Defaults to empty array. + */ + public function relations() + { + return array(); + } + + /** + * Returns the declaration of named scopes. + * A named scope represents a query criteria that can be chained together with + * other named scopes and applied to a query. This method should be overridden + * by child classes to declare named scopes for the particular AR classes. + * For example, the following code declares two named scopes: 'recently' and + * 'published'. + * <pre> + * return array( + * 'published'=>array( + * 'condition'=>'status=1', + * ), + * 'recently'=>array( + * 'order'=>'create_time DESC', + * 'limit'=>5, + * ), + * ); + * </pre> + * If the above scopes are declared in a 'Post' model, we can perform the following + * queries: + * <pre> + * $posts=Post::model()->published()->findAll(); + * $posts=Post::model()->published()->recently()->findAll(); + * $posts=Post::model()->published()->with('comments')->findAll(); + * </pre> + * Note that the last query is a relational query. + * + * @return array the scope definition. The array keys are scope names; the array + * values are the corresponding scope definitions. Each scope definition is represented + * as an array whose keys must be properties of {@link CDbCriteria}. + */ + public function scopes() + { + return array(); + } + + /** + * Returns the list of all attribute names of the model. + * This would return all column names of the table associated with this AR class. + * @return array list of attribute names. + */ + public function attributeNames() + { + return array_keys($this->getMetaData()->columns); + } + + /** + * Returns the text label for the specified attribute. + * This method overrides the parent implementation by supporting + * returning the label defined in relational object. + * In particular, if the attribute name is in the form of "post.author.name", + * then this method will derive the label from the "author" relation's "name" attribute. + * @param string $attribute the attribute name + * @return string the attribute label + * @see generateAttributeLabel + * @since 1.1.4 + */ + public function getAttributeLabel($attribute) + { + $labels=$this->attributeLabels(); + if(isset($labels[$attribute])) + return $labels[$attribute]; + else if(strpos($attribute,'.')!==false) + { + $segs=explode('.',$attribute); + $name=array_pop($segs); + $model=$this; + foreach($segs as $seg) + { + $relations=$model->getMetaData()->relations; + if(isset($relations[$seg])) + $model=CActiveRecord::model($relations[$seg]->className); + else + break; + } + return $model->getAttributeLabel($name); + } + else + return $this->generateAttributeLabel($attribute); + } + + /** + * Returns the database connection used by active record. + * By default, the "db" application component is used as the database connection. + * You may override this method if you want to use a different database connection. + * @return CDbConnection the database connection used by active record. + */ + public function getDbConnection() + { + if(self::$db!==null) + return self::$db; + else + { + self::$db=Yii::app()->getDb(); + if(self::$db instanceof CDbConnection) + return self::$db; + else + throw new CDbException(Yii::t('yii','Active Record requires a "db" CDbConnection application component.')); + } + } + + /** + * Returns the named relation declared for this AR class. + * @param string $name the relation name + * @return CActiveRelation the named relation declared for this AR class. Null if the relation does not exist. + */ + public function getActiveRelation($name) + { + return isset($this->getMetaData()->relations[$name]) ? $this->getMetaData()->relations[$name] : null; + } + + /** + * Returns the metadata of the table that this AR belongs to + * @return CDbTableSchema the metadata of the table that this AR belongs to + */ + public function getTableSchema() + { + return $this->getMetaData()->tableSchema; + } + + /** + * Returns the command builder used by this AR. + * @return CDbCommandBuilder the command builder used by this AR + */ + public function getCommandBuilder() + { + return $this->getDbConnection()->getSchema()->getCommandBuilder(); + } + + /** + * Checks whether this AR has the named attribute + * @param string $name attribute name + * @return boolean whether this AR has the named attribute (table column). + */ + public function hasAttribute($name) + { + return isset($this->getMetaData()->columns[$name]); + } + + /** + * Returns the named attribute value. + * If this is a new record and the attribute is not set before, + * the default column value will be returned. + * If this record is the result of a query and the attribute is not loaded, + * null will be returned. + * You may also use $this->AttributeName to obtain the attribute value. + * @param string $name the attribute name + * @return mixed the attribute value. Null if the attribute is not set or does not exist. + * @see hasAttribute + */ + public function getAttribute($name) + { + if(property_exists($this,$name)) + return $this->$name; + else if(isset($this->_attributes[$name])) + return $this->_attributes[$name]; + } + + /** + * Sets the named attribute value. + * You may also use $this->AttributeName to set the attribute value. + * @param string $name the attribute name + * @param mixed $value the attribute value. + * @return boolean whether the attribute exists and the assignment is conducted successfully + * @see hasAttribute + */ + public function setAttribute($name,$value) + { + if(property_exists($this,$name)) + $this->$name=$value; + else if(isset($this->getMetaData()->columns[$name])) + $this->_attributes[$name]=$value; + else + return false; + return true; + } + + /** + * Do not call this method. This method is used internally by {@link CActiveFinder} to populate + * related objects. This method adds a related object to this record. + * @param string $name attribute name + * @param mixed $record the related record + * @param mixed $index the index value in the related object collection. + * If true, it means using zero-based integer index. + * If false, it means a HAS_ONE or BELONGS_TO object and no index is needed. + */ + public function addRelatedRecord($name,$record,$index) + { + if($index!==false) + { + if(!isset($this->_related[$name])) + $this->_related[$name]=array(); + if($record instanceof CActiveRecord) + { + if($index===true) + $this->_related[$name][]=$record; + else + $this->_related[$name][$index]=$record; + } + } + else if(!isset($this->_related[$name])) + $this->_related[$name]=$record; + } + + /** + * Returns all column attribute values. + * Note, related objects are not returned. + * @param mixed $names names of attributes whose value needs to be returned. + * If this is true (default), then all attribute values will be returned, including + * those that are not loaded from DB (null will be returned for those attributes). + * If this is null, all attributes except those that are not loaded from DB will be returned. + * @return array attribute values indexed by attribute names. + */ + public function getAttributes($names=true) + { + $attributes=$this->_attributes; + foreach($this->getMetaData()->columns as $name=>$column) + { + if(property_exists($this,$name)) + $attributes[$name]=$this->$name; + else if($names===true && !isset($attributes[$name])) + $attributes[$name]=null; + } + if(is_array($names)) + { + $attrs=array(); + foreach($names as $name) + { + if(property_exists($this,$name)) + $attrs[$name]=$this->$name; + else + $attrs[$name]=isset($attributes[$name])?$attributes[$name]:null; + } + return $attrs; + } + else + return $attributes; + } + + /** + * Saves the current record. + * + * The record is inserted as a row into the database table if its {@link isNewRecord} + * property is true (usually the case when the record is created using the 'new' + * operator). Otherwise, it will be used to update the corresponding row in the table + * (usually the case if the record is obtained using one of those 'find' methods.) + * + * Validation will be performed before saving the record. If the validation fails, + * the record will not be saved. You can call {@link getErrors()} to retrieve the + * validation errors. + * + * If the record is saved via insertion, its {@link isNewRecord} property will be + * set false, and its {@link scenario} property will be set to be 'update'. + * And if its primary key is auto-incremental and is not set before insertion, + * the primary key will be populated with the automatically generated key value. + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be saved to database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the saving succeeds + */ + public function save($runValidation=true,$attributes=null) + { + if(!$runValidation || $this->validate($attributes)) + return $this->getIsNewRecord() ? $this->insert($attributes) : $this->update($attributes); + else + return false; + } + + /** + * Returns if the current record is new. + * @return boolean whether the record is new and should be inserted when calling {@link save}. + * This property is automatically set in constructor and {@link populateRecord}. + * Defaults to false, but it will be set to true if the instance is created using + * the new operator. + */ + public function getIsNewRecord() + { + return $this->_new; + } + + /** + * Sets if the record is new. + * @param boolean $value whether the record is new and should be inserted when calling {@link save}. + * @see getIsNewRecord + */ + public function setIsNewRecord($value) + { + $this->_new=$value; + } + + /** + * This event is raised before the record is saved. + * By setting {@link CModelEvent::isValid} to be false, the normal {@link save()} process will be stopped. + * @param CModelEvent $event the event parameter + */ + public function onBeforeSave($event) + { + $this->raiseEvent('onBeforeSave',$event); + } + + /** + * This event is raised after the record is saved. + * @param CEvent $event the event parameter + */ + public function onAfterSave($event) + { + $this->raiseEvent('onAfterSave',$event); + } + + /** + * This event is raised before the record is deleted. + * By setting {@link CModelEvent::isValid} to be false, the normal {@link delete()} process will be stopped. + * @param CModelEvent $event the event parameter + */ + public function onBeforeDelete($event) + { + $this->raiseEvent('onBeforeDelete',$event); + } + + /** + * This event is raised after the record is deleted. + * @param CEvent $event the event parameter + */ + public function onAfterDelete($event) + { + $this->raiseEvent('onAfterDelete',$event); + } + + /** + * This event is raised before an AR finder performs a find call. + * In this event, the {@link CModelEvent::criteria} property contains the query criteria + * passed as parameters to those find methods. If you want to access + * the query criteria specified in scopes, please use {@link getDbCriteria()}. + * You can modify either criteria to customize them based on needs. + * @param CModelEvent $event the event parameter + * @see beforeFind + */ + public function onBeforeFind($event) + { + $this->raiseEvent('onBeforeFind',$event); + } + + /** + * This event is raised after the record is instantiated by a find method. + * @param CEvent $event the event parameter + */ + public function onAfterFind($event) + { + $this->raiseEvent('onAfterFind',$event); + } + + /** + * This method is invoked before saving a record (after validation, if any). + * The default implementation raises the {@link onBeforeSave} event. + * You may override this method to do any preparation work for record saving. + * Use {@link isNewRecord} to determine whether the saving is + * for inserting or updating record. + * Make sure you call the parent implementation so that the event is raised properly. + * @return boolean whether the saving should be executed. Defaults to true. + */ + protected function beforeSave() + { + if($this->hasEventHandler('onBeforeSave')) + { + $event=new CModelEvent($this); + $this->onBeforeSave($event); + return $event->isValid; + } + else + return true; + } + + /** + * This method is invoked after saving a record successfully. + * The default implementation raises the {@link onAfterSave} event. + * You may override this method to do postprocessing after record saving. + * Make sure you call the parent implementation so that the event is raised properly. + */ + protected function afterSave() + { + if($this->hasEventHandler('onAfterSave')) + $this->onAfterSave(new CEvent($this)); + } + + /** + * This method is invoked before deleting a record. + * The default implementation raises the {@link onBeforeDelete} event. + * You may override this method to do any preparation work for record deletion. + * Make sure you call the parent implementation so that the event is raised properly. + * @return boolean whether the record should be deleted. Defaults to true. + */ + protected function beforeDelete() + { + if($this->hasEventHandler('onBeforeDelete')) + { + $event=new CModelEvent($this); + $this->onBeforeDelete($event); + return $event->isValid; + } + else + return true; + } + + /** + * This method is invoked after deleting a record. + * The default implementation raises the {@link onAfterDelete} event. + * You may override this method to do postprocessing after the record is deleted. + * Make sure you call the parent implementation so that the event is raised properly. + */ + protected function afterDelete() + { + if($this->hasEventHandler('onAfterDelete')) + $this->onAfterDelete(new CEvent($this)); + } + + /** + * This method is invoked before an AR finder executes a find call. + * The find calls include {@link find}, {@link findAll}, {@link findByPk}, + * {@link findAllByPk}, {@link findByAttributes} and {@link findAllByAttributes}. + * The default implementation raises the {@link onBeforeFind} event. + * If you override this method, make sure you call the parent implementation + * so that the event is raised properly. + * + * Starting from version 1.1.5, this method may be called with a hidden {@link CDbCriteria} + * parameter which represents the current query criteria as passed to a find method of AR. + * + */ + protected function beforeFind() + { + if($this->hasEventHandler('onBeforeFind')) + { + $event=new CModelEvent($this); + // for backward compatibility + $event->criteria=func_num_args()>0 ? func_get_arg(0) : null; + $this->onBeforeFind($event); + } + } + + /** + * This method is invoked after each record is instantiated by a find method. + * The default implementation raises the {@link onAfterFind} event. + * You may override this method to do postprocessing after each newly found record is instantiated. + * Make sure you call the parent implementation so that the event is raised properly. + */ + protected function afterFind() + { + if($this->hasEventHandler('onAfterFind')) + $this->onAfterFind(new CEvent($this)); + } + + /** + * Calls {@link beforeFind}. + * This method is internally used. + */ + public function beforeFindInternal() + { + $this->beforeFind(); + } + + /** + * Calls {@link afterFind}. + * This method is internally used. + */ + public function afterFindInternal() + { + $this->afterFind(); + } + + /** + * Inserts a row into the table based on this active record attributes. + * If the table's primary key is auto-incremental and is null before insertion, + * it will be populated with the actual value after insertion. + * Note, validation is not performed in this method. You may call {@link validate} to perform the validation. + * After the record is inserted to DB successfully, its {@link isNewRecord} property will be set false, + * and its {@link scenario} property will be set to be 'update'. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the attributes are valid and the record is inserted successfully. + * @throws CException if the record is not new + */ + public function insert($attributes=null) + { + if(!$this->getIsNewRecord()) + throw new CDbException(Yii::t('yii','The active record cannot be inserted to database because it is not new.')); + if($this->beforeSave()) + { + Yii::trace(get_class($this).'.insert()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $table=$this->getMetaData()->tableSchema; + $command=$builder->createInsertCommand($table,$this->getAttributes($attributes)); + if($command->execute()) + { + $primaryKey=$table->primaryKey; + if($table->sequenceName!==null) + { + if(is_string($primaryKey) && $this->$primaryKey===null) + $this->$primaryKey=$builder->getLastInsertID($table); + else if(is_array($primaryKey)) + { + foreach($primaryKey as $pk) + { + if($this->$pk===null) + { + $this->$pk=$builder->getLastInsertID($table); + break; + } + } + } + } + $this->_pk=$this->getPrimaryKey(); + $this->afterSave(); + $this->setIsNewRecord(false); + $this->setScenario('update'); + return true; + } + } + return false; + } + + /** + * Updates the row represented by this active record. + * All loaded attributes will be saved to the database. + * Note, validation is not performed in this method. You may call {@link validate} to perform the validation. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes that are loaded from DB will be saved. + * @return boolean whether the update is successful + * @throws CException if the record is new + */ + public function update($attributes=null) + { + if($this->getIsNewRecord()) + throw new CDbException(Yii::t('yii','The active record cannot be updated because it is new.')); + if($this->beforeSave()) + { + Yii::trace(get_class($this).'.update()','system.db.ar.CActiveRecord'); + if($this->_pk===null) + $this->_pk=$this->getPrimaryKey(); + $this->updateByPk($this->getOldPrimaryKey(),$this->getAttributes($attributes)); + $this->_pk=$this->getPrimaryKey(); + $this->afterSave(); + return true; + } + else + return false; + } + + /** + * Saves a selected list of attributes. + * Unlike {@link save}, this method only saves the specified attributes + * of an existing row dataset and does NOT call either {@link beforeSave} or {@link afterSave}. + * Also note that this method does neither attribute filtering nor validation. + * So do not use this method with untrusted data (such as user posted data). + * You may consider the following alternative if you want to do so: + * <pre> + * $postRecord=Post::model()->findByPk($postID); + * $postRecord->attributes=$_POST['post']; + * $postRecord->save(); + * </pre> + * @param array $attributes attributes to be updated. Each element represents an attribute name + * or an attribute value indexed by its name. If the latter, the record's + * attribute will be changed accordingly before saving. + * @return boolean whether the update is successful + * @throws CException if the record is new or any database error + */ + public function saveAttributes($attributes) + { + if(!$this->getIsNewRecord()) + { + Yii::trace(get_class($this).'.saveAttributes()','system.db.ar.CActiveRecord'); + $values=array(); + foreach($attributes as $name=>$value) + { + if(is_integer($name)) + $values[$value]=$this->$value; + else + $values[$name]=$this->$name=$value; + } + if($this->_pk===null) + $this->_pk=$this->getPrimaryKey(); + if($this->updateByPk($this->getOldPrimaryKey(),$values)>0) + { + $this->_pk=$this->getPrimaryKey(); + return true; + } + else + return false; + } + else + throw new CDbException(Yii::t('yii','The active record cannot be updated because it is new.')); + } + + /** + * Saves one or several counter columns for the current AR object. + * Note that this method differs from {@link updateCounters} in that it only + * saves the current AR object. + * An example usage is as follows: + * <pre> + * $postRecord=Post::model()->findByPk($postID); + * $postRecord->saveCounters(array('view_count'=>1)); + * </pre> + * Use negative values if you want to decrease the counters. + * @param array $counters the counters to be updated (column name=>increment value) + * @return boolean whether the saving is successful + * @see updateCounters + * @since 1.1.8 + */ + public function saveCounters($counters) + { + Yii::trace(get_class($this).'.saveCounters()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $table=$this->getTableSchema(); + $criteria=$builder->createPkCriteria($table,$this->getOldPrimaryKey()); + $command=$builder->createUpdateCounterCommand($this->getTableSchema(),$counters,$criteria); + if($command->execute()) + { + foreach($counters as $name=>$value) + $this->$name=$this->$name+$value; + return true; + } + else + return false; + } + + /** + * Deletes the row corresponding to this active record. + * @return boolean whether the deletion is successful. + * @throws CException if the record is new + */ + public function delete() + { + if(!$this->getIsNewRecord()) + { + Yii::trace(get_class($this).'.delete()','system.db.ar.CActiveRecord'); + if($this->beforeDelete()) + { + $result=$this->deleteByPk($this->getPrimaryKey())>0; + $this->afterDelete(); + return $result; + } + else + return false; + } + else + throw new CDbException(Yii::t('yii','The active record cannot be deleted because it is new.')); + } + + /** + * Repopulates this active record with the latest data. + * @return boolean whether the row still exists in the database. If true, the latest data will be populated to this active record. + */ + public function refresh() + { + Yii::trace(get_class($this).'.refresh()','system.db.ar.CActiveRecord'); + if(!$this->getIsNewRecord() && ($record=$this->findByPk($this->getPrimaryKey()))!==null) + { + $this->_attributes=array(); + $this->_related=array(); + foreach($this->getMetaData()->columns as $name=>$column) + { + if(property_exists($this,$name)) + $this->$name=$record->$name; + else + $this->_attributes[$name]=$record->$name; + } + return true; + } + else + return false; + } + + /** + * Compares current active record with another one. + * The comparison is made by comparing table name and the primary key values of the two active records. + * @param CActiveRecord $record record to compare to + * @return boolean whether the two active records refer to the same row in the database table. + */ + public function equals($record) + { + return $this->tableName()===$record->tableName() && $this->getPrimaryKey()===$record->getPrimaryKey(); + } + + /** + * Returns the primary key value. + * @return mixed the primary key value. An array (column name=>column value) is returned if the primary key is composite. + * If primary key is not defined, null will be returned. + */ + public function getPrimaryKey() + { + $table=$this->getMetaData()->tableSchema; + if(is_string($table->primaryKey)) + return $this->{$table->primaryKey}; + else if(is_array($table->primaryKey)) + { + $values=array(); + foreach($table->primaryKey as $name) + $values[$name]=$this->$name; + return $values; + } + else + return null; + } + + /** + * Sets the primary key value. + * After calling this method, the old primary key value can be obtained from {@link oldPrimaryKey}. + * @param mixed $value the new primary key value. If the primary key is composite, the new value + * should be provided as an array (column name=>column value). + * @since 1.1.0 + */ + public function setPrimaryKey($value) + { + $this->_pk=$this->getPrimaryKey(); + $table=$this->getMetaData()->tableSchema; + if(is_string($table->primaryKey)) + $this->{$table->primaryKey}=$value; + else if(is_array($table->primaryKey)) + { + foreach($table->primaryKey as $name) + $this->$name=$value[$name]; + } + } + + /** + * Returns the old primary key value. + * This refers to the primary key value that is populated into the record + * after executing a find method (e.g. find(), findAll()). + * The value remains unchanged even if the primary key attribute is manually assigned with a different value. + * @return mixed the old primary key value. An array (column name=>column value) is returned if the primary key is composite. + * If primary key is not defined, null will be returned. + * @since 1.1.0 + */ + public function getOldPrimaryKey() + { + return $this->_pk; + } + + /** + * Sets the old primary key value. + * @param mixed $value the old primary key value. + * @since 1.1.3 + */ + public function setOldPrimaryKey($value) + { + $this->_pk=$value; + } + + /** + * Performs the actual DB query and populates the AR objects with the query result. + * This method is mainly internally used by other AR query methods. + * @param CDbCriteria $criteria the query criteria + * @param boolean $all whether to return all data + * @return mixed the AR objects populated with the query result + * @since 1.1.7 + */ + protected function query($criteria,$all=false) + { + $this->beforeFind(); + $this->applyScopes($criteria); + if(empty($criteria->with)) + { + if(!$all) + $criteria->limit=1; + $command=$this->getCommandBuilder()->createFindCommand($this->getTableSchema(),$criteria); + return $all ? $this->populateRecords($command->queryAll(), true, $criteria->index) : $this->populateRecord($command->queryRow()); + } + else + { + $finder=new CActiveFinder($this,$criteria->with); + return $finder->query($criteria,$all); + } + } + + /** + * Applies the query scopes to the given criteria. + * This method merges {@link dbCriteria} with the given criteria parameter. + * It then resets {@link dbCriteria} to be null. + * @param CDbCriteria $criteria the query criteria. This parameter may be modified by merging {@link dbCriteria}. + */ + public function applyScopes(&$criteria) + { + if(!empty($criteria->scopes)) + { + $scs=$this->scopes(); + $c=$this->getDbCriteria(); + foreach((array)$criteria->scopes as $k=>$v) + { + if(is_integer($k)) + { + if(is_string($v)) + { + if(isset($scs[$v])) + { + $c->mergeWith($scs[$v],true); + continue; + } + $scope=$v; + $params=array(); + } + else if(is_array($v)) + { + $scope=key($v); + $params=current($v); + } + } + else if(is_string($k)) + { + $scope=$k; + $params=$v; + } + + call_user_func_array(array($this,$scope),(array)$params); + } + } + + if(isset($c) || ($c=$this->getDbCriteria(false))!==null) + { + $c->mergeWith($criteria); + $criteria=$c; + $this->_c=null; + } + } + + /** + * Returns the table alias to be used by the find methods. + * In relational queries, the returned table alias may vary according to + * the corresponding relation declaration. Also, the default table alias + * set by {@link setTableAlias} may be overridden by the applied scopes. + * @param boolean $quote whether to quote the alias name + * @param boolean $checkScopes whether to check if a table alias is defined in the applied scopes so far. + * This parameter must be set false when calling this method in {@link defaultScope}. + * An infinite loop would be formed otherwise. + * @return string the default table alias + * @since 1.1.1 + */ + public function getTableAlias($quote=false, $checkScopes=true) + { + if($checkScopes && ($criteria=$this->getDbCriteria(false))!==null && $criteria->alias!='') + $alias=$criteria->alias; + else + $alias=$this->_alias; + return $quote ? $this->getDbConnection()->getSchema()->quoteTableName($alias) : $alias; + } + + /** + * Sets the table alias to be used in queries. + * @param string $alias the table alias to be used in queries. The alias should NOT be quoted. + * @since 1.1.3 + */ + public function setTableAlias($alias) + { + $this->_alias=$alias; + } + + /** + * Finds a single active record with the specified condition. + * @param mixed $condition query condition or criteria. + * If a string, it is treated as query condition (the WHERE clause); + * If an array, it is treated as the initial values for constructing a {@link CDbCriteria} object; + * Otherwise, it should be an instance of {@link CDbCriteria}. + * @param array $params parameters to be bound to an SQL statement. + * This is only used when the first parameter is a string (query condition). + * In other cases, please use {@link CDbCriteria::params} to set parameters. + * @return CActiveRecord the record found. Null if no record is found. + */ + public function find($condition='',$params=array()) + { + Yii::trace(get_class($this).'.find()','system.db.ar.CActiveRecord'); + $criteria=$this->getCommandBuilder()->createCriteria($condition,$params); + return $this->query($criteria); + } + + /** + * Finds all active records satisfying the specified condition. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return array list of active records satisfying the specified condition. An empty array is returned if none is found. + */ + public function findAll($condition='',$params=array()) + { + Yii::trace(get_class($this).'.findAll()','system.db.ar.CActiveRecord'); + $criteria=$this->getCommandBuilder()->createCriteria($condition,$params); + return $this->query($criteria,true); + } + + /** + * Finds a single active record with the specified primary key. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return CActiveRecord the record found. Null if none is found. + */ + public function findByPk($pk,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.findByPk()','system.db.ar.CActiveRecord'); + $prefix=$this->getTableAlias(true).'.'; + $criteria=$this->getCommandBuilder()->createPkCriteria($this->getTableSchema(),$pk,$condition,$params,$prefix); + return $this->query($criteria); + } + + /** + * Finds all active records with the specified primary keys. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return array the records found. An empty array is returned if none is found. + */ + public function findAllByPk($pk,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.findAllByPk()','system.db.ar.CActiveRecord'); + $prefix=$this->getTableAlias(true).'.'; + $criteria=$this->getCommandBuilder()->createPkCriteria($this->getTableSchema(),$pk,$condition,$params,$prefix); + return $this->query($criteria,true); + } + + /** + * Finds a single active record that has the specified attribute values. + * See {@link find()} for detailed explanation about $condition and $params. + * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match. + * An attribute value can be an array which will be used to generate an IN condition. + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return CActiveRecord the record found. Null if none is found. + */ + public function findByAttributes($attributes,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.findByAttributes()','system.db.ar.CActiveRecord'); + $prefix=$this->getTableAlias(true).'.'; + $criteria=$this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(),$attributes,$condition,$params,$prefix); + return $this->query($criteria); + } + + /** + * Finds all active records that have the specified attribute values. + * See {@link find()} for detailed explanation about $condition and $params. + * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match. + * An attribute value can be an array which will be used to generate an IN condition. + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return array the records found. An empty array is returned if none is found. + */ + public function findAllByAttributes($attributes,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.findAllByAttributes()','system.db.ar.CActiveRecord'); + $prefix=$this->getTableAlias(true).'.'; + $criteria=$this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(),$attributes,$condition,$params,$prefix); + return $this->query($criteria,true); + } + + /** + * Finds a single active record with the specified SQL statement. + * @param string $sql the SQL statement + * @param array $params parameters to be bound to the SQL statement + * @return CActiveRecord the record found. Null if none is found. + */ + public function findBySql($sql,$params=array()) + { + Yii::trace(get_class($this).'.findBySql()','system.db.ar.CActiveRecord'); + $this->beforeFind(); + if(($criteria=$this->getDbCriteria(false))!==null && !empty($criteria->with)) + { + $this->_c=null; + $finder=new CActiveFinder($this,$criteria->with); + return $finder->findBySql($sql,$params); + } + else + { + $command=$this->getCommandBuilder()->createSqlCommand($sql,$params); + return $this->populateRecord($command->queryRow()); + } + } + + /** + * Finds all active records using the specified SQL statement. + * @param string $sql the SQL statement + * @param array $params parameters to be bound to the SQL statement + * @return array the records found. An empty array is returned if none is found. + */ + public function findAllBySql($sql,$params=array()) + { + Yii::trace(get_class($this).'.findAllBySql()','system.db.ar.CActiveRecord'); + $this->beforeFind(); + if(($criteria=$this->getDbCriteria(false))!==null && !empty($criteria->with)) + { + $this->_c=null; + $finder=new CActiveFinder($this,$criteria->with); + return $finder->findAllBySql($sql,$params); + } + else + { + $command=$this->getCommandBuilder()->createSqlCommand($sql,$params); + return $this->populateRecords($command->queryAll()); + } + } + + /** + * Finds the number of rows satisfying the specified query condition. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return string the number of rows satisfying the specified query condition. Note: type is string to keep max. precision. + */ + public function count($condition='',$params=array()) + { + Yii::trace(get_class($this).'.count()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $this->applyScopes($criteria); + + if(empty($criteria->with)) + return $builder->createCountCommand($this->getTableSchema(),$criteria)->queryScalar(); + else + { + $finder=new CActiveFinder($this,$criteria->with); + return $finder->count($criteria); + } + } + + /** + * Finds the number of rows that have the specified attribute values. + * See {@link find()} for detailed explanation about $condition and $params. + * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match. + * An attribute value can be an array which will be used to generate an IN condition. + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return string the number of rows satisfying the specified query condition. Note: type is string to keep max. precision. + * @since 1.1.4 + */ + public function countByAttributes($attributes,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.countByAttributes()','system.db.ar.CActiveRecord'); + $prefix=$this->getTableAlias(true).'.'; + $builder=$this->getCommandBuilder(); + $criteria=$builder->createColumnCriteria($this->getTableSchema(),$attributes,$condition,$params,$prefix); + $this->applyScopes($criteria); + + if(empty($criteria->with)) + return $builder->createCountCommand($this->getTableSchema(),$criteria)->queryScalar(); + else + { + $finder=new CActiveFinder($this,$criteria->with); + return $finder->count($criteria); + } + } + + /** + * Finds the number of rows using the given SQL statement. + * This is equivalent to calling {@link CDbCommand::queryScalar} with the specified + * SQL statement and the parameters. + * @param string $sql the SQL statement + * @param array $params parameters to be bound to the SQL statement + * @return string the number of rows using the given SQL statement. Note: type is string to keep max. precision. + */ + public function countBySql($sql,$params=array()) + { + Yii::trace(get_class($this).'.countBySql()','system.db.ar.CActiveRecord'); + return $this->getCommandBuilder()->createSqlCommand($sql,$params)->queryScalar(); + } + + /** + * Checks whether there is row satisfying the specified condition. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return boolean whether there is row satisfying the specified condition. + */ + public function exists($condition='',$params=array()) + { + Yii::trace(get_class($this).'.exists()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $table=$this->getTableSchema(); + $criteria->select='1'; + $criteria->limit=1; + $this->applyScopes($criteria); + + if(empty($criteria->with)) + return $builder->createFindCommand($table,$criteria)->queryRow()!==false; + else + { + $criteria->select='*'; + $finder=new CActiveFinder($this,$criteria->with); + return $finder->count($criteria)>0; + } + } + + /** + * Specifies which related objects should be eagerly loaded. + * This method takes variable number of parameters. Each parameter specifies + * the name of a relation or child-relation. For example, + * <pre> + * // find all posts together with their author and comments + * Post::model()->with('author','comments')->findAll(); + * // find all posts together with their author and the author's profile + * Post::model()->with('author','author.profile')->findAll(); + * </pre> + * The relations should be declared in {@link relations()}. + * + * By default, the options specified in {@link relations()} will be used + * to do relational query. In order to customize the options on the fly, + * we should pass an array parameter to the with() method. The array keys + * are relation names, and the array values are the corresponding query options. + * For example, + * <pre> + * Post::model()->with(array( + * 'author'=>array('select'=>'id, name'), + * 'comments'=>array('condition'=>'approved=1', 'order'=>'create_time'), + * ))->findAll(); + * </pre> + * + * @return CActiveRecord the AR object itself. + */ + public function with() + { + if(func_num_args()>0) + { + $with=func_get_args(); + if(is_array($with[0])) // the parameter is given as an array + $with=$with[0]; + if(!empty($with)) + $this->getDbCriteria()->mergeWith(array('with'=>$with)); + } + return $this; + } + + /** + * Sets {@link CDbCriteria::together} property to be true. + * This is only used in relational AR query. Please refer to {@link CDbCriteria::together} + * for more details. + * @return CActiveRecord the AR object itself + * @since 1.1.4 + */ + public function together() + { + $this->getDbCriteria()->together=true; + return $this; + } + + /** + * Updates records with the specified primary key(s). + * See {@link find()} for detailed explanation about $condition and $params. + * Note, the attributes are not checked for safety and validation is NOT performed. + * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). + * @param array $attributes list of attributes (name=>$value) to be updated + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return integer the number of rows being updated + */ + public function updateByPk($pk,$attributes,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.updateByPk()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $table=$this->getTableSchema(); + $criteria=$builder->createPkCriteria($table,$pk,$condition,$params); + $command=$builder->createUpdateCommand($table,$attributes,$criteria); + return $command->execute(); + } + + /** + * Updates records with the specified condition. + * See {@link find()} for detailed explanation about $condition and $params. + * Note, the attributes are not checked for safety and no validation is done. + * @param array $attributes list of attributes (name=>$value) to be updated + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return integer the number of rows being updated + */ + public function updateAll($attributes,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.updateAll()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $command=$builder->createUpdateCommand($this->getTableSchema(),$attributes,$criteria); + return $command->execute(); + } + + /** + * Updates one or several counter columns. + * Note, this updates all rows of data unless a condition or criteria is specified. + * See {@link find()} for detailed explanation about $condition and $params. + * @param array $counters the counters to be updated (column name=>increment value) + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return integer the number of rows being updated + * @see saveCounters + */ + public function updateCounters($counters,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.updateCounters()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $command=$builder->createUpdateCounterCommand($this->getTableSchema(),$counters,$criteria); + return $command->execute(); + } + + /** + * Deletes rows with the specified primary key. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return integer the number of rows deleted + */ + public function deleteByPk($pk,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.deleteByPk()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); + $command=$builder->createDeleteCommand($this->getTableSchema(),$criteria); + return $command->execute(); + } + + /** + * Deletes rows with the specified condition. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return integer the number of rows deleted + */ + public function deleteAll($condition='',$params=array()) + { + Yii::trace(get_class($this).'.deleteAll()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $command=$builder->createDeleteCommand($this->getTableSchema(),$criteria); + return $command->execute(); + } + + /** + * Deletes rows which match the specified attribute values. + * See {@link find()} for detailed explanation about $condition and $params. + * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match. + * An attribute value can be an array which will be used to generate an IN condition. + * @param mixed $condition query condition or criteria. + * @param array $params parameters to be bound to an SQL statement. + * @return integer number of rows affected by the execution. + */ + public function deleteAllByAttributes($attributes,$condition='',$params=array()) + { + Yii::trace(get_class($this).'.deleteAllByAttributes()','system.db.ar.CActiveRecord'); + $builder=$this->getCommandBuilder(); + $table=$this->getTableSchema(); + $criteria=$builder->createColumnCriteria($table,$attributes,$condition,$params); + $command=$builder->createDeleteCommand($table,$criteria); + return $command->execute(); + } + + /** + * Creates an active record with the given attributes. + * This method is internally used by the find methods. + * @param array $attributes attribute values (column name=>column value) + * @param boolean $callAfterFind whether to call {@link afterFind} after the record is populated. + * @return CActiveRecord the newly created active record. The class of the object is the same as the model class. + * Null is returned if the input data is false. + */ + public function populateRecord($attributes,$callAfterFind=true) + { + if($attributes!==false) + { + $record=$this->instantiate($attributes); + $record->setScenario('update'); + $record->init(); + $md=$record->getMetaData(); + foreach($attributes as $name=>$value) + { + if(property_exists($record,$name)) + $record->$name=$value; + else if(isset($md->columns[$name])) + $record->_attributes[$name]=$value; + } + $record->_pk=$record->getPrimaryKey(); + $record->attachBehaviors($record->behaviors()); + if($callAfterFind) + $record->afterFind(); + return $record; + } + else + return null; + } + + /** + * Creates a list of active records based on the input data. + * This method is internally used by the find methods. + * @param array $data list of attribute values for the active records. + * @param boolean $callAfterFind whether to call {@link afterFind} after each record is populated. + * @param string $index the name of the attribute whose value will be used as indexes of the query result array. + * If null, it means the array will be indexed by zero-based integers. + * @return array list of active records. + */ + public function populateRecords($data,$callAfterFind=true,$index=null) + { + $records=array(); + foreach($data as $attributes) + { + if(($record=$this->populateRecord($attributes,$callAfterFind))!==null) + { + if($index===null) + $records[]=$record; + else + $records[$record->$index]=$record; + } + } + return $records; + } + + /** + * Creates an active record instance. + * This method is called by {@link populateRecord} and {@link populateRecords}. + * You may override this method if the instance being created + * depends the attributes that are to be populated to the record. + * For example, by creating a record based on the value of a column, + * you may implement the so-called single-table inheritance mapping. + * @param array $attributes list of attribute values for the active records. + * @return CActiveRecord the active record + */ + protected function instantiate($attributes) + { + $class=get_class($this); + $model=new $class(null); + return $model; + } + + /** + * Returns whether there is an element at the specified offset. + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to check on + * @return boolean + */ + public function offsetExists($offset) + { + return $this->__isset($offset); + } +} + + +/** + * CBaseActiveRelation is the base class for all active relations. + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecord.php 3533 2012-01-08 22:07:55Z mdomba $ + * @package system.db.ar + */ +class CBaseActiveRelation extends CComponent +{ + /** + * @var string name of the related object + */ + public $name; + /** + * @var string name of the related active record class + */ + public $className; + /** + * @var mixed the foreign key in this relation + */ + public $foreignKey; + /** + * @var mixed list of column names (an array, or a string of names separated by commas) to be selected. + * Do not quote or prefix the column names unless they are used in an expression. + * In that case, you should prefix the column names with 'relationName.'. + */ + public $select='*'; + /** + * @var string WHERE clause. For {@link CActiveRelation} descendant classes, column names + * referenced in the condition should be disambiguated with prefix 'relationName.'. + */ + public $condition=''; + /** + * @var array the parameters that are to be bound to the condition. + * The keys are parameter placeholder names, and the values are parameter values. + */ + public $params=array(); + /** + * @var string GROUP BY clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix 'relationName.'. + */ + public $group=''; + /** + * @var string how to join with other tables. This refers to the JOIN clause in an SQL statement. + * For example, <code>'LEFT JOIN users ON users.id=authorID'</code>. + * @since 1.1.3 + */ + public $join=''; + /** + * @var string HAVING clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix 'relationName.'. + */ + public $having=''; + /** + * @var string ORDER BY clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix 'relationName.'. + */ + public $order=''; + + /** + * Constructor. + * @param string $name name of the relation + * @param string $className name of the related active record class + * @param string $foreignKey foreign key for this relation + * @param array $options additional options (name=>value). The keys must be the property names of this class. + */ + public function __construct($name,$className,$foreignKey,$options=array()) + { + $this->name=$name; + $this->className=$className; + $this->foreignKey=$foreignKey; + foreach($options as $name=>$value) + $this->$name=$value; + } + + /** + * Merges this relation with a criteria specified dynamically. + * @param array $criteria the dynamically specified criteria + * @param boolean $fromScope whether the criteria to be merged is from scopes + */ + public function mergeWith($criteria,$fromScope=false) + { + if($criteria instanceof CDbCriteria) + $criteria=$criteria->toArray(); + if(isset($criteria['select']) && $this->select!==$criteria['select']) + { + if($this->select==='*') + $this->select=$criteria['select']; + else if($criteria['select']!=='*') + { + $select1=is_string($this->select)?preg_split('/\s*,\s*/',trim($this->select),-1,PREG_SPLIT_NO_EMPTY):$this->select; + $select2=is_string($criteria['select'])?preg_split('/\s*,\s*/',trim($criteria['select']),-1,PREG_SPLIT_NO_EMPTY):$criteria['select']; + $this->select=array_merge($select1,array_diff($select2,$select1)); + } + } + + if(isset($criteria['condition']) && $this->condition!==$criteria['condition']) + { + if($this->condition==='') + $this->condition=$criteria['condition']; + else if($criteria['condition']!=='') + $this->condition="({$this->condition}) AND ({$criteria['condition']})"; + } + + if(isset($criteria['params']) && $this->params!==$criteria['params']) + $this->params=array_merge($this->params,$criteria['params']); + + if(isset($criteria['order']) && $this->order!==$criteria['order']) + { + if($this->order==='') + $this->order=$criteria['order']; + else if($criteria['order']!=='') + $this->order=$criteria['order'].', '.$this->order; + } + + if(isset($criteria['group']) && $this->group!==$criteria['group']) + { + if($this->group==='') + $this->group=$criteria['group']; + else if($criteria['group']!=='') + $this->group.=', '.$criteria['group']; + } + + if(isset($criteria['join']) && $this->join!==$criteria['join']) + { + if($this->join==='') + $this->join=$criteria['join']; + else if($criteria['join']!=='') + $this->join.=' '.$criteria['join']; + } + + if(isset($criteria['having']) && $this->having!==$criteria['having']) + { + if($this->having==='') + $this->having=$criteria['having']; + else if($criteria['having']!=='') + $this->having="({$this->having}) AND ({$criteria['having']})"; + } + } +} + + +/** + * CStatRelation represents a statistical relational query. + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecord.php 3533 2012-01-08 22:07:55Z mdomba $ + * @package system.db.ar + */ +class CStatRelation extends CBaseActiveRelation +{ + /** + * @var string the statistical expression. Defaults to 'COUNT(*)', meaning + * the count of child objects. + */ + public $select='COUNT(*)'; + /** + * @var mixed the default value to be assigned to those records that do not + * receive a statistical query result. Defaults to 0. + */ + public $defaultValue=0; + + /** + * Merges this relation with a criteria specified dynamically. + * @param array $criteria the dynamically specified criteria + * @param boolean $fromScope whether the criteria to be merged is from scopes + */ + public function mergeWith($criteria,$fromScope=false) + { + if($criteria instanceof CDbCriteria) + $criteria=$criteria->toArray(); + parent::mergeWith($criteria,$fromScope); + + if(isset($criteria['defaultValue'])) + $this->defaultValue=$criteria['defaultValue']; + } +} + + +/** + * CActiveRelation is the base class for representing active relations that bring back related objects. + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecord.php 3533 2012-01-08 22:07:55Z mdomba $ + * @package system.db.ar + * @since 1.0 + */ +class CActiveRelation extends CBaseActiveRelation +{ + /** + * @var string join type. Defaults to 'LEFT OUTER JOIN'. + */ + public $joinType='LEFT OUTER JOIN'; + /** + * @var string ON clause. The condition specified here will be appended to the joining condition using AND operator. + */ + public $on=''; + /** + * @var string the alias for the table that this relation refers to. Defaults to null, meaning + * the alias will be the same as the relation name. + */ + public $alias; + /** + * @var string|array specifies which related objects should be eagerly loaded when this related object is lazily loaded. + * For more details about this property, see {@link CActiveRecord::with()}. + */ + public $with=array(); + /** + * @var boolean whether this table should be joined with the primary table. + * When setting this property to be false, the table associated with this relation will + * appear in a separate JOIN statement. + * If this property is set true, then the corresponding table will ALWAYS be joined together + * with the primary table, no matter the primary table is limited or not. + * If this property is not set, the corresponding table will be joined with the primary table + * only when the primary table is not limited. + */ + public $together; + /** + * @var mixed scopes to apply + * Can be set to the one of the following: + * <ul> + * <li>Single scope: 'scopes'=>'scopeName'.</li> + * <li>Multiple scopes: 'scopes'=>array('scopeName1','scopeName2').</li> + * </ul> + * @since 1.1.9 + */ + public $scopes; + + /** + * Merges this relation with a criteria specified dynamically. + * @param array $criteria the dynamically specified criteria + * @param boolean $fromScope whether the criteria to be merged is from scopes + */ + public function mergeWith($criteria,$fromScope=false) + { + if($criteria instanceof CDbCriteria) + $criteria=$criteria->toArray(); + if($fromScope) + { + if(isset($criteria['condition']) && $this->on!==$criteria['condition']) + { + if($this->on==='') + $this->on=$criteria['condition']; + else if($criteria['condition']!=='') + $this->on="({$this->on}) AND ({$criteria['condition']})"; + } + unset($criteria['condition']); + } + + parent::mergeWith($criteria); + + if(isset($criteria['joinType'])) + $this->joinType=$criteria['joinType']; + + if(isset($criteria['on']) && $this->on!==$criteria['on']) + { + if($this->on==='') + $this->on=$criteria['on']; + else if($criteria['on']!=='') + $this->on="({$this->on}) AND ({$criteria['on']})"; + } + + if(isset($criteria['with'])) + $this->with=$criteria['with']; + + if(isset($criteria['alias'])) + $this->alias=$criteria['alias']; + + if(isset($criteria['together'])) + $this->together=$criteria['together']; + } +} + + +/** + * CBelongsToRelation represents the parameters specifying a BELONGS_TO relation. + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecord.php 3533 2012-01-08 22:07:55Z mdomba $ + * @package system.db.ar + * @since 1.0 + */ +class CBelongsToRelation extends CActiveRelation +{ +} + + +/** + * CHasOneRelation represents the parameters specifying a HAS_ONE relation. + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecord.php 3533 2012-01-08 22:07:55Z mdomba $ + * @package system.db.ar + * @since 1.0 + */ +class CHasOneRelation extends CActiveRelation +{ + /** + * @var string the name of the relation that should be used as the bridge to this relation. + * Defaults to null, meaning don't use any bridge. + * @since 1.1.7 + */ + public $through; +} + + +/** + * CHasManyRelation represents the parameters specifying a HAS_MANY relation. + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecord.php 3533 2012-01-08 22:07:55Z mdomba $ + * @package system.db.ar + * @since 1.0 + */ +class CHasManyRelation extends CActiveRelation +{ + /** + * @var integer limit of the rows to be selected. It is effective only for lazy loading this related object. Defaults to -1, meaning no limit. + */ + public $limit=-1; + /** + * @var integer offset of the rows to be selected. It is effective only for lazy loading this related object. Defaults to -1, meaning no offset. + */ + public $offset=-1; + /** + * @var string the name of the column that should be used as the key for storing related objects. + * Defaults to null, meaning using zero-based integer IDs. + */ + public $index; + /** + * @var string the name of the relation that should be used as the bridge to this relation. + * Defaults to null, meaning don't use any bridge. + * @since 1.1.7 + */ + public $through; + + /** + * Merges this relation with a criteria specified dynamically. + * @param array $criteria the dynamically specified criteria + * @param boolean $fromScope whether the criteria to be merged is from scopes + */ + public function mergeWith($criteria,$fromScope=false) + { + if($criteria instanceof CDbCriteria) + $criteria=$criteria->toArray(); + parent::mergeWith($criteria,$fromScope); + if(isset($criteria['limit']) && $criteria['limit']>0) + $this->limit=$criteria['limit']; + + if(isset($criteria['offset']) && $criteria['offset']>=0) + $this->offset=$criteria['offset']; + + if(isset($criteria['index'])) + $this->index=$criteria['index']; + } +} + + +/** + * CManyManyRelation represents the parameters specifying a MANY_MANY relation. + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecord.php 3533 2012-01-08 22:07:55Z mdomba $ + * @package system.db.ar + * @since 1.0 + */ +class CManyManyRelation extends CHasManyRelation +{ +} + + +/** + * CActiveRecordMetaData represents the meta-data for an Active Record class. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecord.php 3533 2012-01-08 22:07:55Z mdomba $ + * @package system.db.ar + * @since 1.0 + */ +class CActiveRecordMetaData +{ + /** + * @var CDbTableSchema the table schema information + */ + public $tableSchema; + /** + * @var array table columns + */ + public $columns; + /** + * @var array list of relations + */ + public $relations=array(); + /** + * @var array attribute default values + */ + public $attributeDefaults=array(); + + private $_model; + + /** + * Constructor. + * @param CActiveRecord $model the model instance + */ + public function __construct($model) + { + $this->_model=$model; + + $tableName=$model->tableName(); + if(($table=$model->getDbConnection()->getSchema()->getTable($tableName))===null) + throw new CDbException(Yii::t('yii','The table "{table}" for active record class "{class}" cannot be found in the database.', + array('{class}'=>get_class($model),'{table}'=>$tableName))); + if($table->primaryKey===null) + { + $table->primaryKey=$model->primaryKey(); + if(is_string($table->primaryKey) && isset($table->columns[$table->primaryKey])) + $table->columns[$table->primaryKey]->isPrimaryKey=true; + else if(is_array($table->primaryKey)) + { + foreach($table->primaryKey as $name) + { + if(isset($table->columns[$name])) + $table->columns[$name]->isPrimaryKey=true; + } + } + } + $this->tableSchema=$table; + $this->columns=$table->columns; + + foreach($table->columns as $name=>$column) + { + if(!$column->isPrimaryKey && $column->defaultValue!==null) + $this->attributeDefaults[$name]=$column->defaultValue; + } + + foreach($model->relations() as $name=>$config) + { + $this->addRelation($name,$config); + } + } + + /** + * Adds a relation. + * + * $config is an array with three elements: + * relation type, the related active record class and the foreign key. + * + * @throws CDbException + * @param string $name $name Name of the relation. + * @param array $config $config Relation parameters. + * @return void + * @since 1.1.2 + */ + public function addRelation($name,$config) + { + if(isset($config[0],$config[1],$config[2])) // relation class, AR class, FK + $this->relations[$name]=new $config[0]($name,$config[1],$config[2],array_slice($config,3)); + else + throw new CDbException(Yii::t('yii','Active record "{class}" has an invalid configuration for relation "{relation}". It must specify the relation type, the related active record class and the foreign key.', array('{class}'=>get_class($this->_model),'{relation}'=>$name))); + } + + /** + * Checks if there is a relation with specified name defined. + * + * @param string $name $name Name of the relation. + * @return boolean + * @since 1.1.2 + */ + public function hasRelation($name) + { + return isset($this->relations[$name]); + } + + /** + * Deletes a relation with specified name. + * + * @param string $name $name + * @return void + * @since 1.1.2 + */ + public function removeRelation($name) + { + unset($this->relations[$name]); + } +} diff --git a/framework/db/ar/CActiveRecordBehavior.php b/framework/db/ar/CActiveRecordBehavior.php new file mode 100644 index 0000000..cded48d --- /dev/null +++ b/framework/db/ar/CActiveRecordBehavior.php @@ -0,0 +1,97 @@ +<?php +/** + * CActiveRecordBehavior class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CActiveRecordBehavior is the base class for behaviors that can be attached to {@link CActiveRecord}. + * Compared with {@link CModelBehavior}, CActiveRecordBehavior attaches to more events + * that are only defined by {@link CActiveRecord}. + * + * @property CActiveRecord $owner The owner AR that this behavior is attached to. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveRecordBehavior.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.ar + */ +class CActiveRecordBehavior extends CModelBehavior +{ + /** + * Declares events and the corresponding event handler methods. + * If you override this method, make sure you merge the parent result to the return value. + * @return array events (array keys) and the corresponding event handler methods (array values). + * @see CBehavior::events + */ + public function events() + { + return array_merge(parent::events(), array( + 'onBeforeSave'=>'beforeSave', + 'onAfterSave'=>'afterSave', + 'onBeforeDelete'=>'beforeDelete', + 'onAfterDelete'=>'afterDelete', + 'onBeforeFind'=>'beforeFind', + 'onAfterFind'=>'afterFind', + )); + } + + /** + * Responds to {@link CActiveRecord::onBeforeSave} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * You may set {@link CModelEvent::isValid} to be false to quit the saving process. + * @param CModelEvent $event event parameter + */ + public function beforeSave($event) + { + } + + /** + * Responds to {@link CActiveRecord::onAfterSave} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CModelEvent $event event parameter + */ + public function afterSave($event) + { + } + + /** + * Responds to {@link CActiveRecord::onBeforeDelete} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * You may set {@link CModelEvent::isValid} to be false to quit the deletion process. + * @param CEvent $event event parameter + */ + public function beforeDelete($event) + { + } + + /** + * Responds to {@link CActiveRecord::onAfterDelete} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CEvent $event event parameter + */ + public function afterDelete($event) + { + } + + /** + * Responds to {@link CActiveRecord::onBeforeFind} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CEvent $event event parameter + */ + public function beforeFind($event) + { + } + + /** + * Responds to {@link CActiveRecord::onAfterFind} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CEvent $event event parameter + */ + public function afterFind($event) + { + } +} diff --git a/framework/db/schema/CDbColumnSchema.php b/framework/db/schema/CDbColumnSchema.php new file mode 100644 index 0000000..642094b --- /dev/null +++ b/framework/db/schema/CDbColumnSchema.php @@ -0,0 +1,148 @@ +<?php +/** + * CDbColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbColumnSchema class describes the column meta data of a database table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbColumnSchema.php 3558 2012-02-09 17:39:04Z alexander.makarow $ + * @package system.db.schema + * @since 1.0 + */ +class CDbColumnSchema extends CComponent +{ + /** + * @var string name of this column (without quotes). + */ + public $name; + /** + * @var string raw name of this column. This is the quoted name that can be used in SQL queries. + */ + public $rawName; + /** + * @var boolean whether this column can be null. + */ + public $allowNull; + /** + * @var string the DB type of this column. + */ + public $dbType; + /** + * @var string the PHP type of this column. + */ + public $type; + /** + * @var mixed default value of this column + */ + public $defaultValue; + /** + * @var integer size of the column. + */ + public $size; + /** + * @var integer precision of the column data, if it is numeric. + */ + public $precision; + /** + * @var integer scale of the column data, if it is numeric. + */ + public $scale; + /** + * @var boolean whether this column is a primary key + */ + public $isPrimaryKey; + /** + * @var boolean whether this column is a foreign key + */ + public $isForeignKey; + /** + * @var boolean whether this column is auto-incremental + * @since 1.1.7 + */ + public $autoIncrement=false; + + + /** + * Initializes the column with its DB type and default value. + * This sets up the column's PHP type, size, precision, scale as well as default value. + * @param string $dbType the column's DB type + * @param mixed $defaultValue the default value + */ + public function init($dbType, $defaultValue) + { + $this->dbType=$dbType; + $this->extractType($dbType); + $this->extractLimit($dbType); + if($defaultValue!==null) + $this->extractDefault($defaultValue); + } + + /** + * Extracts the PHP type from DB type. + * @param string $dbType DB type + */ + protected function extractType($dbType) + { + if(stripos($dbType,'int')!==false && stripos($dbType,'unsigned int')===false) + $this->type='integer'; + else if(stripos($dbType,'bool')!==false) + $this->type='boolean'; + else if(preg_match('/(real|floa|doub)/i',$dbType)) + $this->type='double'; + else + $this->type='string'; + } + + /** + * Extracts size, precision and scale information from column's DB type. + * @param string $dbType the column's DB type + */ + protected function extractLimit($dbType) + { + if(strpos($dbType,'(') && preg_match('/\((.*)\)/',$dbType,$matches)) + { + $values=explode(',',$matches[1]); + $this->size=$this->precision=(int)$values[0]; + if(isset($values[1])) + $this->scale=(int)$values[1]; + } + } + + /** + * Extracts the default value for the column. + * The value is typecasted to correct PHP type. + * @param mixed $defaultValue the default value obtained from metadata + */ + protected function extractDefault($defaultValue) + { + $this->defaultValue=$this->typecast($defaultValue); + } + + /** + * Converts the input value to the type that this column is of. + * @param mixed $value input value + * @return mixed converted value + */ + public function typecast($value) + { + if(gettype($value)===$this->type || $value===null || $value instanceof CDbExpression) + return $value; + if($value==='' && $this->allowNull) + return $this->type==='string' ? '' : null; + switch($this->type) + { + case 'string': return (string)$value; + case 'integer': return (integer)$value; + case 'boolean': return (boolean)$value; + case 'double': + default: return $value; + } + } +} diff --git a/framework/db/schema/CDbCommandBuilder.php b/framework/db/schema/CDbCommandBuilder.php new file mode 100644 index 0000000..bde4d06 --- /dev/null +++ b/framework/db/schema/CDbCommandBuilder.php @@ -0,0 +1,735 @@ +<?php +/** + * CDbCommandBuilder class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbCommandBuilder provides basic methods to create query commands for tables. + * + * @property CDbConnection $dbConnection Database connection. + * @property CDbSchema $schema The schema for this command builder. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbCommandBuilder.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema + * @since 1.0 + */ +class CDbCommandBuilder extends CComponent +{ + const PARAM_PREFIX=':yp'; + + private $_schema; + private $_connection; + + /** + * @param CDbSchema $schema the schema for this command builder + */ + public function __construct($schema) + { + $this->_schema=$schema; + $this->_connection=$schema->getDbConnection(); + } + + /** + * @return CDbConnection database connection. + */ + public function getDbConnection() + { + return $this->_connection; + } + + /** + * @return CDbSchema the schema for this command builder. + */ + public function getSchema() + { + return $this->_schema; + } + + /** + * Returns the last insertion ID for the specified table. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @return mixed last insertion id. Null is returned if no sequence name. + */ + public function getLastInsertID($table) + { + $this->ensureTable($table); + if($table->sequenceName!==null) + return $this->_connection->getLastInsertID($table->sequenceName); + else + return null; + } + + /** + * Creates a SELECT command for a single table. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param CDbCriteria $criteria the query criteria + * @param string $alias the alias name of the primary table. Defaults to 't'. + * @return CDbCommand query command. + */ + public function createFindCommand($table,$criteria,$alias='t') + { + $this->ensureTable($table); + $select=is_array($criteria->select) ? implode(', ',$criteria->select) : $criteria->select; + if($criteria->alias!='') + $alias=$criteria->alias; + $alias=$this->_schema->quoteTableName($alias); + + // issue 1432: need to expand * when SQL has JOIN + if($select==='*' && !empty($criteria->join)) + { + $prefix=$alias.'.'; + $select=array(); + foreach($table->getColumnNames() as $name) + $select[]=$prefix.$this->_schema->quoteColumnName($name); + $select=implode(', ',$select); + } + + $sql=($criteria->distinct ? 'SELECT DISTINCT':'SELECT')." {$select} FROM {$table->rawName} $alias"; + $sql=$this->applyJoin($sql,$criteria->join); + $sql=$this->applyCondition($sql,$criteria->condition); + $sql=$this->applyGroup($sql,$criteria->group); + $sql=$this->applyHaving($sql,$criteria->having); + $sql=$this->applyOrder($sql,$criteria->order); + $sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset); + $command=$this->_connection->createCommand($sql); + $this->bindValues($command,$criteria->params); + return $command; + } + + /** + * Creates a COUNT(*) command for a single table. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param CDbCriteria $criteria the query criteria + * @param string $alias the alias name of the primary table. Defaults to 't'. + * @return CDbCommand query command. + */ + public function createCountCommand($table,$criteria,$alias='t') + { + $this->ensureTable($table); + if($criteria->alias!='') + $alias=$criteria->alias; + $alias=$this->_schema->quoteTableName($alias); + + if(!empty($criteria->group) || !empty($criteria->having)) + { + $select=is_array($criteria->select) ? implode(', ',$criteria->select) : $criteria->select; + if($criteria->alias!='') + $alias=$criteria->alias; + $sql=($criteria->distinct ? 'SELECT DISTINCT':'SELECT')." {$select} FROM {$table->rawName} $alias"; + $sql=$this->applyJoin($sql,$criteria->join); + $sql=$this->applyCondition($sql,$criteria->condition); + $sql=$this->applyGroup($sql,$criteria->group); + $sql=$this->applyHaving($sql,$criteria->having); + $sql="SELECT COUNT(*) FROM ($sql) sq"; + } + else + { + if(is_string($criteria->select) && stripos($criteria->select,'count')===0) + $sql="SELECT ".$criteria->select; + else if($criteria->distinct) + { + if(is_array($table->primaryKey)) + { + $pk=array(); + foreach($table->primaryKey as $key) + $pk[]=$alias.'.'.$key; + $pk=implode(', ',$pk); + } + else + $pk=$alias.'.'.$table->primaryKey; + $sql="SELECT COUNT(DISTINCT $pk)"; + } + else + $sql="SELECT COUNT(*)"; + $sql.=" FROM {$table->rawName} $alias"; + $sql=$this->applyJoin($sql,$criteria->join); + $sql=$this->applyCondition($sql,$criteria->condition); + } + + $command=$this->_connection->createCommand($sql); + $this->bindValues($command,$criteria->params); + return $command; + } + + /** + * Creates a DELETE command. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param CDbCriteria $criteria the query criteria + * @return CDbCommand delete command. + */ + public function createDeleteCommand($table,$criteria) + { + $this->ensureTable($table); + $sql="DELETE FROM {$table->rawName}"; + $sql=$this->applyJoin($sql,$criteria->join); + $sql=$this->applyCondition($sql,$criteria->condition); + $sql=$this->applyGroup($sql,$criteria->group); + $sql=$this->applyHaving($sql,$criteria->having); + $sql=$this->applyOrder($sql,$criteria->order); + $sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset); + $command=$this->_connection->createCommand($sql); + $this->bindValues($command,$criteria->params); + return $command; + } + + /** + * Creates an INSERT command. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param array $data data to be inserted (column name=>column value). If a key is not a valid column name, the corresponding value will be ignored. + * @return CDbCommand insert command + */ + public function createInsertCommand($table,$data) + { + $this->ensureTable($table); + $fields=array(); + $values=array(); + $placeholders=array(); + $i=0; + foreach($data as $name=>$value) + { + if(($column=$table->getColumn($name))!==null && ($value!==null || $column->allowNull)) + { + $fields[]=$column->rawName; + if($value instanceof CDbExpression) + { + $placeholders[]=$value->expression; + foreach($value->params as $n=>$v) + $values[$n]=$v; + } + else + { + $placeholders[]=self::PARAM_PREFIX.$i; + $values[self::PARAM_PREFIX.$i]=$column->typecast($value); + $i++; + } + } + } + if($fields===array()) + { + $pks=is_array($table->primaryKey) ? $table->primaryKey : array($table->primaryKey); + foreach($pks as $pk) + { + $fields[]=$table->getColumn($pk)->rawName; + $placeholders[]='NULL'; + } + } + $sql="INSERT INTO {$table->rawName} (".implode(', ',$fields).') VALUES ('.implode(', ',$placeholders).')'; + $command=$this->_connection->createCommand($sql); + + foreach($values as $name=>$value) + $command->bindValue($name,$value); + + return $command; + } + + /** + * Creates an UPDATE command. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param array $data list of columns to be updated (name=>value) + * @param CDbCriteria $criteria the query criteria + * @return CDbCommand update command. + */ + public function createUpdateCommand($table,$data,$criteria) + { + $this->ensureTable($table); + $fields=array(); + $values=array(); + $bindByPosition=isset($criteria->params[0]); + $i=0; + foreach($data as $name=>$value) + { + if(($column=$table->getColumn($name))!==null) + { + if($value instanceof CDbExpression) + { + $fields[]=$column->rawName.'='.$value->expression; + foreach($value->params as $n=>$v) + $values[$n]=$v; + } + else if($bindByPosition) + { + $fields[]=$column->rawName.'=?'; + $values[]=$column->typecast($value); + } + else + { + $fields[]=$column->rawName.'='.self::PARAM_PREFIX.$i; + $values[self::PARAM_PREFIX.$i]=$column->typecast($value); + $i++; + } + } + } + if($fields===array()) + throw new CDbException(Yii::t('yii','No columns are being updated for table "{table}".', + array('{table}'=>$table->name))); + $sql="UPDATE {$table->rawName} SET ".implode(', ',$fields); + $sql=$this->applyJoin($sql,$criteria->join); + $sql=$this->applyCondition($sql,$criteria->condition); + $sql=$this->applyOrder($sql,$criteria->order); + $sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset); + + $command=$this->_connection->createCommand($sql); + $this->bindValues($command,array_merge($values,$criteria->params)); + + return $command; + } + + /** + * Creates an UPDATE command that increments/decrements certain columns. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param array $counters counters to be updated (counter increments/decrements indexed by column names.) + * @param CDbCriteria $criteria the query criteria + * @return CDbCommand the created command + * @throws CException if no counter is specified + */ + public function createUpdateCounterCommand($table,$counters,$criteria) + { + $this->ensureTable($table); + $fields=array(); + foreach($counters as $name=>$value) + { + if(($column=$table->getColumn($name))!==null) + { + $value=(int)$value; + if($value<0) + $fields[]="{$column->rawName}={$column->rawName}-".(-$value); + else + $fields[]="{$column->rawName}={$column->rawName}+".$value; + } + } + if($fields!==array()) + { + $sql="UPDATE {$table->rawName} SET ".implode(', ',$fields); + $sql=$this->applyJoin($sql,$criteria->join); + $sql=$this->applyCondition($sql,$criteria->condition); + $sql=$this->applyOrder($sql,$criteria->order); + $sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset); + $command=$this->_connection->createCommand($sql); + $this->bindValues($command,$criteria->params); + return $command; + } + else + throw new CDbException(Yii::t('yii','No counter columns are being updated for table "{table}".', + array('{table}'=>$table->name))); + } + + /** + * Creates a command based on a given SQL statement. + * @param string $sql the explicitly specified SQL statement + * @param array $params parameters that will be bound to the SQL statement + * @return CDbCommand the created command + */ + public function createSqlCommand($sql,$params=array()) + { + $command=$this->_connection->createCommand($sql); + $this->bindValues($command,$params); + return $command; + } + + /** + * Alters the SQL to apply JOIN clause. + * @param string $sql the SQL statement to be altered + * @param string $join the JOIN clause (starting with join type, such as INNER JOIN) + * @return string the altered SQL statement + */ + public function applyJoin($sql,$join) + { + if($join!='') + return $sql.' '.$join; + else + return $sql; + } + + /** + * Alters the SQL to apply WHERE clause. + * @param string $sql the SQL statement without WHERE clause + * @param string $condition the WHERE clause (without WHERE keyword) + * @return string the altered SQL statement + */ + public function applyCondition($sql,$condition) + { + if($condition!='') + return $sql.' WHERE '.$condition; + else + return $sql; + } + + /** + * Alters the SQL to apply ORDER BY. + * @param string $sql SQL statement without ORDER BY. + * @param string $orderBy column ordering + * @return string modified SQL applied with ORDER BY. + */ + public function applyOrder($sql,$orderBy) + { + if($orderBy!='') + return $sql.' ORDER BY '.$orderBy; + else + return $sql; + } + + /** + * Alters the SQL to apply LIMIT and OFFSET. + * Default implementation is applicable for PostgreSQL, MySQL and SQLite. + * @param string $sql SQL query string without LIMIT and OFFSET. + * @param integer $limit maximum number of rows, -1 to ignore limit. + * @param integer $offset row offset, -1 to ignore offset. + * @return string SQL with LIMIT and OFFSET + */ + public function applyLimit($sql,$limit,$offset) + { + if($limit>=0) + $sql.=' LIMIT '.(int)$limit; + if($offset>0) + $sql.=' OFFSET '.(int)$offset; + return $sql; + } + + /** + * Alters the SQL to apply GROUP BY. + * @param string $sql SQL query string without GROUP BY. + * @param string $group GROUP BY + * @return string SQL with GROUP BY. + */ + public function applyGroup($sql,$group) + { + if($group!='') + return $sql.' GROUP BY '.$group; + else + return $sql; + } + + /** + * Alters the SQL to apply HAVING. + * @param string $sql SQL query string without HAVING + * @param string $having HAVING + * @return string SQL with HAVING + */ + public function applyHaving($sql,$having) + { + if($having!='') + return $sql.' HAVING '.$having; + else + return $sql; + } + + /** + * Binds parameter values for an SQL command. + * @param CDbCommand $command database command + * @param array $values values for binding (integer-indexed array for question mark placeholders, string-indexed array for named placeholders) + */ + public function bindValues($command, $values) + { + if(($n=count($values))===0) + return; + if(isset($values[0])) // question mark placeholders + { + for($i=0;$i<$n;++$i) + $command->bindValue($i+1,$values[$i]); + } + else // named placeholders + { + foreach($values as $name=>$value) + { + if($name[0]!==':') + $name=':'.$name; + $command->bindValue($name,$value); + } + } + } + + /** + * Creates a query criteria. + * @param mixed $condition query condition or criteria. + * If a string, it is treated as query condition (the WHERE clause); + * If an array, it is treated as the initial values for constructing a {@link CDbCriteria} object; + * Otherwise, it should be an instance of {@link CDbCriteria}. + * @param array $params parameters to be bound to an SQL statement. + * This is only used when the first parameter is a string (query condition). + * In other cases, please use {@link CDbCriteria::params} to set parameters. + * @return CDbCriteria the created query criteria + * @throws CException if the condition is not string, array and CDbCriteria + */ + public function createCriteria($condition='',$params=array()) + { + if(is_array($condition)) + $criteria=new CDbCriteria($condition); + else if($condition instanceof CDbCriteria) + $criteria=clone $condition; + else + { + $criteria=new CDbCriteria; + $criteria->condition=$condition; + $criteria->params=$params; + } + return $criteria; + } + + /** + * Creates a query criteria with the specified primary key. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param mixed $pk primary key value(s). Use array for multiple primary keys. For composite key, each key value must be an array (column name=>column value). + * @param mixed $condition query condition or criteria. + * If a string, it is treated as query condition; + * If an array, it is treated as the initial values for constructing a {@link CDbCriteria}; + * Otherwise, it should be an instance of {@link CDbCriteria}. + * @param array $params parameters to be bound to an SQL statement. + * This is only used when the second parameter is a string (query condition). + * In other cases, please use {@link CDbCriteria::params} to set parameters. + * @param string $prefix column prefix (ended with dot). If null, it will be the table name + * @return CDbCriteria the created query criteria + */ + public function createPkCriteria($table,$pk,$condition='',$params=array(),$prefix=null) + { + $this->ensureTable($table); + $criteria=$this->createCriteria($condition,$params); + if($criteria->alias!='') + $prefix=$this->_schema->quoteTableName($criteria->alias).'.'; + if(!is_array($pk)) // single key + $pk=array($pk); + if(is_array($table->primaryKey) && !isset($pk[0]) && $pk!==array()) // single composite key + $pk=array($pk); + $condition=$this->createInCondition($table,$table->primaryKey,$pk,$prefix); + if($criteria->condition!='') + $criteria->condition=$condition.' AND ('.$criteria->condition.')'; + else + $criteria->condition=$condition; + + return $criteria; + } + + /** + * Generates the expression for selecting rows of specified primary key values. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param array $values list of primary key values to be selected within + * @param string $prefix column prefix (ended with dot). If null, it will be the table name + * @return string the expression for selection + */ + public function createPkCondition($table,$values,$prefix=null) + { + $this->ensureTable($table); + return $this->createInCondition($table,$table->primaryKey,$values,$prefix); + } + + /** + * Creates a query criteria with the specified column values. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param array $columns column values that should be matched in the query (name=>value) + * @param mixed $condition query condition or criteria. + * If a string, it is treated as query condition; + * If an array, it is treated as the initial values for constructing a {@link CDbCriteria}; + * Otherwise, it should be an instance of {@link CDbCriteria}. + * @param array $params parameters to be bound to an SQL statement. + * This is only used when the third parameter is a string (query condition). + * In other cases, please use {@link CDbCriteria::params} to set parameters. + * @param string $prefix column prefix (ended with dot). If null, it will be the table name + * @return CDbCriteria the created query criteria + */ + public function createColumnCriteria($table,$columns,$condition='',$params=array(),$prefix=null) + { + $this->ensureTable($table); + $criteria=$this->createCriteria($condition,$params); + if($criteria->alias!='') + $prefix=$this->_schema->quoteTableName($criteria->alias).'.'; + $bindByPosition=isset($criteria->params[0]); + $conditions=array(); + $values=array(); + $i=0; + if($prefix===null) + $prefix=$table->rawName.'.'; + foreach($columns as $name=>$value) + { + if(($column=$table->getColumn($name))!==null) + { + if(is_array($value)) + $conditions[]=$this->createInCondition($table,$name,$value,$prefix); + else if($value!==null) + { + if($bindByPosition) + { + $conditions[]=$prefix.$column->rawName.'=?'; + $values[]=$value; + } + else + { + $conditions[]=$prefix.$column->rawName.'='.self::PARAM_PREFIX.$i; + $values[self::PARAM_PREFIX.$i]=$value; + $i++; + } + } + else + $conditions[]=$prefix.$column->rawName.' IS NULL'; + } + else + throw new CDbException(Yii::t('yii','Table "{table}" does not have a column named "{column}".', + array('{table}'=>$table->name,'{column}'=>$name))); + } + $criteria->params=array_merge($values,$criteria->params); + if(isset($conditions[0])) + { + if($criteria->condition!='') + $criteria->condition=implode(' AND ',$conditions).' AND ('.$criteria->condition.')'; + else + $criteria->condition=implode(' AND ',$conditions); + } + return $criteria; + } + + /** + * Generates the expression for searching the specified keywords within a list of columns. + * The search expression is generated using the 'LIKE' SQL syntax. + * Every word in the keywords must be present and appear in at least one of the columns. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param array $columns list of column names for potential search condition. + * @param mixed $keywords search keywords. This can be either a string with space-separated keywords or an array of keywords. + * @param string $prefix optional column prefix (with dot at the end). If null, the table name will be used as the prefix. + * @param boolean $caseSensitive whether the search is case-sensitive. Defaults to true. + * @return string SQL search condition matching on a set of columns. An empty string is returned + * if either the column array or the keywords are empty. + */ + public function createSearchCondition($table,$columns,$keywords,$prefix=null,$caseSensitive=true) + { + $this->ensureTable($table); + if(!is_array($keywords)) + $keywords=preg_split('/\s+/u',$keywords,-1,PREG_SPLIT_NO_EMPTY); + if(empty($keywords)) + return ''; + if($prefix===null) + $prefix=$table->rawName.'.'; + $conditions=array(); + foreach($columns as $name) + { + if(($column=$table->getColumn($name))===null) + throw new CDbException(Yii::t('yii','Table "{table}" does not have a column named "{column}".', + array('{table}'=>$table->name,'{column}'=>$name))); + $condition=array(); + foreach($keywords as $keyword) + { + $keyword='%'.strtr($keyword,array('%'=>'\%', '_'=>'\_')).'%'; + if($caseSensitive) + $condition[]=$prefix.$column->rawName.' LIKE '.$this->_connection->quoteValue('%'.$keyword.'%'); + else + $condition[]='LOWER('.$prefix.$column->rawName.') LIKE LOWER('.$this->_connection->quoteValue('%'.$keyword.'%').')'; + } + $conditions[]=implode(' AND ',$condition); + } + return '('.implode(' OR ',$conditions).')'; + } + + /** + * Generates the expression for selecting rows of specified primary key values. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param mixed $columnName the column name(s). It can be either a string indicating a single column + * or an array of column names. If the latter, it stands for a composite key. + * @param array $values list of key values to be selected within + * @param string $prefix column prefix (ended with dot). If null, it will be the table name + * @return string the expression for selection + */ + public function createInCondition($table,$columnName,$values,$prefix=null) + { + if(($n=count($values))<1) + return '0=1'; + + $this->ensureTable($table); + + if($prefix===null) + $prefix=$table->rawName.'.'; + + $db=$this->_connection; + + if(is_array($columnName) && count($columnName)===1) + $columnName=reset($columnName); + + if(is_string($columnName)) // simple key + { + if(!isset($table->columns[$columnName])) + throw new CDbException(Yii::t('yii','Table "{table}" does not have a column named "{column}".', + array('{table}'=>$table->name, '{column}'=>$columnName))); + $column=$table->columns[$columnName]; + + foreach($values as &$value) + { + $value=$column->typecast($value); + if(is_string($value)) + $value=$db->quoteValue($value); + } + if($n===1) + return $prefix.$column->rawName.($values[0]===null?' IS NULL':'='.$values[0]); + else + return $prefix.$column->rawName.' IN ('.implode(', ',$values).')'; + } + else if(is_array($columnName)) // composite key: $values=array(array('pk1'=>'v1','pk2'=>'v2'),array(...)) + { + foreach($columnName as $name) + { + if(!isset($table->columns[$name])) + throw new CDbException(Yii::t('yii','Table "{table}" does not have a column named "{column}".', + array('{table}'=>$table->name, '{column}'=>$name))); + + for($i=0;$i<$n;++$i) + { + if(isset($values[$i][$name])) + { + $value=$table->columns[$name]->typecast($values[$i][$name]); + if(is_string($value)) + $values[$i][$name]=$db->quoteValue($value); + else + $values[$i][$name]=$value; + } + else + throw new CDbException(Yii::t('yii','The value for the column "{column}" is not supplied when querying the table "{table}".', + array('{table}'=>$table->name,'{column}'=>$name))); + } + } + if(count($values)===1) + { + $entries=array(); + foreach($values[0] as $name=>$value) + $entries[]=$prefix.$table->columns[$name]->rawName.($value===null?' IS NULL':'='.$value); + return implode(' AND ',$entries); + } + + return $this->createCompositeInCondition($table,$values,$prefix); + } + else + throw new CDbException(Yii::t('yii','Column name must be either a string or an array.')); + } + + /** + * Generates the expression for selecting rows with specified composite key values. + * @param CDbTableSchema $table the table schema + * @param array $values list of primary key values to be selected within + * @param string $prefix column prefix (ended with dot) + * @return string the expression for selection + */ + protected function createCompositeInCondition($table,$values,$prefix) + { + $keyNames=array(); + foreach(array_keys($values[0]) as $name) + $keyNames[]=$prefix.$table->columns[$name]->rawName; + $vs=array(); + foreach($values as $value) + $vs[]='('.implode(', ',$value).')'; + return '('.implode(', ',$keyNames).') IN ('.implode(', ',$vs).')'; + } + + /** + * Checks if the parameter is a valid table schema. + * If it is a string, the corresponding table schema will be retrieved. + * @param mixed $table table schema ({@link CDbTableSchema}) or table name (string). + * If this refers to a valid table name, this parameter will be returned with the corresponding table schema. + * @throws CDbException if the table name is not valid + */ + protected function ensureTable(&$table) + { + if(is_string($table) && ($table=$this->_schema->getTable($tableName=$table))===null) + throw new CDbException(Yii::t('yii','Table "{table}" does not exist.', + array('{table}'=>$tableName))); + } +}
\ No newline at end of file diff --git a/framework/db/schema/CDbCriteria.php b/framework/db/schema/CDbCriteria.php new file mode 100644 index 0000000..88741b7 --- /dev/null +++ b/framework/db/schema/CDbCriteria.php @@ -0,0 +1,607 @@ +<?php +/** + * CDbCriteria class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbCriteria represents a query criteria, such as conditions, ordering by, limit/offset. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbCriteria.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema + * @since 1.0 + */ +class CDbCriteria extends CComponent +{ + const PARAM_PREFIX=':ycp'; + /** + * @var integer the global counter for anonymous binding parameters. + * This counter is used for generating the name for the anonymous parameters. + */ + public static $paramCount=0; + /** + * @var mixed the columns being selected. This refers to the SELECT clause in an SQL + * statement. The property can be either a string (column names separated by commas) + * or an array of column names. Defaults to '*', meaning all columns. + */ + public $select='*'; + /** + * @var boolean whether to select distinct rows of data only. If this is set true, + * the SELECT clause would be changed to SELECT DISTINCT. + */ + public $distinct=false; + /** + * @var string query condition. This refers to the WHERE clause in an SQL statement. + * For example, <code>age>31 AND team=1</code>. + */ + public $condition=''; + /** + * @var array list of query parameter values indexed by parameter placeholders. + * For example, <code>array(':name'=>'Dan', ':age'=>31)</code>. + */ + public $params=array(); + /** + * @var integer maximum number of records to be returned. If less than 0, it means no limit. + */ + public $limit=-1; + /** + * @var integer zero-based offset from where the records are to be returned. If less than 0, it means starting from the beginning. + */ + public $offset=-1; + /** + * @var string how to sort the query results. This refers to the ORDER BY clause in an SQL statement. + */ + public $order=''; + /** + * @var string how to group the query results. This refers to the GROUP BY clause in an SQL statement. + * For example, <code>'projectID, teamID'</code>. + */ + public $group=''; + /** + * @var string how to join with other tables. This refers to the JOIN clause in an SQL statement. + * For example, <code>'LEFT JOIN users ON users.id=authorID'</code>. + */ + public $join=''; + /** + * @var string the condition to be applied with GROUP-BY clause. + * For example, <code>'SUM(revenue)<50000'</code>. + */ + public $having=''; + /** + * @var mixed the relational query criteria. This is used for fetching related objects in eager loading fashion. + * This property is effective only when the criteria is passed as a parameter to the following methods of CActiveRecord: + * <ul> + * <li>{@link CActiveRecord::find()}</li> + * <li>{@link CActiveRecord::findAll()}</li> + * <li>{@link CActiveRecord::findByPk()}</li> + * <li>{@link CActiveRecord::findAllByPk()}</li> + * <li>{@link CActiveRecord::findByAttributes()}</li> + * <li>{@link CActiveRecord::findAllByAttributes()}</li> + * <li>{@link CActiveRecord::count()}</li> + * </ul> + * The property value will be used as the parameter to the {@link CActiveRecord::with()} method + * to perform the eager loading. Please refer to {@link CActiveRecord::with()} on how to specify this parameter. + * @since 1.1.0 + */ + public $with; + /** + * @var string the alias name of the table. If not set, it means the alias is 't'. + */ + public $alias; + /** + * @var boolean whether the foreign tables should be joined with the primary table in a single SQL. + * This property is only used in relational AR queries for HAS_MANY and MANY_MANY relations. + * + * When this property is set true, only a single SQL will be executed for a relational AR query, + * even if the primary table is limited and the relationship between a foreign table and the primary + * table is many-to-one. + * + * When this property is set false, a SQL statement will be executed for each HAS_MANY relation. + * + * When this property is not set, if the primary table is limited or paginated, + * a SQL statement will be executed for each HAS_MANY relation. + * Otherwise, a single SQL statement will be executed for all. + * + * @since 1.1.4 + */ + public $together; + /** + * @var string the name of the AR attribute whose value should be used as index of the query result array. + * Defaults to null, meaning the result array will be zero-based integers. + * @since 1.1.5 + */ + public $index; + /** + * @var mixed scopes to apply + * + * This property is effective only when passing criteria to + * the one of the following methods: + * <ul> + * <li>{@link CActiveRecord::find()}</li> + * <li>{@link CActiveRecord::findAll()}</li> + * <li>{@link CActiveRecord::findByPk()}</li> + * <li>{@link CActiveRecord::findAllByPk()}</li> + * <li>{@link CActiveRecord::findByAttributes()}</li> + * <li>{@link CActiveRecord::findAllByAttributes()}</li> + * <li>{@link CActiveRecord::count()}</li> + * </ul> + * + * Can be set to one of the following: + * <ul> + * <li>One scope: $criteria->scopes='scopeName';</li> + * <li>Multiple scopes: $criteria->scopes=array('scopeName1','scopeName2');</li> + * <li>Scope with parameters: $criteria->scopes=array('scopeName'=>array($params));</li> + * <li>Multiple scopes with parameters: $criteria->scopes=array('scopeName1'=>array($params1),'scopeName2'=>array($params2));</li> + * <li>Multiple scopes with the same name: array(array('scopeName'=>array($params1)),array('scopeName'=>array($params2)));</li> + * </ul> + * @since 1.1.7 + */ + public $scopes; + + /** + * Constructor. + * @param array $data criteria initial property values (indexed by property name) + */ + public function __construct($data=array()) + { + foreach($data as $name=>$value) + $this->$name=$value; + } + + /** + * Remaps criteria parameters on unserialize to prevent name collisions. + * @since 1.1.9 + */ + public function __wakeup() + { + $map=array(); + $params=array(); + foreach($this->params as $name=>$value) + { + $newName=self::PARAM_PREFIX.self::$paramCount++; + $map[$name]=$newName; + $params[$newName]=$value; + } + $this->condition=strtr($this->condition,$map); + $this->params=$params; + } + + /** + * Appends a condition to the existing {@link condition}. + * The new condition and the existing condition will be concatenated via the specified operator + * which defaults to 'AND'. + * The new condition can also be an array. In this case, all elements in the array + * will be concatenated together via the operator. + * This method handles the case when the existing condition is empty. + * After calling this method, the {@link condition} property will be modified. + * @param mixed $condition the new condition. It can be either a string or an array of strings. + * @param string $operator the operator to join different conditions. Defaults to 'AND'. + * @return CDbCriteria the criteria object itself + */ + public function addCondition($condition,$operator='AND') + { + if(is_array($condition)) + { + if($condition===array()) + return $this; + $condition='('.implode(') '.$operator.' (',$condition).')'; + } + if($this->condition==='') + $this->condition=$condition; + else + $this->condition='('.$this->condition.') '.$operator.' ('.$condition.')'; + return $this; + } + + /** + * Appends a search condition to the existing {@link condition}. + * The search condition and the existing condition will be concatenated via the specified operator + * which defaults to 'AND'. + * The search condition is generated using the SQL LIKE operator with the given column name and + * search keyword. + * @param string $column the column name (or a valid SQL expression) + * @param string $keyword the search keyword. This interpretation of the keyword is affected by the next parameter. + * @param boolean $escape whether the keyword should be escaped if it contains characters % or _. + * When this parameter is true (default), the special characters % (matches 0 or more characters) + * and _ (matches a single character) will be escaped, and the keyword will be surrounded with a % + * character on both ends. When this parameter is false, the keyword will be directly used for + * matching without any change. + * @param string $operator the operator used to concatenate the new condition with the existing one. + * Defaults to 'AND'. + * @param string $like the LIKE operator. Defaults to 'LIKE'. You may also set this to be 'NOT LIKE'. + * @return CDbCriteria the criteria object itself + */ + public function addSearchCondition($column,$keyword,$escape=true,$operator='AND',$like='LIKE') + { + if($keyword=='') + return $this; + if($escape) + $keyword='%'.strtr($keyword,array('%'=>'\%', '_'=>'\_', '\\'=>'\\\\')).'%'; + $condition=$column." $like ".self::PARAM_PREFIX.self::$paramCount; + $this->params[self::PARAM_PREFIX.self::$paramCount++]=$keyword; + return $this->addCondition($condition, $operator); + } + + /** + * Appends an IN condition to the existing {@link condition}. + * The IN condition and the existing condition will be concatenated via the specified operator + * which defaults to 'AND'. + * The IN condition is generated by using the SQL IN operator which requires the specified + * column value to be among the given list of values. + * @param string $column the column name (or a valid SQL expression) + * @param array $values list of values that the column value should be in + * @param string $operator the operator used to concatenate the new condition with the existing one. + * Defaults to 'AND'. + * @return CDbCriteria the criteria object itself + */ + public function addInCondition($column,$values,$operator='AND') + { + if(($n=count($values))<1) + return $this->addCondition('0=1',$operator); // 0=1 is used because in MSSQL value alone can't be used in WHERE + if($n===1) + { + $value=reset($values); + if($value===null) + return $this->addCondition($column.' IS NULL'); + $condition=$column.'='.self::PARAM_PREFIX.self::$paramCount; + $this->params[self::PARAM_PREFIX.self::$paramCount++]=$value; + } + else + { + $params=array(); + foreach($values as $value) + { + $params[]=self::PARAM_PREFIX.self::$paramCount; + $this->params[self::PARAM_PREFIX.self::$paramCount++]=$value; + } + $condition=$column.' IN ('.implode(', ',$params).')'; + } + return $this->addCondition($condition,$operator); + } + + /** + * Appends an NOT IN condition to the existing {@link condition}. + * The NOT IN condition and the existing condition will be concatenated via the specified operator + * which defaults to 'AND'. + * The NOT IN condition is generated by using the SQL NOT IN operator which requires the specified + * column value to be among the given list of values. + * @param string $column the column name (or a valid SQL expression) + * @param array $values list of values that the column value should not be in + * @param string $operator the operator used to concatenate the new condition with the existing one. + * Defaults to 'AND'. + * @return CDbCriteria the criteria object itself + * @since 1.1.1 + */ + public function addNotInCondition($column,$values,$operator='AND') + { + if(($n=count($values))<1) + return $this; + if($n===1) + { + $value=reset($values); + if($value===null) + return $this->addCondition($column.' IS NOT NULL'); + $condition=$column.'!='.self::PARAM_PREFIX.self::$paramCount; + $this->params[self::PARAM_PREFIX.self::$paramCount++]=$value; + } + else + { + $params=array(); + foreach($values as $value) + { + $params[]=self::PARAM_PREFIX.self::$paramCount; + $this->params[self::PARAM_PREFIX.self::$paramCount++]=$value; + } + $condition=$column.' NOT IN ('.implode(', ',$params).')'; + } + return $this->addCondition($condition,$operator); + } + + /** + * Appends a condition for matching the given list of column values. + * The generated condition will be concatenated to the existing {@link condition} + * via the specified operator which defaults to 'AND'. + * The condition is generated by matching each column and the corresponding value. + * @param array $columns list of column names and values to be matched (name=>value) + * @param string $columnOperator the operator to concatenate multiple column matching condition. Defaults to 'AND'. + * @param string $operator the operator used to concatenate the new condition with the existing one. + * Defaults to 'AND'. + * @return CDbCriteria the criteria object itself + */ + public function addColumnCondition($columns,$columnOperator='AND',$operator='AND') + { + $params=array(); + foreach($columns as $name=>$value) + { + if($value===null) + $params[]=$name.' IS NULL'; + else + { + $params[]=$name.'='.self::PARAM_PREFIX.self::$paramCount; + $this->params[self::PARAM_PREFIX.self::$paramCount++]=$value; + } + } + return $this->addCondition(implode(" $columnOperator ",$params), $operator); + } + + /** + * Adds a comparison expression to the {@link condition} property. + * + * This method is a helper that appends to the {@link condition} property + * with a new comparison expression. The comparison is done by comparing a column + * with the given value using some comparison operator. + * + * The comparison operator is intelligently determined based on the first few + * characters in the given value. In particular, it recognizes the following operators + * if they appear as the leading characters in the given value: + * <ul> + * <li><code><</code>: the column must be less than the given value.</li> + * <li><code>></code>: the column must be greater than the given value.</li> + * <li><code><=</code>: the column must be less than or equal to the given value.</li> + * <li><code>>=</code>: the column must be greater than or equal to the given value.</li> + * <li><code><></code>: the column must not be the same as the given value. + * Note that when $partialMatch is true, this would mean the value must not be a substring + * of the column.</li> + * <li><code>=</code>: the column must be equal to the given value.</li> + * <li>none of the above: the column must be equal to the given value. Note that when $partialMatch + * is true, this would mean the value must be the same as the given value or be a substring of it.</li> + * </ul> + * + * Note that any surrounding white spaces will be removed from the value before comparison. + * When the value is empty, no comparison expression will be added to the search condition. + * + * @param string $column the name of the column to be searched + * @param mixed $value the column value to be compared with. If the value is a string, the aforementioned + * intelligent comparison will be conducted. If the value is an array, the comparison is done + * by exact match of any of the value in the array. If the string or the array is empty, + * the existing search condition will not be modified. + * @param boolean $partialMatch whether the value should consider partial text match (using LIKE and NOT LIKE operators). + * Defaults to false, meaning exact comparison. + * @param string $operator the operator used to concatenate the new condition with the existing one. + * Defaults to 'AND'. + * @param boolean $escape whether the value should be escaped if $partialMatch is true and + * the value contains characters % or _. When this parameter is true (default), + * the special characters % (matches 0 or more characters) + * and _ (matches a single character) will be escaped, and the value will be surrounded with a % + * character on both ends. When this parameter is false, the value will be directly used for + * matching without any change. + * @return CDbCriteria the criteria object itself + * @since 1.1.1 + */ + public function compare($column, $value, $partialMatch=false, $operator='AND', $escape=true) + { + if(is_array($value)) + { + if($value===array()) + return $this; + return $this->addInCondition($column,$value,$operator); + } + else + $value="$value"; + + if(preg_match('/^(?:\s*(<>|<=|>=|<|>|=))?(.*)$/',$value,$matches)) + { + $value=$matches[2]; + $op=$matches[1]; + } + else + $op=''; + + if($value==='') + return $this; + + if($partialMatch) + { + if($op==='') + return $this->addSearchCondition($column,$value,$escape,$operator); + if($op==='<>') + return $this->addSearchCondition($column,$value,$escape,$operator,'NOT LIKE'); + } + else if($op==='') + $op='='; + + $this->addCondition($column.$op.self::PARAM_PREFIX.self::$paramCount,$operator); + $this->params[self::PARAM_PREFIX.self::$paramCount++]=$value; + + return $this; + } + + /** + * Adds a between condition to the {@link condition} property. + * + * The new between condition and the existing condition will be concatenated via + * the specified operator which defaults to 'AND'. + * If one or both values are empty then the condition is not added to the existing condition. + * This method handles the case when the existing condition is empty. + * After calling this method, the {@link condition} property will be modified. + * @param string $column the name of the column to search between. + * @param string $valueStart the beginning value to start the between search. + * @param string $valueEnd the ending value to end the between search. + * @param string $operator the operator used to concatenate the new condition with the existing one. + * Defaults to 'AND'. + * @return CDbCriteria the criteria object itself + * @since 1.1.2 + */ + public function addBetweenCondition($column,$valueStart,$valueEnd,$operator='AND') + { + if($valueStart==='' || $valueEnd==='') + return $this; + + $paramStart=self::PARAM_PREFIX.self::$paramCount++; + $paramEnd=self::PARAM_PREFIX.self::$paramCount++; + $this->params[$paramStart]=$valueStart; + $this->params[$paramEnd]=$valueEnd; + $condition="$column BETWEEN $paramStart AND $paramEnd"; + + if($this->condition==='') + $this->condition=$condition; + else + $this->condition='('.$this->condition.') '.$operator.' ('.$condition.')'; + return $this; + } + + /** + * Merges with another criteria. + * In general, the merging makes the resulting criteria more restrictive. + * For example, if both criterias have conditions, they will be 'AND' together. + * Also, the criteria passed as the parameter takes precedence in case + * two options cannot be merged (e.g. LIMIT, OFFSET). + * @param mixed $criteria the criteria to be merged with. Either an array or CDbCriteria. + * @param boolean $useAnd whether to use 'AND' to merge condition and having options. + * If false, 'OR' will be used instead. Defaults to 'AND'. + */ + public function mergeWith($criteria,$useAnd=true) + { + $and=$useAnd ? 'AND' : 'OR'; + if(is_array($criteria)) + $criteria=new self($criteria); + if($this->select!==$criteria->select) + { + if($this->select==='*') + $this->select=$criteria->select; + else if($criteria->select!=='*') + { + $select1=is_string($this->select)?preg_split('/\s*,\s*/',trim($this->select),-1,PREG_SPLIT_NO_EMPTY):$this->select; + $select2=is_string($criteria->select)?preg_split('/\s*,\s*/',trim($criteria->select),-1,PREG_SPLIT_NO_EMPTY):$criteria->select; + $this->select=array_merge($select1,array_diff($select2,$select1)); + } + } + + if($this->condition!==$criteria->condition) + { + if($this->condition==='') + $this->condition=$criteria->condition; + else if($criteria->condition!=='') + $this->condition="({$this->condition}) $and ({$criteria->condition})"; + } + + if($this->params!==$criteria->params) + $this->params=array_merge($this->params,$criteria->params); + + if($criteria->limit>0) + $this->limit=$criteria->limit; + + if($criteria->offset>=0) + $this->offset=$criteria->offset; + + if($criteria->alias!==null) + $this->alias=$criteria->alias; + + if($this->order!==$criteria->order) + { + if($this->order==='') + $this->order=$criteria->order; + else if($criteria->order!=='') + $this->order=$criteria->order.', '.$this->order; + } + + if($this->group!==$criteria->group) + { + if($this->group==='') + $this->group=$criteria->group; + else if($criteria->group!=='') + $this->group.=', '.$criteria->group; + } + + if($this->join!==$criteria->join) + { + if($this->join==='') + $this->join=$criteria->join; + else if($criteria->join!=='') + $this->join.=' '.$criteria->join; + } + + if($this->having!==$criteria->having) + { + if($this->having==='') + $this->having=$criteria->having; + else if($criteria->having!=='') + $this->having="({$this->having}) $and ({$criteria->having})"; + } + + if($criteria->distinct>0) + $this->distinct=$criteria->distinct; + + if($criteria->together!==null) + $this->together=$criteria->together; + + if($criteria->index!==null) + $this->index=$criteria->index; + + if(empty($this->scopes)) + $this->scopes=$criteria->scopes; + else if(!empty($criteria->scopes)) + { + $scopes1=(array)$this->scopes; + $scopes2=(array)$criteria->scopes; + foreach($scopes1 as $k=>$v) + { + if(is_integer($k)) + $scopes[]=$v; + else if(isset($scopes2[$k])) + $scopes[]=array($k=>$v); + else + $scopes[$k]=$v; + } + foreach($scopes2 as $k=>$v) + { + if(is_integer($k)) + $scopes[]=$v; + else if(isset($scopes1[$k])) + $scopes[]=array($k=>$v); + else + $scopes[$k]=$v; + } + $this->scopes=$scopes; + } + + if(empty($this->with)) + $this->with=$criteria->with; + else if(!empty($criteria->with)) + { + $this->with=(array)$this->with; + foreach((array)$criteria->with as $k=>$v) + { + if(is_integer($k)) + $this->with[]=$v; + else if(isset($this->with[$k])) + { + $excludes=array(); + foreach(array('joinType','on') as $opt) + { + if(isset($this->with[$k][$opt])) + $excludes[$opt]=$this->with[$k][$opt]; + if(isset($v[$opt])) + $excludes[$opt]= ($opt==='on' && isset($excludes[$opt]) && $v[$opt]!==$excludes[$opt]) ? + "($excludes[$opt]) AND $v[$opt]" : $v[$opt]; + unset($this->with[$k][$opt]); + unset($v[$opt]); + } + $this->with[$k]=new self($this->with[$k]); + $this->with[$k]->mergeWith($v,$useAnd); + $this->with[$k]=$this->with[$k]->toArray(); + if (count($excludes)!==0) + $this->with[$k]=CMap::mergeArray($this->with[$k],$excludes); + } + else + $this->with[$k]=$v; + } + } + } + + /** + * @return array the array representation of the criteria + */ + public function toArray() + { + $result=array(); + foreach(array('select', 'condition', 'params', 'limit', 'offset', 'order', 'group', 'join', 'having', 'distinct', 'scopes', 'with', 'alias', 'index', 'together') as $name) + $result[$name]=$this->$name; + return $result; + } +} diff --git a/framework/db/schema/CDbExpression.php b/framework/db/schema/CDbExpression.php new file mode 100644 index 0000000..5fc8316 --- /dev/null +++ b/framework/db/schema/CDbExpression.php @@ -0,0 +1,61 @@ +<?php +/** + * CDbExpression class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbExpression represents a DB expression that does not need escaping. + * CDbExpression is mainly used in {@link CActiveRecord} as attribute values. + * When inserting or updating a {@link CActiveRecord}, attribute values of + * type CDbExpression will be directly put into the corresponding SQL statement + * without escaping. A typical usage is that an attribute is set with 'NOW()' + * expression so that saving the record would fill the corresponding column + * with the current DB server timestamp. + * + * Starting from version 1.1.1, one can also specify parameters to be bound + * for the expression. For example, if the expression is 'LOWER(:value)', then + * one can set {@link params} to be <code>array(':value'=>$value)</code>. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbExpression.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema + */ +class CDbExpression extends CComponent +{ + /** + * @var string the DB expression + */ + public $expression; + /** + * @var array list of parameters that should be bound for this expression. + * The keys are placeholders appearing in {@link expression}, while the values + * are the corresponding parameter values. + * @since 1.1.1 + */ + public $params=array(); + + /** + * Constructor. + * @param string $expression the DB expression + * @param array $params parameters + */ + public function __construct($expression,$params=array()) + { + $this->expression=$expression; + $this->params=$params; + } + + /** + * String magic method + * @return string the DB expression + */ + public function __toString() + { + return $this->expression; + } +}
\ No newline at end of file diff --git a/framework/db/schema/CDbSchema.php b/framework/db/schema/CDbSchema.php new file mode 100644 index 0000000..9cd7e15 --- /dev/null +++ b/framework/db/schema/CDbSchema.php @@ -0,0 +1,563 @@ +<?php +/** + * CDbSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbSchema is the base class for retrieving metadata information. + * + * @property CDbConnection $dbConnection Database connection. The connection is active. + * @property array $tables The metadata for all tables in the database. + * Each array element is an instance of {@link CDbTableSchema} (or its child class). + * The array keys are table names. + * @property array $tableNames All table names in the database. + * @property CDbCommandBuilder $commandBuilder The SQL command builder for this connection. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema + * @since 1.0 + */ +abstract class CDbSchema extends CComponent +{ + /** + * @var array the abstract column types mapped to physical column types. + * @since 1.1.6 + */ + public $columnTypes=array(); + + private $_tableNames=array(); + private $_tables=array(); + private $_connection; + private $_builder; + private $_cacheExclude=array(); + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return CDbTableSchema driver dependent table metadata, null if the table does not exist. + */ + abstract protected function loadTable($name); + + /** + * Constructor. + * @param CDbConnection $conn database connection. + */ + public function __construct($conn) + { + $this->_connection=$conn; + foreach($conn->schemaCachingExclude as $name) + $this->_cacheExclude[$name]=true; + } + + /** + * @return CDbConnection database connection. The connection is active. + */ + public function getDbConnection() + { + return $this->_connection; + } + + /** + * Obtains the metadata for the named table. + * @param string $name table name + * @param boolean $refresh if we need to refresh schema cache for a table. + * Parameter available since 1.1.9 + * @return CDbTableSchema table metadata. Null if the named table does not exist. + */ + public function getTable($name,$refresh=false) + { + if($refresh===false && isset($this->_tables[$name])) + return $this->_tables[$name]; + else + { + if($this->_connection->tablePrefix!==null && strpos($name,'{{')!==false) + $realName=preg_replace('/\{\{(.*?)\}\}/',$this->_connection->tablePrefix.'$1',$name); + else + $realName=$name; + + // temporarily disable query caching + if($this->_connection->queryCachingDuration>0) + { + $qcDuration=$this->_connection->queryCachingDuration; + $this->_connection->queryCachingDuration=0; + } + + if(!isset($this->_cacheExclude[$name]) && ($duration=$this->_connection->schemaCachingDuration)>0 && $this->_connection->schemaCacheID!==false && ($cache=Yii::app()->getComponent($this->_connection->schemaCacheID))!==null) + { + $key='yii:dbschema'.$this->_connection->connectionString.':'.$this->_connection->username.':'.$name; + $table=$cache->get($key); + if($refresh===true || $table===false) + { + $table=$this->loadTable($realName); + if($table!==null) + $cache->set($key,$table,$duration); + } + $this->_tables[$name]=$table; + } + else + $this->_tables[$name]=$table=$this->loadTable($realName); + + if(isset($qcDuration)) // re-enable query caching + $this->_connection->queryCachingDuration=$qcDuration; + + return $table; + } + } + + /** + * Returns the metadata for all tables in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @return array the metadata for all tables in the database. + * Each array element is an instance of {@link CDbTableSchema} (or its child class). + * The array keys are table names. + */ + public function getTables($schema='') + { + $tables=array(); + foreach($this->getTableNames($schema) as $name) + { + if(($table=$this->getTable($name))!==null) + $tables[$name]=$table; + } + return $tables; + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * If not empty, the returned table names will be prefixed with the schema name. + * @return array all table names in the database. + */ + public function getTableNames($schema='') + { + if(!isset($this->_tableNames[$schema])) + $this->_tableNames[$schema]=$this->findTableNames($schema); + return $this->_tableNames[$schema]; + } + + /** + * @return CDbCommandBuilder the SQL command builder for this connection. + */ + public function getCommandBuilder() + { + if($this->_builder!==null) + return $this->_builder; + else + return $this->_builder=$this->createCommandBuilder(); + } + + /** + * Refreshes the schema. + * This method resets the loaded table metadata and command builder + * so that they can be recreated to reflect the change of schema. + */ + public function refresh() + { + if(($duration=$this->_connection->schemaCachingDuration)>0 && $this->_connection->schemaCacheID!==false && ($cache=Yii::app()->getComponent($this->_connection->schemaCacheID))!==null) + { + foreach(array_keys($this->_tables) as $name) + { + if(!isset($this->_cacheExclude[$name])) + { + $key='yii:dbschema'.$this->_connection->connectionString.':'.$this->_connection->username.':'.$name; + $cache->delete($key); + } + } + } + $this->_tables=array(); + $this->_tableNames=array(); + $this->_builder=null; + } + + /** + * Quotes a table name for use in a query. + * If the table name contains schema prefix, the prefix will also be properly quoted. + * @param string $name table name + * @return string the properly quoted table name + * @see quoteSimpleTableName + */ + public function quoteTableName($name) + { + if(strpos($name,'.')===false) + return $this->quoteSimpleTableName($name); + $parts=explode('.',$name); + foreach($parts as $i=>$part) + $parts[$i]=$this->quoteSimpleTableName($part); + return implode('.',$parts); + + } + + /** + * Quotes a simple table name for use in a query. + * A simple table name does not schema prefix. + * @param string $name table name + * @return string the properly quoted table name + * @since 1.1.6 + */ + public function quoteSimpleTableName($name) + { + return "'".$name."'"; + } + + /** + * Quotes a column name for use in a query. + * If the column name contains prefix, the prefix will also be properly quoted. + * @param string $name column name + * @return string the properly quoted column name + * @see quoteSimpleColumnName + */ + public function quoteColumnName($name) + { + if(($pos=strrpos($name,'.'))!==false) + { + $prefix=$this->quoteTableName(substr($name,0,$pos)).'.'; + $name=substr($name,$pos+1); + } + else + $prefix=''; + return $prefix . ($name==='*' ? $name : $this->quoteSimpleColumnName($name)); + } + + /** + * Quotes a simple column name for use in a query. + * A simple column name does not contain prefix. + * @param string $name column name + * @return string the properly quoted column name + * @since 1.1.6 + */ + public function quoteSimpleColumnName($name) + { + return '"'.$name.'"'; + } + + /** + * Compares two table names. + * The table names can be either quoted or unquoted. This method + * will consider both cases. + * @param string $name1 table name 1 + * @param string $name2 table name 2 + * @return boolean whether the two table names refer to the same table. + */ + public function compareTableNames($name1,$name2) + { + $name1=str_replace(array('"','`',"'"),'',$name1); + $name2=str_replace(array('"','`',"'"),'',$name2); + if(($pos=strrpos($name1,'.'))!==false) + $name1=substr($name1,$pos+1); + if(($pos=strrpos($name2,'.'))!==false) + $name2=substr($name2,$pos+1); + if($this->_connection->tablePrefix!==null) + { + if(strpos($name1,'{')!==false) + $name1=$this->_connection->tablePrefix.str_replace(array('{','}'),'',$name1); + if(strpos($name2,'{')!==false) + $name2=$this->_connection->tablePrefix.str_replace(array('{','}'),'',$name2); + } + return $name1===$name2; + } + + /** + * Resets the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param CDbTableSchema $table the table schema whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @since 1.1 + */ + public function resetSequence($table,$value=null) + { + } + + /** + * Enables or disables integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @since 1.1 + */ + public function checkIntegrity($check=true,$schema='') + { + } + + /** + * Creates a command builder for the database. + * This method may be overridden by child classes to create a DBMS-specific command builder. + * @return CDbCommandBuilder command builder instance + */ + protected function createCommandBuilder() + { + return new CDbCommandBuilder($this); + } + + /** + * Returns all table names in the database. + * This method should be overridden by child classes in order to support this feature + * because the default implementation simply throws an exception. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * If not empty, the returned table names will be prefixed with the schema name. + * @return array all table names in the database. + */ + protected function findTableNames($schema='') + { + throw new CDbException(Yii::t('yii','{class} does not support fetching all table names.', + array('{class}'=>get_class($this)))); + } + + /** + * Converts an abstract column type into a physical column type. + * The conversion is done using the type map specified in {@link columnTypes}. + * These abstract column types are supported (using MySQL as example to explain the corresponding + * physical types): + * <ul> + * <li>pk: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY"</li> + * <li>string: string type, will be converted into "varchar(255)"</li> + * <li>text: a long string type, will be converted into "text"</li> + * <li>integer: integer type, will be converted into "int(11)"</li> + * <li>boolean: boolean type, will be converted into "tinyint(1)"</li> + * <li>float: float number type, will be converted into "float"</li> + * <li>decimal: decimal number type, will be converted into "decimal"</li> + * <li>datetime: datetime type, will be converted into "datetime"</li> + * <li>timestamp: timestamp type, will be converted into "timestamp"</li> + * <li>time: time type, will be converted into "time"</li> + * <li>date: date type, will be converted into "date"</li> + * <li>binary: binary data type, will be converted into "blob"</li> + * </ul> + * + * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only + * the first part will be converted, and the rest of the parts will be appended to the conversion result. + * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'. + * @param string $type abstract column type + * @return string physical column type. + * @since 1.1.6 + */ + public function getColumnType($type) + { + if(isset($this->columnTypes[$type])) + return $this->columnTypes[$type]; + else if(($pos=strpos($type,' '))!==false) + { + $t=substr($type,0,$pos); + return (isset($this->columnTypes[$t]) ? $this->columnTypes[$t] : $t).substr($type,$pos); + } + else + return $type; + } + + /** + * Builds a SQL statement for creating a new DB table. + * + * The columns in the new table should be specified as name-definition pairs (e.g. 'name'=>'string'), + * where name stands for a column name which will be properly quoted by the method, and definition + * stands for the column type which can contain an abstract DB type. + * The {@link getColumnType} method will be invoked to convert any abstract type into a physical one. + * + * If a column is specified with definition only (e.g. 'PRIMARY KEY (name, type)'), it will be directly + * inserted into the generated SQL. + * + * @param string $table the name of the table to be created. The name will be properly quoted by the method. + * @param array $columns the columns (name=>definition) in the new table. + * @param string $options additional SQL fragment that will be appended to the generated SQL. + * @return string the SQL statement for creating a new DB table. + * @since 1.1.6 + */ + public function createTable($table, $columns, $options=null) + { + $cols=array(); + foreach($columns as $name=>$type) + { + if(is_string($name)) + $cols[]="\t".$this->quoteColumnName($name).' '.$this->getColumnType($type); + else + $cols[]="\t".$type; + } + $sql="CREATE TABLE ".$this->quoteTableName($table)." (\n".implode(",\n",$cols)."\n)"; + return $options===null ? $sql : $sql.' '.$options; + } + + /** + * Builds a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + * @since 1.1.6 + */ + public function renameTable($table, $newName) + { + return 'RENAME TABLE ' . $this->quoteTableName($table) . ' TO ' . $this->quoteTableName($newName); + } + + /** + * Builds a SQL statement for dropping a DB table. + * @param string $table the table to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a DB table. + * @since 1.1.6 + */ + public function dropTable($table) + { + return "DROP TABLE ".$this->quoteTableName($table); + } + + /** + * Builds a SQL statement for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + * @return string the SQL statement for truncating a DB table. + * @since 1.1.6 + */ + public function truncateTable($table) + { + return "TRUNCATE TABLE ".$this->quoteTableName($table); + } + + /** + * Builds a SQL statement for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for adding a new column. + * @since 1.1.6 + */ + public function addColumn($table, $column, $type) + { + return 'ALTER TABLE ' . $this->quoteTableName($table) + . ' ADD ' . $this->quoteColumnName($column) . ' ' + . $this->getColumnType($type); + } + + /** + * Builds a SQL statement for dropping a DB column. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a DB column. + * @since 1.1.6 + */ + public function dropColumn($table, $column) + { + return "ALTER TABLE ".$this->quoteTableName($table) + ." DROP COLUMN ".$this->quoteColumnName($column); + } + + /** + * Builds a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB column. + * @since 1.1.6 + */ + public function renameColumn($table, $name, $newName) + { + return "ALTER TABLE ".$this->quoteTableName($table) + . " RENAME COLUMN ".$this->quoteColumnName($name) + . " TO ".$this->quoteColumnName($newName); + } + + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + * @since 1.1.6 + */ + public function alterColumn($table, $column, $type) + { + return 'ALTER TABLE ' . $this->quoteTableName($table) . ' CHANGE ' + . $this->quoteColumnName($column) . ' ' + . $this->quoteColumnName($column) . ' ' + . $this->getColumnType($type); + } + + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * The method will properly quote the table and column names. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas. + * @param string $refTable the table that the foreign key references to. + * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @return string the SQL statement for adding a foreign key constraint to an existing table. + * @since 1.1.6 + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete=null, $update=null) + { + $columns=preg_split('/\s*,\s*/',$columns,-1,PREG_SPLIT_NO_EMPTY); + foreach($columns as $i=>$col) + $columns[$i]=$this->quoteColumnName($col); + $refColumns=preg_split('/\s*,\s*/',$refColumns,-1,PREG_SPLIT_NO_EMPTY); + foreach($refColumns as $i=>$col) + $refColumns[$i]=$this->quoteColumnName($col); + $sql='ALTER TABLE '.$this->quoteTableName($table) + .' ADD CONSTRAINT '.$this->quoteColumnName($name) + .' FOREIGN KEY ('.implode(', ', $columns).')' + .' REFERENCES '.$this->quoteTableName($refTable) + .' ('.implode(', ', $refColumns).')'; + if($delete!==null) + $sql.=' ON DELETE '.$delete; + if($update!==null) + $sql.=' ON UPDATE '.$update; + return $sql; + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a foreign key constraint. + * @since 1.1.6 + */ + public function dropForeignKey($name, $table) + { + return 'ALTER TABLE '.$this->quoteTableName($table) + .' DROP CONSTRAINT '.$this->quoteColumnName($name); + } + + /** + * Builds a SQL statement for creating a new index. + * @param string $name the name of the index. The name will be properly quoted by the method. + * @param string $table the table that the new index will be created for. The table name will be properly quoted by the method. + * @param string $column the column(s) that should be included in the index. If there are multiple columns, please separate them + * by commas. Each column name will be properly quoted by the method, unless a parenthesis is found in the name. + * @param boolean $unique whether to add UNIQUE constraint on the created index. + * @return string the SQL statement for creating a new index. + * @since 1.1.6 + */ + public function createIndex($name, $table, $column, $unique=false) + { + $cols=array(); + $columns=preg_split('/\s*,\s*/',$column,-1,PREG_SPLIT_NO_EMPTY); + foreach($columns as $col) + { + if(strpos($col,'(')!==false) + $cols[]=$col; + else + $cols[]=$this->quoteColumnName($col); + } + return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ') + . $this->quoteTableName($name).' ON ' + . $this->quoteTableName($table).' ('.implode(', ',$cols).')'; + } + + /** + * Builds a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping an index. + * @since 1.1.6 + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX '.$this->quoteTableName($name).' ON '.$this->quoteTableName($table); + } +} diff --git a/framework/db/schema/CDbTableSchema.php b/framework/db/schema/CDbTableSchema.php new file mode 100644 index 0000000..b70a2c8 --- /dev/null +++ b/framework/db/schema/CDbTableSchema.php @@ -0,0 +1,78 @@ +<?php +/** + * CDbTableSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CDbTableSchema is the base class for representing the metadata of a database table. + * + * It may be extended by different DBMS driver to provide DBMS-specific table metadata. + * + * CDbTableSchema provides the following information about a table: + * <ul> + * <li>{@link name}</li> + * <li>{@link rawName}</li> + * <li>{@link columns}</li> + * <li>{@link primaryKey}</li> + * <li>{@link foreignKeys}</li> + * <li>{@link sequenceName}</li> + * </ul> + * + * @property array $columnNames List of column names. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CDbTableSchema.php 3426 2011-10-25 00:01:09Z alexander.makarow $ + * @package system.db.schema + * @since 1.0 + */ +class CDbTableSchema extends CComponent +{ + /** + * @var string name of this table. + */ + public $name; + /** + * @var string raw name of this table. This is the quoted version of table name with optional schema name. It can be directly used in SQLs. + */ + public $rawName; + /** + * @var string|array primary key name of this table. If composite key, an array of key names is returned. + */ + public $primaryKey; + /** + * @var string sequence name for the primary key. Null if no sequence. + */ + public $sequenceName; + /** + * @var array foreign keys of this table. The array is indexed by column name. Each value is an array of foreign table name and foreign column name. + */ + public $foreignKeys=array(); + /** + * @var array column metadata of this table. Each array element is a CDbColumnSchema object, indexed by column names. + */ + public $columns=array(); + + /** + * Gets the named column metadata. + * This is a convenient method for retrieving a named column even if it does not exist. + * @param string $name column name + * @return CDbColumnSchema metadata of the named column. Null if the named column does not exist. + */ + public function getColumn($name) + { + return isset($this->columns[$name]) ? $this->columns[$name] : null; + } + + /** + * @return array list of column names + */ + public function getColumnNames() + { + return array_keys($this->columns); + } +} diff --git a/framework/db/schema/mssql/CMssqlColumnSchema.php b/framework/db/schema/mssql/CMssqlColumnSchema.php new file mode 100644 index 0000000..88dc654 --- /dev/null +++ b/framework/db/schema/mssql/CMssqlColumnSchema.php @@ -0,0 +1,72 @@ +<?php +/** + * CMssqlColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CMssqlColumnSchema class describes the column meta data of a MSSQL table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @version $Id: CMssqlColumnSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.mssql + */ +class CMssqlColumnSchema extends CDbColumnSchema +{ + /** + * Extracts the PHP type from DB type. + * @param string $dbType DB type + */ + protected function extractType($dbType) + { + if(strpos($dbType,'float')!==false || strpos($dbType,'real')!==false) + $this->type='double'; + else if(strpos($dbType,'bigint')===false && (strpos($dbType,'int')!==false || strpos($dbType,'smallint')!==false || strpos($dbType,'tinyint'))) + $this->type='integer'; + else if(strpos($dbType,'bit')!==false) + $this->type='boolean'; + else + $this->type='string'; + } + + /** + * Extracts the default value for the column. + * The value is typecasted to correct PHP type. + * @param mixed $defaultValue the default value obtained from metadata + */ + protected function extractDefault($defaultValue) + { + if($this->dbType==='timestamp' ) + $this->defaultValue=null; + else + parent::extractDefault(str_replace(array('(',')',"'"), '', $defaultValue)); + } + + /** + * Extracts size, precision and scale information from column's DB type. + * We do nothing here, since sizes and precisions have been computed before. + * @param string $dbType the column's DB type + */ + protected function extractLimit($dbType) + { + } + + /** + * Converts the input value to the type that this column is of. + * @param mixed $value input value + * @return mixed converted value + */ + public function typecast($value) + { + if($this->type==='boolean') + return $value ? 1 : 0; + else + return parent::typecast($value); + } +} diff --git a/framework/db/schema/mssql/CMssqlCommandBuilder.php b/framework/db/schema/mssql/CMssqlCommandBuilder.php new file mode 100644 index 0000000..affaa7a --- /dev/null +++ b/framework/db/schema/mssql/CMssqlCommandBuilder.php @@ -0,0 +1,337 @@ +<?php +/** + * CMsCommandBuilder class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CMssqlCommandBuilder provides basic methods to create query commands for tables for Mssql Servers. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + * @version $Id: CMssqlCommandBuilder.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.mssql + */ +class CMssqlCommandBuilder extends CDbCommandBuilder +{ + /** + * Creates a COUNT(*) command for a single table. + * Override parent implementation to remove the order clause of criteria if it exists + * @param CDbTableSchema $table the table metadata + * @param CDbCriteria $criteria the query criteria + * @param string $alias the alias name of the primary table. Defaults to 't'. + * @return CDbCommand query command. + */ + public function createCountCommand($table,$criteria,$alias='t') + { + $criteria->order=''; + return parent::createCountCommand($table, $criteria,$alias); + } + + /** + * Creates a SELECT command for a single table. + * Override parent implementation to check if an orderby clause if specified when querying with an offset + * @param CDbTableSchema $table the table metadata + * @param CDbCriteria $criteria the query criteria + * @param string $alias the alias name of the primary table. Defaults to 't'. + * @return CDbCommand query command. + */ + public function createFindCommand($table,$criteria,$alias='t') + { + $criteria=$this->checkCriteria($table,$criteria); + return parent::createFindCommand($table,$criteria,$alias); + + } + + /** + * Creates an UPDATE command. + * Override parent implementation because mssql don't want to update an identity column + * @param CDbTableSchema $table the table metadata + * @param array $data list of columns to be updated (name=>value) + * @param CDbCriteria $criteria the query criteria + * @return CDbCommand update command. + */ + public function createUpdateCommand($table,$data,$criteria) + { + $criteria=$this->checkCriteria($table,$criteria); + $fields=array(); + $values=array(); + $bindByPosition=isset($criteria->params[0]); + $i=0; + foreach($data as $name=>$value) + { + if(($column=$table->getColumn($name))!==null) + { + if ($table->sequenceName !== null && $column->isPrimaryKey === true) continue; + if ($column->dbType === 'timestamp') continue; + if($value instanceof CDbExpression) + { + $fields[]=$column->rawName.'='.$value->expression; + foreach($value->params as $n=>$v) + $values[$n]=$v; + } + else if($bindByPosition) + { + $fields[]=$column->rawName.'=?'; + $values[]=$column->typecast($value); + } + else + { + $fields[]=$column->rawName.'='.self::PARAM_PREFIX.$i; + $values[self::PARAM_PREFIX.$i]=$column->typecast($value); + $i++; + } + } + } + if($fields===array()) + throw new CDbException(Yii::t('yii','No columns are being updated for table "{table}".', + array('{table}'=>$table->name))); + $sql="UPDATE {$table->rawName} SET ".implode(', ',$fields); + $sql=$this->applyJoin($sql,$criteria->join); + $sql=$this->applyCondition($sql,$criteria->condition); + $sql=$this->applyOrder($sql,$criteria->order); + $sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset); + + $command=$this->getDbConnection()->createCommand($sql); + $this->bindValues($command,array_merge($values,$criteria->params)); + + return $command; + } + + /** + * Creates a DELETE command. + * Override parent implementation to check if an orderby clause if specified when querying with an offset + * @param CDbTableSchema $table the table metadata + * @param CDbCriteria $criteria the query criteria + * @return CDbCommand delete command. + */ + public function createDeleteCommand($table,$criteria) + { + $criteria=$this->checkCriteria($table, $criteria); + return parent::createDeleteCommand($table, $criteria); + } + + /** + * Creates an UPDATE command that increments/decrements certain columns. + * Override parent implementation to check if an orderby clause if specified when querying with an offset + * @param CDbTableSchema $table the table metadata + * @param CDbCriteria $counters the query criteria + * @param array $criteria counters to be updated (counter increments/decrements indexed by column names.) + * @return CDbCommand the created command + * @throws CException if no counter is specified + */ + public function createUpdateCounterCommand($table,$counters,$criteria) + { + $criteria=$this->checkCriteria($table, $criteria); + return parent::createUpdateCounterCommand($table, $counters, $criteria); + } + + /** + * This is a port from Prado Framework. + * + * Overrides parent implementation. Alters the sql to apply $limit and $offset. + * The idea for limit with offset is done by modifying the sql on the fly + * with numerous assumptions on the structure of the sql string. + * The modification is done with reference to the notes from + * http://troels.arvin.dk/db/rdbms/#select-limit-offset + * + * <code> + * SELECT * FROM ( + * SELECT TOP n * FROM ( + * SELECT TOP z columns -- (z=n+skip) + * FROM tablename + * ORDER BY key ASC + * ) AS FOO ORDER BY key DESC -- ('FOO' may be anything) + * ) AS BAR ORDER BY key ASC -- ('BAR' may be anything) + * </code> + * + * <b>Regular expressions are used to alter the SQL query. The resulting SQL query + * may be malformed for complex queries.</b> The following restrictions apply + * + * <ul> + * <li> + * In particular, <b>commas</b> should <b>NOT</b> + * be used as part of the ordering expression or identifier. Commas must only be + * used for separating the ordering clauses. + * </li> + * <li> + * In the ORDER BY clause, the column name should NOT be be qualified + * with a table name or view name. Alias the column names or use column index. + * </li> + * <li> + * No clauses should follow the ORDER BY clause, e.g. no COMPUTE or FOR clauses. + * </li> + * + * @param string $sql SQL query string. + * @param integer $limit maximum number of rows, -1 to ignore limit. + * @param integer $offset row offset, -1 to ignore offset. + * @return string SQL with limit and offset. + * + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + */ + public function applyLimit($sql, $limit, $offset) + { + $limit = $limit!==null ? intval($limit) : -1; + $offset = $offset!==null ? intval($offset) : -1; + if ($limit > 0 && $offset <= 0) //just limit + $sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $limit", $sql); + else if($limit > 0 && $offset > 0) + $sql = $this->rewriteLimitOffsetSql($sql, $limit,$offset); + return $sql; + } + + /** + * Rewrite sql to apply $limit > and $offset > 0 for MSSQL database. + * See http://troels.arvin.dk/db/rdbms/#select-limit-offset + * @param string $sql sql query + * @param integer $limit $limit > 0 + * @param integer $offset $offset > 0 + * @return sql modified sql query applied with limit and offset. + * + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + */ + protected function rewriteLimitOffsetSql($sql, $limit, $offset) + { + $fetch = $limit+$offset; + $sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $fetch", $sql); + $ordering = $this->findOrdering($sql); + $orginalOrdering = $this->joinOrdering($ordering, '[__outer__]'); + $reverseOrdering = $this->joinOrdering($this->reverseDirection($ordering), '[__inner__]'); + $sql = "SELECT * FROM (SELECT TOP {$limit} * FROM ($sql) as [__inner__] {$reverseOrdering}) as [__outer__] {$orginalOrdering}"; + return $sql; + } + + /** + * Base on simplified syntax http://msdn2.microsoft.com/en-us/library/aa259187(SQL.80).aspx + * + * @param string $sql $sql + * @return array ordering expression as key and ordering direction as value + * + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + */ + protected function findOrdering($sql) + { + if(!preg_match('/ORDER BY/i', $sql)) + return array(); + $matches=array(); + $ordering=array(); + preg_match_all('/(ORDER BY)[\s"\[](.*)(ASC|DESC)?(?:[\s"\[]|$|COMPUTE|FOR)/i', $sql, $matches); + if(count($matches)>1 && count($matches[2]) > 0) + { + $parts = explode(',', $matches[2][0]); + foreach($parts as $part) + { + $subs=array(); + if(preg_match_all('/(.*)[\s"\]](ASC|DESC)$/i', trim($part), $subs)) + { + if(count($subs) > 1 && count($subs[2]) > 0) + { + $name=''; + foreach(explode('.', $subs[1][0]) as $p) + { + if($name!=='') + $name.='.'; + $name.='[' . trim($p, '[]') . ']'; + } + $ordering[$name] = $subs[2][0]; + } + //else what? + } + else + $ordering[trim($part)] = 'ASC'; + } + } + + // replacing column names with their alias names + foreach($ordering as $name => $direction) + { + $matches = array(); + $pattern = '/\s+'.str_replace(array('[',']'), array('\[','\]'), $name).'\s+AS\s+(\[[^\]]+\])/i'; + preg_match($pattern, $sql, $matches); + if(isset($matches[1])) + { + $ordering[$matches[1]] = $ordering[$name]; + unset($ordering[$name]); + } + } + + return $ordering; + } + + /** + * @param array $orders ordering obtained from findOrdering() + * @param string $newPrefix new table prefix to the ordering columns + * @return string concat the orderings + * + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + */ + protected function joinOrdering($orders, $newPrefix) + { + if(count($orders)>0) + { + $str=array(); + foreach($orders as $column => $direction) + $str[] = $column.' '.$direction; + $orderBy = 'ORDER BY '.implode(', ', $str); + return preg_replace('/\s+\[[^\]]+\]\.(\[[^\]]+\])/i', ' '.$newPrefix.'.\1', $orderBy); + } + } + + /** + * @param array $orders original ordering + * @return array ordering with reversed direction. + * + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + */ + protected function reverseDirection($orders) + { + foreach($orders as $column => $direction) + $orders[$column] = strtolower(trim($direction))==='desc' ? 'ASC' : 'DESC'; + return $orders; + } + + + /** + * Checks if the criteria has an order by clause when using offset/limit. + * Override parent implementation to check if an orderby clause if specified when querying with an offset + * If not, order it by pk. + * @param CMssqlTableSchema $table table schema + * @param CDbCriteria $criteria criteria + * @return CDbCrireria the modified criteria + */ + protected function checkCriteria($table, $criteria) + { + if ($criteria->offset > 0 && $criteria->order==='') + { + $criteria->order=is_array($table->primaryKey)?implode(',',$table->primaryKey):$table->primaryKey; + } + return $criteria; + } + + /** + * Generates the expression for selecting rows with specified composite key values. + * @param CDbTableSchema $table the table schema + * @param array $values list of primary key values to be selected within + * @param string $prefix column prefix (ended with dot) + * @return string the expression for selection + */ + protected function createCompositeInCondition($table,$values,$prefix) + { + $vs=array(); + foreach($values as $value) + { + $c=array(); + foreach($value as $k=>$v) + $c[]=$prefix.$table->columns[$k]->rawName.'='.$v; + $vs[]='('.implode(' AND ',$c).')'; + } + return '('.implode(' OR ',$vs).')'; + } +} diff --git a/framework/db/schema/mssql/CMssqlPdoAdapter.php b/framework/db/schema/mssql/CMssqlPdoAdapter.php new file mode 100644 index 0000000..aca6343 --- /dev/null +++ b/framework/db/schema/mssql/CMssqlPdoAdapter.php @@ -0,0 +1,75 @@ +<?php +/** + * CMssqlPdo class file + * + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * This is an extension of default PDO class for mssql driver only + * It provides some missing functionalities of pdo driver + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @version $Id: CMssqlPdoAdapter.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.mssql + */ +class CMssqlPdoAdapter extends PDO +{ + /** + * Get the last inserted id value + * MSSQL doesn't support sequence, so, argument is ignored + * + * @param string|null sequence name. Defaults to null + * @return integer last inserted id + */ + public function lastInsertId ($sequence=NULL) + { + $value=$this->query('SELECT SCOPE_IDENTITY()')->fetchColumn(); + $value=preg_replace('/[,.]0+$/', '', $value); // issue 2312 + return strtr($value,array(','=>'','.'=>'')); + } + + /** + * Begin a transaction + * + * Is is necessary to override pdo's method, as mssql pdo drivers + * does not support transaction + * + * @return boolean + */ + public function beginTransaction () + { + $this->exec('BEGIN TRANSACTION'); + return true; + } + + /** + * Commit a transaction + * + * Is is necessary to override pdo's method, as mssql pdo drivers + * does not support transaction + * + * @return boolean + */ + public function commit () + { + $this->exec('COMMIT TRANSACTION'); + return true; + } + + /** + * Rollback a transaction + * + * Is is necessary to override pdo's method, ac mssql pdo drivers + * does not support transaction + * + * @return boolean + */ + public function rollBack () + { + $this->exec('ROLLBACK TRANSACTION'); + return true; + } +} diff --git a/framework/db/schema/mssql/CMssqlSchema.php b/framework/db/schema/mssql/CMssqlSchema.php new file mode 100644 index 0000000..89c4d8a --- /dev/null +++ b/framework/db/schema/mssql/CMssqlSchema.php @@ -0,0 +1,424 @@ +<?php +/** + * CMssqlSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CMssqlSchema is the class for retrieving metadata information from a MS SQL Server database. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @version $Id: CMssqlSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.mssql + */ +class CMssqlSchema extends CDbSchema +{ + const DEFAULT_SCHEMA='dbo'; + + /** + * @var array the abstract column types mapped to physical column types. + * @since 1.1.6 + */ + public $columnTypes=array( + 'pk' => 'int IDENTITY PRIMARY KEY', + 'string' => 'varchar(255)', + 'text' => 'text', + 'integer' => 'int', + 'float' => 'float', + 'decimal' => 'decimal', + 'datetime' => 'datetime', + 'timestamp' => 'timestamp', + 'time' => 'time', + 'date' => 'date', + 'binary' => 'binary', + 'boolean' => 'bit', + ); + + /** + * Quotes a table name for use in a query. + * A simple table name does not schema prefix. + * @param string $name table name + * @return string the properly quoted table name + * @since 1.1.6 + */ + public function quoteSimpleTableName($name) + { + return '['.$name.']'; + } + + /** + * Quotes a column name for use in a query. + * A simple column name does not contain prefix. + * @param string $name column name + * @return string the properly quoted column name + * @since 1.1.6 + */ + public function quoteSimpleColumnName($name) + { + return '['.$name.']'; + } + + /** + * Compares two table names. + * The table names can be either quoted or unquoted. This method + * will consider both cases. + * @param string $name1 table name 1 + * @param string $name2 table name 2 + * @return boolean whether the two table names refer to the same table. + */ + public function compareTableNames($name1,$name2) + { + $name1=str_replace(array('[',']'),'',$name1); + $name2=str_replace(array('[',']'),'',$name2); + return parent::compareTableNames(strtolower($name1),strtolower($name2)); + } + + /** + * Resets the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param CDbTableSchema $table the table schema whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @since 1.1.6 + */ + public function resetSequence($table,$value=null) + { + if($table->sequenceName!==null) + { + $db=$this->getDbConnection(); + if($value===null) + $value=$db->createCommand("SELECT MAX(`{$table->primaryKey}`) FROM {$table->rawName}")->queryScalar(); + $value=(int)$value; + $name=strtr($table->rawName,array('['=>'',']'=>'')); + $db->createCommand("DBCC CHECKIDENT ('$name', RESEED, $value)")->execute(); + } + } + + private $_normalTables=array(); // non-view tables + /** + * Enables or disables integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @since 1.1.6 + */ + public function checkIntegrity($check=true,$schema='') + { + $enable=$check ? 'CHECK' : 'NOCHECK'; + if(!isset($this->_normalTables[$schema])) + $this->_normalTables[$schema]=$this->findTableNames($schema,false); + $db=$this->getDbConnection(); + foreach($this->_normalTables[$schema] as $tableName) + { + $tableName=$this->quoteTableName($tableName); + $db->createCommand("ALTER TABLE $tableName $enable CONSTRAINT ALL")->execute(); + } + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return CMssqlTableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function loadTable($name) + { + $table=new CMssqlTableSchema; + $this->resolveTableNames($table,$name); + //if (!in_array($table->name, $this->tableNames)) return null; + $table->primaryKey=$this->findPrimaryKey($table); + $table->foreignKeys=$this->findForeignKeys($table); + if($this->findColumns($table)) + { + return $table; + } + else + return null; + } + + /** + * Generates various kinds of table names. + * @param CMssqlTableSchema $table the table instance + * @param string $name the unquoted table name + */ + protected function resolveTableNames($table,$name) + { + $parts=explode('.',str_replace(array('[',']'),'',$name)); + if(($c=count($parts))==3) + { + // Catalog name, schema name and table name provided + $table->catalogName=$parts[0]; + $table->schemaName=$parts[1]; + $table->name=$parts[2]; + $table->rawName=$this->quoteTableName($table->catalogName).'.'.$this->quoteTableName($table->schemaName).'.'.$this->quoteTableName($table->name); + } + elseif ($c==2) + { + // Only schema name and table name provided + $table->name=$parts[1]; + $table->schemaName=$parts[0]; + $table->rawName=$this->quoteTableName($table->schemaName).'.'.$this->quoteTableName($table->name); + } + else + { + // Only the name given, we need to get at least the schema name + //if (empty($this->_schemaNames)) $this->findTableNames(); + $table->name=$parts[0]; + $table->schemaName=self::DEFAULT_SCHEMA; + $table->rawName=$this->quoteTableName($table->schemaName).'.'.$this->quoteTableName($table->name); + } + } + + /** + * Gets the primary key column(s) details for the given table. + * @param CMssqlTableSchema $table table + * @return mixed primary keys (null if no pk, string if only 1 column pk, or array if composite pk) + */ + protected function findPrimaryKey($table) + { + $kcu='INFORMATION_SCHEMA.KEY_COLUMN_USAGE'; + $tc='INFORMATION_SCHEMA.TABLE_CONSTRAINTS'; + if (isset($table->catalogName)) + { + $kcu=$table->catalogName.'.'.$kcu; + $tc=$table->catalogName.'.'.$tc; + } + + $sql = <<<EOD + SELECT k.column_name field_name + FROM {$this->quoteTableName($kcu)} k + LEFT JOIN {$this->quoteTableName($tc)} c + ON k.table_name = c.table_name + AND k.constraint_name = c.constraint_name + WHERE c.constraint_type ='PRIMARY KEY' + AND k.table_name = :table + AND k.table_schema = :schema +EOD; + $command = $this->getDbConnection()->createCommand($sql); + $command->bindValue(':table', $table->name); + $command->bindValue(':schema', $table->schemaName); + $primary=$command->queryColumn(); + switch (count($primary)) + { + case 0: // No primary key on table + $primary=null; + break; + case 1: // Only 1 primary key + $primary=$primary[0]; + break; + } + return $primary; + } + + /** + * Gets foreign relationship constraint keys and table name + * @param CMssqlTableSchema $table table + * @return array foreign relationship table name and keys. + */ + protected function findForeignKeys($table) + { + $rc='INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS'; + $kcu='INFORMATION_SCHEMA.KEY_COLUMN_USAGE'; + if (isset($table->catalogName)) + { + $kcu=$table->catalogName.'.'.$kcu; + $rc=$table->catalogName.'.'.$rc; + } + + //From http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx + $sql = <<<EOD + SELECT + KCU1.CONSTRAINT_NAME AS 'FK_CONSTRAINT_NAME' + , KCU1.TABLE_NAME AS 'FK_TABLE_NAME' + , KCU1.COLUMN_NAME AS 'FK_COLUMN_NAME' + , KCU1.ORDINAL_POSITION AS 'FK_ORDINAL_POSITION' + , KCU2.CONSTRAINT_NAME AS 'UQ_CONSTRAINT_NAME' + , KCU2.TABLE_NAME AS 'UQ_TABLE_NAME' + , KCU2.COLUMN_NAME AS 'UQ_COLUMN_NAME' + , KCU2.ORDINAL_POSITION AS 'UQ_ORDINAL_POSITION' + FROM {$this->quoteTableName($rc)} RC + JOIN {$this->quoteTableName($kcu)} KCU1 + ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG + AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA + AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME + JOIN {$this->quoteTableName($kcu)} KCU2 + ON KCU2.CONSTRAINT_CATALOG = + RC.UNIQUE_CONSTRAINT_CATALOG + AND KCU2.CONSTRAINT_SCHEMA = + RC.UNIQUE_CONSTRAINT_SCHEMA + AND KCU2.CONSTRAINT_NAME = + RC.UNIQUE_CONSTRAINT_NAME + AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION + WHERE KCU1.TABLE_NAME = :table +EOD; + $command = $this->getDbConnection()->createCommand($sql); + $command->bindValue(':table', $table->name); + $fkeys=array(); + foreach($command->queryAll() as $info) + { + $fkeys[$info['FK_COLUMN_NAME']]=array($info['UQ_TABLE_NAME'],$info['UQ_COLUMN_NAME'],); + + } + return $fkeys; + } + + + /** + * Collects the table column metadata. + * @param CMssqlTableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $columnsTable="INFORMATION_SCHEMA.COLUMNS"; + $where=array(); + $where[]="TABLE_NAME='".$table->name."'"; + if (isset($table->catalogName)) + { + $where[]="TABLE_CATALOG='".$table->catalogName."'"; + $columnsTable = $table->catalogName.'.'.$columnsTable; + } + if (isset($table->schemaName)) + $where[]="TABLE_SCHEMA='".$table->schemaName."'"; + + $sql="SELECT *, columnproperty(object_id(table_schema+'.'+table_name), column_name, 'IsIdentity') as IsIdentity ". + "FROM ".$this->quoteTableName($columnsTable)." WHERE ".join(' AND ',$where); + if (($columns=$this->getDbConnection()->createCommand($sql)->queryAll())===array()) + return false; + + foreach($columns as $column) + { + $c=$this->createColumn($column); + if (is_array($table->primaryKey)) + $c->isPrimaryKey=in_array($c->name, $table->primaryKey); + else + $c->isPrimaryKey=strcasecmp($c->name,$table->primaryKey)===0; + + $c->isForeignKey=isset($table->foreignKeys[$c->name]); + $table->columns[$c->name]=$c; + if ($c->autoIncrement && $table->sequenceName===null) + $table->sequenceName=$table->name; + } + return true; + } + + /** + * Creates a table column. + * @param array $column column metadata + * @return CDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new CMssqlColumnSchema; + $c->name=$column['COLUMN_NAME']; + $c->rawName=$this->quoteColumnName($c->name); + $c->allowNull=$column['IS_NULLABLE']=='YES'; + if ($column['NUMERIC_PRECISION_RADIX']!==null) + { + // We have a numeric datatype + $c->size=$c->precision=$column['NUMERIC_PRECISION']!==null?(int)$column['NUMERIC_PRECISION']:null; + $c->scale=$column['NUMERIC_SCALE']!==null?(int)$column['NUMERIC_SCALE']:null; + } + elseif ($column['DATA_TYPE']=='image' || $column['DATA_TYPE']=='text') + $c->size=$c->precision=null; + else + $c->size=$c->precision=($column['CHARACTER_MAXIMUM_LENGTH']!== null)?(int)$column['CHARACTER_MAXIMUM_LENGTH']:null; + $c->autoIncrement=$column['IsIdentity']==1; + + $c->init($column['DATA_TYPE'],$column['COLUMN_DEFAULT']); + return $c; + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * If not empty, the returned table names will be prefixed with the schema name. + * @param boolean $includeViews whether to include views in the result. Defaults to true. + * @return array all table names in the database. + */ + protected function findTableNames($schema='',$includeViews=true) + { + if($schema==='') + $schema=self::DEFAULT_SCHEMA; + if($includeViews) + $condition="TABLE_TYPE in ('BASE TABLE','VIEW')"; + else + $condition="TABLE_TYPE='BASE TABLE'"; + $sql=<<<EOD +SELECT TABLE_NAME, TABLE_SCHEMA FROM [INFORMATION_SCHEMA].[TABLES] +WHERE TABLE_SCHEMA=:schema AND $condition +EOD; + $command=$this->getDbConnection()->createCommand($sql); + $command->bindParam(":schema", $schema); + $rows=$command->queryAll(); + $names=array(); + foreach ($rows as $row) + { + if ($schema == self::DEFAULT_SCHEMA) + $names[]=$row['TABLE_NAME']; + else + $names[]=$schema.'.'.$row['TABLE_SCHEMA'].'.'.$row['TABLE_NAME']; + } + + return $names; + } + + /** + * Creates a command builder for the database. + * This method overrides parent implementation in order to create a MSSQL specific command builder + * @return CDbCommandBuilder command builder instance + */ + protected function createCommandBuilder() + { + return new CMssqlCommandBuilder($this); + } + + /** + * Builds a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + * @since 1.1.6 + */ + public function renameTable($table, $newName) + { + return "sp_rename '$table', '$newName'"; + } + + /** + * Builds a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB column. + * @since 1.1.6 + */ + public function renameColumn($table, $name, $newName) + { + return "sp_rename '$table.$name', '$newName', 'COLUMN'"; + } + + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + * @since 1.1.6 + */ + public function alterColumn($table, $column, $type) + { + $type=$this->getColumnType($type); + $sql='ALTER TABLE ' . $this->quoteTableName($table) . ' ALTER COLUMN ' + . $this->quoteColumnName($column) . ' ' + . $this->getColumnType($type); + return $sql; + } +} diff --git a/framework/db/schema/mssql/CMssqlTableSchema.php b/framework/db/schema/mssql/CMssqlTableSchema.php new file mode 100644 index 0000000..6f0d137 --- /dev/null +++ b/framework/db/schema/mssql/CMssqlTableSchema.php @@ -0,0 +1,32 @@ +<?php +/** + * CMssqlTableSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CMssqlTableSchema represents the metadata for a MSSQL table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @version $Id: CMssqlTableSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.mssql + */ +class CMssqlTableSchema extends CDbTableSchema +{ + /** + * @var string name of the catalog (database) that this table belongs to. + * Defaults to null, meaning no schema (or the current database). + */ + public $catalogName; + /** + * @var string name of the schema that this table belongs to. + * Defaults to null, meaning no schema (or the current database owner). + */ + public $schemaName; +} diff --git a/framework/db/schema/mysql/CMysqlColumnSchema.php b/framework/db/schema/mysql/CMysqlColumnSchema.php new file mode 100644 index 0000000..7ffb03d --- /dev/null +++ b/framework/db/schema/mysql/CMysqlColumnSchema.php @@ -0,0 +1,72 @@ +<?php +/** + * CMysqlColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CMysqlColumnSchema class describes the column meta data of a MySQL table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CMysqlColumnSchema.php 3204 2011-05-05 21:36:32Z alexander.makarow $ + * @package system.db.schema.mysql + * @since 1.0 + */ +class CMysqlColumnSchema extends CDbColumnSchema +{ + /** + * Extracts the PHP type from DB type. + * @param string $dbType DB type + */ + protected function extractType($dbType) + { + if(strncmp($dbType,'enum',4)===0) + $this->type='string'; + else if(strpos($dbType,'float')!==false || strpos($dbType,'double')!==false) + $this->type='double'; + else if(strpos($dbType,'bool')!==false) + $this->type='boolean'; + else if(strpos($dbType,'int')===0 && strpos($dbType,'unsigned')===false || preg_match('/(bit|tinyint|smallint|mediumint)/',$dbType)) + $this->type='integer'; + else + $this->type='string'; + } + + /** + * Extracts the default value for the column. + * The value is typecasted to correct PHP type. + * @param mixed $defaultValue the default value obtained from metadata + */ + protected function extractDefault($defaultValue) + { + if($this->dbType==='timestamp' && $defaultValue==='CURRENT_TIMESTAMP') + $this->defaultValue=null; + else + parent::extractDefault($defaultValue); + } + + /** + * Extracts size, precision and scale information from column's DB type. + * @param string $dbType the column's DB type + */ + protected function extractLimit($dbType) + { + if (strncmp($dbType, 'enum', 4)===0 && preg_match('/\((.*)\)/',$dbType,$matches)) + { + $values = explode(',', $matches[1]); + $size = 0; + foreach($values as $value) + { + if(($n=strlen($value)) > $size) + $size=$n; + } + $this->size = $this->precision = $size-2; + } + else + parent::extractLimit($dbType); + } +}
\ No newline at end of file diff --git a/framework/db/schema/mysql/CMysqlSchema.php b/framework/db/schema/mysql/CMysqlSchema.php new file mode 100644 index 0000000..4048b4b --- /dev/null +++ b/framework/db/schema/mysql/CMysqlSchema.php @@ -0,0 +1,309 @@ +<?php +/** + * CMysqlSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CMysqlSchema is the class for retrieving metadata information from a MySQL database (version 4.1.x and 5.x). + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CMysqlSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.mysql + * @since 1.0 + */ +class CMysqlSchema extends CDbSchema +{ + /** + * @var array the abstract column types mapped to physical column types. + * @since 1.1.6 + */ + public $columnTypes=array( + 'pk' => 'int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY', + 'string' => 'varchar(255)', + 'text' => 'text', + 'integer' => 'int(11)', + 'float' => 'float', + 'decimal' => 'decimal', + 'datetime' => 'datetime', + 'timestamp' => 'timestamp', + 'time' => 'time', + 'date' => 'date', + 'binary' => 'blob', + 'boolean' => 'tinyint(1)', + 'money' => 'decimal(19,4)', + ); + + /** + * Quotes a table name for use in a query. + * A simple table name does not schema prefix. + * @param string $name table name + * @return string the properly quoted table name + * @since 1.1.6 + */ + public function quoteSimpleTableName($name) + { + return '`'.$name.'`'; + } + + /** + * Quotes a column name for use in a query. + * A simple column name does not contain prefix. + * @param string $name column name + * @return string the properly quoted column name + * @since 1.1.6 + */ + public function quoteSimpleColumnName($name) + { + return '`'.$name.'`'; + } + + /** + * Compares two table names. + * The table names can be either quoted or unquoted. This method + * will consider both cases. + * @param string $name1 table name 1 + * @param string $name2 table name 2 + * @return boolean whether the two table names refer to the same table. + */ + public function compareTableNames($name1,$name2) + { + return parent::compareTableNames(strtolower($name1),strtolower($name2)); + } + + /** + * Resets the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param CDbTableSchema $table the table schema whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @since 1.1 + */ + public function resetSequence($table,$value=null) + { + if($table->sequenceName!==null) + { + if($value===null) + $value=$this->getDbConnection()->createCommand("SELECT MAX(`{$table->primaryKey}`) FROM {$table->rawName}")->queryScalar()+1; + else + $value=(int)$value; + $this->getDbConnection()->createCommand("ALTER TABLE {$table->rawName} AUTO_INCREMENT=$value")->execute(); + } + } + + /** + * Enables or disables integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @since 1.1 + */ + public function checkIntegrity($check=true,$schema='') + { + $this->getDbConnection()->createCommand('SET FOREIGN_KEY_CHECKS='.($check?1:0))->execute(); + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return CMysqlTableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function loadTable($name) + { + $table=new CMysqlTableSchema; + $this->resolveTableNames($table,$name); + + if($this->findColumns($table)) + { + $this->findConstraints($table); + return $table; + } + else + return null; + } + + /** + * Generates various kinds of table names. + * @param CMysqlTableSchema $table the table instance + * @param string $name the unquoted table name + */ + protected function resolveTableNames($table,$name) + { + $parts=explode('.',str_replace('`','',$name)); + if(isset($parts[1])) + { + $table->schemaName=$parts[0]; + $table->name=$parts[1]; + $table->rawName=$this->quoteTableName($table->schemaName).'.'.$this->quoteTableName($table->name); + } + else + { + $table->name=$parts[0]; + $table->rawName=$this->quoteTableName($table->name); + } + } + + /** + * Collects the table column metadata. + * @param CMysqlTableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $sql='SHOW COLUMNS FROM '.$table->rawName; + try + { + $columns=$this->getDbConnection()->createCommand($sql)->queryAll(); + } + catch(Exception $e) + { + return false; + } + foreach($columns as $column) + { + $c=$this->createColumn($column); + $table->columns[$c->name]=$c; + if($c->isPrimaryKey) + { + if($table->primaryKey===null) + $table->primaryKey=$c->name; + else if(is_string($table->primaryKey)) + $table->primaryKey=array($table->primaryKey,$c->name); + else + $table->primaryKey[]=$c->name; + if($c->autoIncrement) + $table->sequenceName=''; + } + } + return true; + } + + /** + * Creates a table column. + * @param array $column column metadata + * @return CDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new CMysqlColumnSchema; + $c->name=$column['Field']; + $c->rawName=$this->quoteColumnName($c->name); + $c->allowNull=$column['Null']==='YES'; + $c->isPrimaryKey=strpos($column['Key'],'PRI')!==false; + $c->isForeignKey=false; + $c->init($column['Type'],$column['Default']); + $c->autoIncrement=strpos(strtolower($column['Extra']),'auto_increment')!==false; + + return $c; + } + + /** + * @return float server version. + */ + protected function getServerVersion() + { + $version=$this->getDbConnection()->getAttribute(PDO::ATTR_SERVER_VERSION); + $digits=array(); + preg_match('/(\d+)\.(\d+)\.(\d+)/', $version, $digits); + return floatval($digits[1].'.'.$digits[2].$digits[3]); + } + + /** + * Collects the foreign key column details for the given table. + * @param CMysqlTableSchema $table the table metadata + */ + protected function findConstraints($table) + { + $row=$this->getDbConnection()->createCommand('SHOW CREATE TABLE '.$table->rawName)->queryRow(); + $matches=array(); + $regexp='/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi'; + foreach($row as $sql) + { + if(preg_match_all($regexp,$sql,$matches,PREG_SET_ORDER)) + break; + } + foreach($matches as $match) + { + $keys=array_map('trim',explode(',',str_replace('`','',$match[1]))); + $fks=array_map('trim',explode(',',str_replace('`','',$match[3]))); + foreach($keys as $k=>$name) + { + $table->foreignKeys[$name]=array(str_replace('`','',$match[2]),$fks[$k]); + if(isset($table->columns[$name])) + $table->columns[$name]->isForeignKey=true; + } + } + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * If not empty, the returned table names will be prefixed with the schema name. + * @return array all table names in the database. + */ + protected function findTableNames($schema='') + { + if($schema==='') + return $this->getDbConnection()->createCommand('SHOW TABLES')->queryColumn(); + $names=$this->getDbConnection()->createCommand('SHOW TABLES FROM '.$this->quoteTableName($schema))->queryColumn(); + foreach($names as &$name) + $name=$schema.'.'.$name; + return $names; + } + + /** + * Builds a SQL statement for renaming a column. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB column. + * @since 1.1.6 + */ + public function renameColumn($table, $name, $newName) + { + $db=$this->getDbConnection(); + $row=$db->createCommand('SHOW CREATE TABLE '.$db->quoteTableName($table))->queryRow(); + if($row===false) + throw new CDbException(Yii::t('yii','Unable to find "{column}" in table "{table}".',array('{column}'=>$name,'{table}'=>$table))); + if(isset($row['Create Table'])) + $sql=$row['Create Table']; + else + { + $row=array_values($row); + $sql=$row[1]; + } + if(preg_match_all('/^\s*`(.*?)`\s+(.*?),?$/m',$sql,$matches)) + { + foreach($matches[1] as $i=>$c) + { + if($c===$name) + { + return "ALTER TABLE ".$db->quoteTableName($table) + . " CHANGE ".$db->quoteColumnName($name) + . ' '.$db->quoteColumnName($newName).' '.$matches[2][$i]; + } + } + } + + // try to give back a SQL anyway + return "ALTER TABLE ".$db->quoteTableName($table) + . " CHANGE ".$db->quoteColumnName($name).' '.$newName; + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a foreign key constraint. + * @since 1.1.6 + */ + public function dropForeignKey($name, $table) + { + return 'ALTER TABLE '.$this->quoteTableName($table) + .' DROP FOREIGN KEY '.$this->quoteColumnName($name); + } +} diff --git a/framework/db/schema/mysql/CMysqlTableSchema.php b/framework/db/schema/mysql/CMysqlTableSchema.php new file mode 100644 index 0000000..9950598 --- /dev/null +++ b/framework/db/schema/mysql/CMysqlTableSchema.php @@ -0,0 +1,26 @@ +<?php +/** + * CMysqlTableSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CMysqlTableSchema represents the metadata for a MySQL table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CMysqlTableSchema.php 2799 2011-01-01 19:31:13Z qiang.xue $ + * @package system.db.schema.mysql + * @since 1.0 + */ +class CMysqlTableSchema extends CDbTableSchema +{ + /** + * @var string name of the schema (database) that this table belongs to. + * Defaults to null, meaning no schema (or the current database). + */ + public $schemaName; +} diff --git a/framework/db/schema/oci/COciColumnSchema.php b/framework/db/schema/oci/COciColumnSchema.php new file mode 100644 index 0000000..bad57c3 --- /dev/null +++ b/framework/db/schema/oci/COciColumnSchema.php @@ -0,0 +1,66 @@ +<?php +/** + * COciColumnSchema class file. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * COciColumnSchema class describes the column meta data of a Oracle table. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @version $Id: COciColumnSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.oci + */ +class COciColumnSchema extends CDbColumnSchema +{ + /** + * Extracts the PHP type from DB type. + * @param string $dbType DB type + * @return string + */ + protected function extractOraType($dbType){ + if(strpos($dbType,'FLOAT')!==false) return 'double'; + + if (strpos($dbType,'NUMBER')!==false || strpos($dbType,'INTEGER')!==false) + { + if(strpos($dbType,'(') && preg_match('/\((.*)\)/',$dbType,$matches)) + { + $values=explode(',',$matches[1]); + if(isset($values[1]) and (((int)$values[1]) > 0)) + return 'double'; + else + return 'integer'; + } + else + return 'double'; + } + else + return 'string'; + } + + /** + * Extracts the PHP type from DB type. + * @param string $dbType DB type + */ + protected function extractType($dbType) + { + $this->type=$this->extractOraType($dbType); + } + + /** + * Extracts the default value for the column. + * The value is typecasted to correct PHP type. + * @param mixed $defaultValue the default value obtained from metadata + */ + protected function extractDefault($defaultValue) + { + if(stripos($defaultValue,'timestamp')!==false) + $this->defaultValue=null; + else + parent::extractDefault($defaultValue); + } +} diff --git a/framework/db/schema/oci/COciCommandBuilder.php b/framework/db/schema/oci/COciCommandBuilder.php new file mode 100644 index 0000000..4c041be --- /dev/null +++ b/framework/db/schema/oci/COciCommandBuilder.php @@ -0,0 +1,125 @@ +<?php +/** + * COciCommandBuilder class file. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * COciCommandBuilder provides basic methods to create query commands for tables. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @version $Id: COciCommandBuilder.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.oci + */ +class COciCommandBuilder extends CDbCommandBuilder +{ + /** + * @var integer the last insertion ID + */ + public $returnID; + + /** + * Returns the last insertion ID for the specified table. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @return mixed last insertion id. Null is returned if no sequence name. + */ + public function getLastInsertID($table) + { + return $this->returnID; + } + + /** + * Alters the SQL to apply LIMIT and OFFSET. + * Default implementation is applicable for PostgreSQL, MySQL and SQLite. + * @param string $sql SQL query string without LIMIT and OFFSET. + * @param integer $limit maximum number of rows, -1 to ignore limit. + * @param integer $offset row offset, -1 to ignore offset. + * @return string SQL with LIMIT and OFFSET + */ + public function applyLimit($sql,$limit,$offset) + { + if (($limit < 0) and ($offset < 0)) return $sql; + + $filters = array(); + if($offset>0){ + $filters[] = 'rowNumId > '.(int)$offset; + } + + if($limit>=0){ + $filters[]= 'rownum <= '.(int)$limit; + } + + if (count($filters) > 0){ + $filter = implode(' and ', $filters); + $filter= " WHERE ".$filter; + }else{ + $filter = ''; + } + + + $sql = <<<EOD + WITH USER_SQL AS ({$sql}), + PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) + SELECT * + FROM PAGINATION + {$filter} +EOD; + + return $sql; + } + + /** + * Creates an INSERT command. + * @param mixed $table the table schema ({@link CDbTableSchema}) or the table name (string). + * @param array $data data to be inserted (column name=>column value). If a key is not a valid column name, the corresponding value will be ignored. + * @return CDbCommand insert command + */ + public function createInsertCommand($table,$data) + { + $this->ensureTable($table); + $fields=array(); + $values=array(); + $placeholders=array(); + $i=0; + foreach($data as $name=>$value) + { + if(($column=$table->getColumn($name))!==null && ($value!==null || $column->allowNull)) + { + $fields[]=$column->rawName; + if($value instanceof CDbExpression) + { + $placeholders[]=$value->expression; + foreach($value->params as $n=>$v) + $values[$n]=$v; + } + else + { + $placeholders[]=self::PARAM_PREFIX.$i; + $values[self::PARAM_PREFIX.$i]=$column->typecast($value); + $i++; + } + } + } + + $sql="INSERT INTO {$table->rawName} (".implode(', ',$fields).') VALUES ('.implode(', ',$placeholders).')'; + + if(is_string($table->primaryKey) && ($column=$table->getColumn($table->primaryKey))!==null && $column->type!=='string') + { + $sql.=' RETURNING '.$column->rawName.' INTO :RETURN_ID'; + $command=$this->getDbConnection()->createCommand($sql); + $command->bindParam(':RETURN_ID', $this->returnID, PDO::PARAM_INT, 12); + $table->sequenceName='RETURN_ID'; + } + else + $command=$this->getDbConnection()->createCommand($sql); + + foreach($values as $name=>$value) + $command->bindValue($name,$value); + + return $command; + } +}
\ No newline at end of file diff --git a/framework/db/schema/oci/COciSchema.php b/framework/db/schema/oci/COciSchema.php new file mode 100644 index 0000000..90ccaa9 --- /dev/null +++ b/framework/db/schema/oci/COciSchema.php @@ -0,0 +1,350 @@ +<?php +/** + * COciSchema class file. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * COciSchema is the class for retrieving metadata information from an Oracle database. + * + * @property string $defaultSchema Default schema. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @version $Id: COciSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.oci + */ +class COciSchema extends CDbSchema +{ + private $_defaultSchema = ''; + + /** + * @var array the abstract column types mapped to physical column types. + * @since 1.1.6 + */ + public $columnTypes=array( + 'pk' => 'NUMBER(10) NOT NULL PRIMARY KEY', + 'string' => 'VARCHAR2(255)', + 'text' => 'CLOB', + 'integer' => 'NUMBER(10)', + 'float' => 'NUMBER', + 'decimal' => 'NUMBER', + 'datetime' => 'TIMESTAMP', + 'timestamp' => 'TIMESTAMP', + 'time' => 'TIMESTAMP', + 'date' => 'DATE', + 'binary' => 'BLOB', + 'boolean' => 'NUMBER(1)', + 'money' => 'NUMBER(19,4)', + ); + + /** + * Quotes a table name for use in a query. + * A simple table name does not schema prefix. + * @param string $name table name + * @return string the properly quoted table name + * @since 1.1.6 + */ + public function quoteSimpleTableName($name) + { + return '"'.$name.'"'; + } + + /** + * Quotes a column name for use in a query. + * A simple column name does not contain prefix. + * @param string $name column name + * @return string the properly quoted column name + * @since 1.1.6 + */ + public function quoteSimpleColumnName($name) + { + return '"'.$name.'"'; + } + + /** + * Creates a command builder for the database. + * This method may be overridden by child classes to create a DBMS-specific command builder. + * @return CDbCommandBuilder command builder instance + */ + protected function createCommandBuilder() + { + return new COciCommandBuilder($this); + } + + /** + * @param string $schema default schema. + */ + public function setDefaultSchema($schema) + { + $this->_defaultSchema=$schema; + } + + /** + * @return string default schema. + */ + public function getDefaultSchema() + { + if (!strlen($this->_defaultSchema)) + { + $this->setDefaultSchema(strtoupper($this->getDbConnection()->username)); + } + + return $this->_defaultSchema; + } + + /** + * @param string $table table name with optional schema name prefix, uses default schema name prefix is not provided. + * @return array tuple as ($schemaName,$tableName) + */ + protected function getSchemaTableName($table) + { + $table = strtoupper($table); + if(count($parts= explode('.', str_replace('"','',$table))) > 1) + return array($parts[0], $parts[1]); + else + return array($this->getDefaultSchema(),$parts[0]); + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return CDbTableSchema driver dependent table metadata. + */ + protected function loadTable($name) + { + $table=new COciTableSchema; + $this->resolveTableNames($table,$name); + + if(!$this->findColumns($table)) + return null; + $this->findConstraints($table); + + return $table; + } + + /** + * Generates various kinds of table names. + * @param COciTableSchema $table the table instance + * @param string $name the unquoted table name + */ + protected function resolveTableNames($table,$name) + { + $parts=explode('.',str_replace('"','',$name)); + if(isset($parts[1])) + { + $schemaName=$parts[0]; + $tableName=$parts[1]; + } + else + { + $schemaName=$this->getDefaultSchema(); + $tableName=$parts[0]; + } + + $table->name=$tableName; + $table->schemaName=$schemaName; + if($schemaName===$this->getDefaultSchema()) + $table->rawName=$this->quoteTableName($tableName); + else + $table->rawName=$this->quoteTableName($schemaName).'.'.$this->quoteTableName($tableName); + } + + /** + * Collects the table column metadata. + * @param COciTableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $schemaName=$table->schemaName; + $tableName=$table->name; + + $sql=<<<EOD +SELECT a.column_name, a.data_type || + case + when data_precision is not null + then '(' || a.data_precision || + case when a.data_scale > 0 then ',' || a.data_scale else '' end + || ')' + when data_type = 'DATE' then '' + when data_type = 'NUMBER' then '' + else '(' || to_char(a.data_length) || ')' + end as data_type, + a.nullable, a.data_default, + ( SELECT D.constraint_type + FROM ALL_CONS_COLUMNS C + inner join ALL_constraints D on D.OWNER = C.OWNER and D.constraint_name = C.constraint_name + WHERE C.OWNER = B.OWNER + and C.table_name = B.object_name + and C.column_name = A.column_name + and D.constraint_type = 'P') as Key +FROM ALL_TAB_COLUMNS A +inner join ALL_OBJECTS B ON b.owner = a.owner and ltrim(B.OBJECT_NAME) = ltrim(A.TABLE_NAME) +WHERE + a.owner = '{$schemaName}' + and (b.object_type = 'TABLE' or b.object_type = 'VIEW') + and b.object_name = '{$tableName}' +ORDER by a.column_id +EOD; + + $command=$this->getDbConnection()->createCommand($sql); + + if(($columns=$command->queryAll())===array()){ + return false; + } + + foreach($columns as $column) + { + $c=$this->createColumn($column); + + $table->columns[$c->name]=$c; + if($c->isPrimaryKey) + { + if($table->primaryKey===null) + $table->primaryKey=$c->name; + else if(is_string($table->primaryKey)) + $table->primaryKey=array($table->primaryKey,$c->name); + else + $table->primaryKey[]=$c->name; + $table->sequenceName=''; + $c->autoIncrement=true; + } + } + return true; + } + + /** + * Creates a table column. + * @param array $column column metadata + * @return CDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new COciColumnSchema; + $c->name=$column['COLUMN_NAME']; + $c->rawName=$this->quoteColumnName($c->name); + $c->allowNull=$column['NULLABLE']==='Y'; + $c->isPrimaryKey=strpos($column['KEY'],'P')!==false; + $c->isForeignKey=false; + $c->init($column['DATA_TYPE'],$column['DATA_DEFAULT']); + + return $c; + } + + /** + * Collects the primary and foreign key column details for the given table. + * @param COciTableSchema $table the table metadata + */ + protected function findConstraints($table) + { + $sql=<<<EOD + SELECT D.constraint_type as CONSTRAINT_TYPE, C.COLUMN_NAME, C.position, D.r_constraint_name, + E.table_name as table_ref, f.column_name as column_ref, + C.table_name + FROM ALL_CONS_COLUMNS C + inner join ALL_constraints D on D.OWNER = C.OWNER and D.constraint_name = C.constraint_name + left join ALL_constraints E on E.OWNER = D.r_OWNER and E.constraint_name = D.r_constraint_name + left join ALL_cons_columns F on F.OWNER = E.OWNER and F.constraint_name = E.constraint_name and F.position = c.position + WHERE C.OWNER = '{$table->schemaName}' + and C.table_name = '{$table->name}' + and D.constraint_type <> 'P' + order by d.constraint_name, c.position +EOD; + $command=$this->getDbConnection()->createCommand($sql); + foreach($command->queryAll() as $row) + { + if($row['CONSTRAINT_TYPE']==='R') // foreign key + { + $name = $row["COLUMN_NAME"]; + $table->foreignKeys[$name]=array($row["TABLE_REF"], $row["COLUMN_REF"]); + if(isset($table->columns[$name])) + $table->columns[$name]->isForeignKey=true; + } + + } + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * If not empty, the returned table names will be prefixed with the schema name. + * @return array all table names in the database. + */ + protected function findTableNames($schema='') + { + if($schema==='') + { + $sql=<<<EOD +SELECT table_name, '{$schema}' as table_schema FROM user_tables +EOD; + $command=$this->getDbConnection()->createCommand($sql); + } + else + { + $sql=<<<EOD +SELECT object_name as table_name, owner as table_schema FROM all_objects +WHERE object_type = 'TABLE' AND owner=:schema +EOD; + $command=$this->getDbConnection()->createCommand($sql); + $command->bindParam(':schema',$schema); + } + + $rows=$command->queryAll(); + $names=array(); + foreach($rows as $row) + { + if($schema===$this->getDefaultSchema() || $schema==='') + $names[]=$row['TABLE_NAME']; + else + $names[]=$row['TABLE_SCHEMA'].'.'.$row['TABLE_NAME']; + } + return $names; + } + + /** + * Builds a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + * @since 1.1.6 + */ + public function renameTable($table, $newName) + { + return 'ALTER TABLE ' . $this->quoteTableName($table) . ' RENAME TO ' . $this->quoteTableName($newName); + } + + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + * @since 1.1.6 + */ + public function alterColumn($table, $column, $type) + { + $type=$this->getColumnType($type); + $sql='ALTER TABLE ' . $this->quoteTableName($table) . ' MODIFY ' + . $this->quoteColumnName($column) . ' ' + . $this->getColumnType($type); + return $sql; + } + + /** + * Builds a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping an index. + * @since 1.1.6 + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX '.$this->quoteTableName($name); + } +} diff --git a/framework/db/schema/oci/COciTableSchema.php b/framework/db/schema/oci/COciTableSchema.php new file mode 100644 index 0000000..8892745 --- /dev/null +++ b/framework/db/schema/oci/COciTableSchema.php @@ -0,0 +1,25 @@ +<?php +/** + * COciTableSchema class file. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * COciTableSchema represents the metadata for a Oracle table. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @version $Id: COciTableSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.oci + */ +class COciTableSchema extends CDbTableSchema +{ + /** + * @var string name of the schema (database) that this table belongs to. + * Defaults to null, meaning no schema (or the current database). + */ + public $schemaName; +} diff --git a/framework/db/schema/pgsql/CPgsqlColumnSchema.php b/framework/db/schema/pgsql/CPgsqlColumnSchema.php new file mode 100644 index 0000000..45b6f56 --- /dev/null +++ b/framework/db/schema/pgsql/CPgsqlColumnSchema.php @@ -0,0 +1,58 @@ +<?php +/** + * CPgsqlColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CPgsqlColumnSchema class describes the column meta data of a PostgreSQL table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CPgsqlColumnSchema.php 2799 2011-01-01 19:31:13Z qiang.xue $ + * @package system.db.schema.pgsql + * @since 1.0 + */ +class CPgsqlColumnSchema extends CDbColumnSchema +{ + /** + * Extracts the PHP type from DB type. + * @param string $dbType DB type + */ + protected function extractType($dbType) + { + if(strpos($dbType,'[')!==false || strpos($dbType,'char')!==false || strpos($dbType,'text')!==false) + $this->type='string'; + else if(strpos($dbType,'bool')!==false) + $this->type='boolean'; + else if(preg_match('/(real|float|double)/',$dbType)) + $this->type='double'; + else if(preg_match('/(integer|oid|serial|smallint)/',$dbType)) + $this->type='integer'; + else + $this->type='string'; + } + + /** + * Extracts the default value for the column. + * The value is typecasted to correct PHP type. + * @param mixed $defaultValue the default value obtained from metadata + */ + protected function extractDefault($defaultValue) + { + if($defaultValue==='true') + $this->defaultValue=true; + else if($defaultValue==='false') + $this->defaultValue=false; + else if(strpos($defaultValue,'nextval')===0) + $this->defaultValue=null; + else if(preg_match('/^\'(.*)\'::/',$defaultValue,$matches)) + $this->defaultValue=$this->typecast(str_replace("''","'",$matches[1])); + else if(preg_match('/^-?\d+(\.\d*)?$/',$defaultValue,$matches)) + $this->defaultValue=$this->typecast($defaultValue); + // else is null + } +} diff --git a/framework/db/schema/pgsql/CPgsqlSchema.php b/framework/db/schema/pgsql/CPgsqlSchema.php new file mode 100644 index 0000000..bff95e0 --- /dev/null +++ b/framework/db/schema/pgsql/CPgsqlSchema.php @@ -0,0 +1,424 @@ +<?php +/** + * CPgsqlSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CPgsqlSchema is the class for retrieving metadata information from a PostgreSQL database. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CPgsqlSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.pgsql + * @since 1.0 + */ +class CPgsqlSchema extends CDbSchema +{ + const DEFAULT_SCHEMA='public'; + + /** + * @var array the abstract column types mapped to physical column types. + * @since 1.1.6 + */ + public $columnTypes=array( + 'pk' => 'serial NOT NULL PRIMARY KEY', + 'string' => 'character varying (255)', + 'text' => 'text', + 'integer' => 'integer', + 'float' => 'double precision', + 'decimal' => 'numeric', + 'datetime' => 'time', + 'timestamp' => 'timestamp', + 'time' => 'time', + 'date' => 'date', + 'binary' => 'bytea', + 'boolean' => 'boolean', + 'money' => 'decimal(19,4)', + ); + + private $_sequences=array(); + + /** + * Quotes a table name for use in a query. + * A simple table name does not schema prefix. + * @param string $name table name + * @return string the properly quoted table name + * @since 1.1.6 + */ + public function quoteSimpleTableName($name) + { + return '"'.$name.'"'; + } + + /** + * Resets the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param CDbTableSchema $table the table schema whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @since 1.1 + */ + public function resetSequence($table,$value=null) + { + if($table->sequenceName!==null) + { + $seq='"'.$table->sequenceName.'"'; + if(strpos($seq,'.')!==false) + $seq=str_replace('.','"."',$seq); + if($value===null) + $value="(SELECT COALESCE(MAX(\"{$table->primaryKey}\"),0) FROM {$table->rawName}) + 1"; + else + $value=(int)$value; + $this->getDbConnection()->createCommand("SELECT SETVAL('$seq', $value, false)")->execute(); + } + } + + /** + * Enables or disables integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @since 1.1 + */ + public function checkIntegrity($check=true,$schema='') + { + $enable=$check ? 'ENABLE' : 'DISABLE'; + $tableNames=$this->getTableNames($schema); + $db=$this->getDbConnection(); + foreach($tableNames as $tableName) + { + $tableName='"'.$tableName.'"'; + if(strpos($tableName,'.')!==false) + $tableName=str_replace('.','"."',$tableName); + $db->createCommand("ALTER TABLE $tableName $enable TRIGGER ALL")->execute(); + } + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return CDbTableSchema driver dependent table metadata. + */ + protected function loadTable($name) + { + $table=new CPgsqlTableSchema; + $this->resolveTableNames($table,$name); + if(!$this->findColumns($table)) + return null; + $this->findConstraints($table); + + if(is_string($table->primaryKey) && isset($this->_sequences[$table->rawName.'.'.$table->primaryKey])) + $table->sequenceName=$this->_sequences[$table->rawName.'.'.$table->primaryKey]; + else if(is_array($table->primaryKey)) + { + foreach($table->primaryKey as $pk) + { + if(isset($this->_sequences[$table->rawName.'.'.$pk])) + { + $table->sequenceName=$this->_sequences[$table->rawName.'.'.$pk]; + break; + } + } + } + + return $table; + } + + /** + * Generates various kinds of table names. + * @param CPgsqlTableSchema $table the table instance + * @param string $name the unquoted table name + */ + protected function resolveTableNames($table,$name) + { + $parts=explode('.',str_replace('"','',$name)); + if(isset($parts[1])) + { + $schemaName=$parts[0]; + $tableName=$parts[1]; + } + else + { + $schemaName=self::DEFAULT_SCHEMA; + $tableName=$parts[0]; + } + + $table->name=$tableName; + $table->schemaName=$schemaName; + if($schemaName===self::DEFAULT_SCHEMA) + $table->rawName=$this->quoteTableName($tableName); + else + $table->rawName=$this->quoteTableName($schemaName).'.'.$this->quoteTableName($tableName); + } + + /** + * Collects the table column metadata. + * @param CPgsqlTableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $sql=<<<EOD +SELECT a.attname, LOWER(format_type(a.atttypid, a.atttypmod)) AS type, d.adsrc, a.attnotnull, a.atthasdef +FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum +WHERE a.attnum > 0 AND NOT a.attisdropped + AND a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table + AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = :schema)) +ORDER BY a.attnum +EOD; + $command=$this->getDbConnection()->createCommand($sql); + $command->bindValue(':table',$table->name); + $command->bindValue(':schema',$table->schemaName); + + if(($columns=$command->queryAll())===array()) + return false; + + foreach($columns as $column) + { + $c=$this->createColumn($column); + $table->columns[$c->name]=$c; + + if(stripos($column['adsrc'],'nextval')===0 && preg_match('/nextval\([^\']*\'([^\']+)\'[^\)]*\)/i',$column['adsrc'],$matches)) + { + if(strpos($matches[1],'.')!==false || $table->schemaName===self::DEFAULT_SCHEMA) + $this->_sequences[$table->rawName.'.'.$c->name]=$matches[1]; + else + $this->_sequences[$table->rawName.'.'.$c->name]=$table->schemaName.'.'.$matches[1]; + $c->autoIncrement=true; + } + } + return true; + } + + /** + * Creates a table column. + * @param array $column column metadata + * @return CDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new CPgsqlColumnSchema; + $c->name=$column['attname']; + $c->rawName=$this->quoteColumnName($c->name); + $c->allowNull=!$column['attnotnull']; + $c->isPrimaryKey=false; + $c->isForeignKey=false; + + $c->init($column['type'],$column['atthasdef'] ? $column['adsrc'] : null); + + return $c; + } + + /** + * Collects the primary and foreign key column details for the given table. + * @param CPgsqlTableSchema $table the table metadata + */ + protected function findConstraints($table) + { + $sql=<<<EOD +SELECT conname, consrc, contype, indkey FROM ( + SELECT + conname, + CASE WHEN contype='f' THEN + pg_catalog.pg_get_constraintdef(oid) + ELSE + 'CHECK (' || consrc || ')' + END AS consrc, + contype, + conrelid AS relid, + NULL AS indkey + FROM + pg_catalog.pg_constraint + WHERE + contype IN ('f', 'c') + UNION ALL + SELECT + pc.relname, + NULL, + CASE WHEN indisprimary THEN + 'p' + ELSE + 'u' + END, + pi.indrelid, + indkey + FROM + pg_catalog.pg_class pc, + pg_catalog.pg_index pi + WHERE + pc.oid=pi.indexrelid + AND EXISTS ( + SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c + ON (d.refclassid = c.tableoid AND d.refobjid = c.oid) + WHERE d.classid = pc.tableoid AND d.objid = pc.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p') + ) +) AS sub +WHERE relid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table + AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace + WHERE nspname=:schema)) +EOD; + $command=$this->getDbConnection()->createCommand($sql); + $command->bindValue(':table',$table->name); + $command->bindValue(':schema',$table->schemaName); + foreach($command->queryAll() as $row) + { + if($row['contype']==='p') // primary key + $this->findPrimaryKey($table,$row['indkey']); + else if($row['contype']==='f') // foreign key + $this->findForeignKey($table,$row['consrc']); + } + } + + /** + * Collects primary key information. + * @param CPgsqlTableSchema $table the table metadata + * @param string $indices pgsql primary key index list + */ + protected function findPrimaryKey($table,$indices) + { + $indices=implode(', ',preg_split('/\s+/',$indices)); + $sql=<<<EOD +SELECT attnum, attname FROM pg_catalog.pg_attribute WHERE + attrelid=( + SELECT oid FROM pg_catalog.pg_class WHERE relname=:table AND relnamespace=( + SELECT oid FROM pg_catalog.pg_namespace WHERE nspname=:schema + ) + ) + AND attnum IN ({$indices}) +EOD; + $command=$this->getDbConnection()->createCommand($sql); + $command->bindValue(':table',$table->name); + $command->bindValue(':schema',$table->schemaName); + foreach($command->queryAll() as $row) + { + $name=$row['attname']; + if(isset($table->columns[$name])) + { + $table->columns[$name]->isPrimaryKey=true; + if($table->primaryKey===null) + $table->primaryKey=$name; + else if(is_string($table->primaryKey)) + $table->primaryKey=array($table->primaryKey,$name); + else + $table->primaryKey[]=$name; + } + } + } + + /** + * Collects foreign key information. + * @param CPgsqlTableSchema $table the table metadata + * @param string $src pgsql foreign key definition + */ + protected function findForeignKey($table,$src) + { + $matches=array(); + $brackets='\(([^\)]+)\)'; + $pattern="/FOREIGN\s+KEY\s+{$brackets}\s+REFERENCES\s+([^\(]+){$brackets}/i"; + if(preg_match($pattern,str_replace('"','',$src),$matches)) + { + $keys=preg_split('/,\s+/', $matches[1]); + $tableName=$matches[2]; + $fkeys=preg_split('/,\s+/', $matches[3]); + foreach($keys as $i=>$key) + { + $table->foreignKeys[$key]=array($tableName,$fkeys[$i]); + if(isset($table->columns[$key])) + $table->columns[$key]->isForeignKey=true; + } + } + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * If not empty, the returned table names will be prefixed with the schema name. + * @return array all table names in the database. + */ + protected function findTableNames($schema='') + { + if($schema==='') + $schema=self::DEFAULT_SCHEMA; + $sql=<<<EOD +SELECT table_name, table_schema FROM information_schema.tables +WHERE table_schema=:schema AND table_type='BASE TABLE' +EOD; + $command=$this->getDbConnection()->createCommand($sql); + $command->bindParam(':schema',$schema); + $rows=$command->queryAll(); + $names=array(); + foreach($rows as $row) + { + if($schema===self::DEFAULT_SCHEMA) + $names[]=$row['table_name']; + else + $names[]=$row['table_schema'].'.'.$row['table_name']; + } + return $names; + } + + /** + * Builds a SQL statement for renaming a DB table. + * @param string $table the table to be renamed. The name will be properly quoted by the method. + * @param string $newName the new table name. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB table. + * @since 1.1.6 + */ + public function renameTable($table, $newName) + { + return 'ALTER TABLE ' . $this->quoteTableName($table) . ' RENAME TO ' . $this->quoteTableName($newName); + } + + /** + * Builds a SQL statement for adding a new DB column. + * @param string $table the table that the new column will be added to. The table name will be properly quoted by the method. + * @param string $column the name of the new column. The name will be properly quoted by the method. + * @param string $type the column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for adding a new column. + * @since 1.1.6 + */ + public function addColumn($table, $column, $type) + { + $type=$this->getColumnType($type); + $sql='ALTER TABLE ' . $this->quoteTableName($table) + . ' ADD COLUMN ' . $this->quoteColumnName($column) . ' ' + . $this->getColumnType($type); + return $sql; + } + + /** + * Builds a SQL statement for changing the definition of a column. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + * @since 1.1.6 + */ + public function alterColumn($table, $column, $type) + { + $type=$this->getColumnType($type); + $sql='ALTER TABLE ' . $this->quoteTableName($table) . ' ALTER COLUMN ' + . $this->quoteColumnName($column) . ' TYPE ' . $this->getColumnType($type); + return $sql; + } + + /** + * Builds a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping an index. + * @since 1.1.6 + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX '.$this->quoteTableName($name); + } +} diff --git a/framework/db/schema/pgsql/CPgsqlTableSchema.php b/framework/db/schema/pgsql/CPgsqlTableSchema.php new file mode 100644 index 0000000..39e0334 --- /dev/null +++ b/framework/db/schema/pgsql/CPgsqlTableSchema.php @@ -0,0 +1,25 @@ +<?php +/** + * CPgsqlTable class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CPgsqlTable represents the metadata for a PostgreSQL table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CPgsqlTableSchema.php 2799 2011-01-01 19:31:13Z qiang.xue $ + * @package system.db.schema.pgsql + * @since 1.0 + */ +class CPgsqlTableSchema extends CDbTableSchema +{ + /** + * @var string name of the schema that this table belongs to. + */ + public $schemaName; +} diff --git a/framework/db/schema/sqlite/CSqliteColumnSchema.php b/framework/db/schema/sqlite/CSqliteColumnSchema.php new file mode 100644 index 0000000..ae51434 --- /dev/null +++ b/framework/db/schema/sqlite/CSqliteColumnSchema.php @@ -0,0 +1,33 @@ +<?php +/** + * CSqliteColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CSqliteColumnSchema class describes the column meta data of a SQLite table. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CSqliteColumnSchema.php 2799 2011-01-01 19:31:13Z qiang.xue $ + * @package system.db.schema.sqlite + * @since 1.0 + */ +class CSqliteColumnSchema extends CDbColumnSchema +{ + /** + * Extracts the default value for the column. + * The value is typecasted to correct PHP type. + * @param mixed $defaultValue the default value obtained from metadata + */ + protected function extractDefault($defaultValue) + { + if($this->type==='string') // PHP 5.2.6 adds single quotes while 5.2.0 doesn't + $this->defaultValue=trim($defaultValue,"'\""); + else + $this->defaultValue=$this->typecast(strcasecmp($defaultValue,'null') ? $defaultValue : null); + } +} diff --git a/framework/db/schema/sqlite/CSqliteCommandBuilder.php b/framework/db/schema/sqlite/CSqliteCommandBuilder.php new file mode 100644 index 0000000..c4bf75a --- /dev/null +++ b/framework/db/schema/sqlite/CSqliteCommandBuilder.php @@ -0,0 +1,40 @@ +<?php +/** + * CSqliteCommandBuilder class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CSqliteCommandBuilder provides basic methods to create query commands for SQLite tables. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CSqliteCommandBuilder.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.sqlite + * @since 1.0 + */ +class CSqliteCommandBuilder extends CDbCommandBuilder +{ + /** + * Generates the expression for selecting rows with specified composite key values. + * This method is overridden because SQLite does not support the default + * IN expression with composite columns. + * @param CDbTableSchema $table the table schema + * @param array $values list of primary key values to be selected within + * @param string $prefix column prefix (ended with dot) + * @return string the expression for selection + */ + protected function createCompositeInCondition($table,$values,$prefix) + { + $keyNames=array(); + foreach(array_keys($values[0]) as $name) + $keyNames[]=$prefix.$table->columns[$name]->rawName; + $vs=array(); + foreach($values as $value) + $vs[]=implode("||','||",$value); + return implode("||','||",$keyNames).' IN ('.implode(', ',$vs).')'; + } +} diff --git a/framework/db/schema/sqlite/CSqliteSchema.php b/framework/db/schema/sqlite/CSqliteSchema.php new file mode 100644 index 0000000..c1c9d76 --- /dev/null +++ b/framework/db/schema/sqlite/CSqliteSchema.php @@ -0,0 +1,287 @@ +<?php +/** + * CSqliteSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CSqliteSchema is the class for retrieving metadata information from a SQLite (2/3) database. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CSqliteSchema.php 3515 2011-12-28 12:29:24Z mdomba $ + * @package system.db.schema.sqlite + * @since 1.0 + */ +class CSqliteSchema extends CDbSchema +{ + /** + * @var array the abstract column types mapped to physical column types. + * @since 1.1.6 + */ + public $columnTypes=array( + 'pk' => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL', + 'string' => 'varchar(255)', + 'text' => 'text', + 'integer' => 'integer', + 'float' => 'float', + 'decimal' => 'decimal', + 'datetime' => 'datetime', + 'timestamp' => 'timestamp', + 'time' => 'time', + 'date' => 'date', + 'binary' => 'blob', + 'boolean' => 'tinyint(1)', + 'money' => 'decimal(19,4)', + ); + + /** + * Resets the sequence value of a table's primary key. + * The sequence will be reset such that the primary key of the next new row inserted + * will have the specified value or 1. + * @param CDbTableSchema $table the table schema whose primary key sequence will be reset + * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, + * the next new row's primary key will have a value 1. + * @since 1.1 + */ + public function resetSequence($table,$value=null) + { + if($table->sequenceName!==null) + { + if($value===null) + $value=$this->getDbConnection()->createCommand("SELECT MAX(`{$table->primaryKey}`) FROM {$table->rawName}")->queryScalar(); + else + $value=(int)$value-1; + try + { + // it's possible sqlite_sequence does not exist + $this->getDbConnection()->createCommand("UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->name}'")->execute(); + } + catch(Exception $e) + { + } + } + } + + /** + * Enables or disables integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @since 1.1 + */ + public function checkIntegrity($check=true,$schema='') + { + // SQLite doesn't enforce integrity + return; + } + + /** + * Returns all table names in the database. + * @param string $schema the schema of the tables. This is not used for sqlite database. + * @return array all table names in the database. + */ + protected function findTableNames($schema='') + { + $sql="SELECT DISTINCT tbl_name FROM sqlite_master WHERE tbl_name<>'sqlite_sequence'"; + return $this->getDbConnection()->createCommand($sql)->queryColumn(); + } + + /** + * Creates a command builder for the database. + * @return CSqliteCommandBuilder command builder instance + */ + protected function createCommandBuilder() + { + return new CSqliteCommandBuilder($this); + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return CDbTableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function loadTable($name) + { + $table=new CDbTableSchema; + $table->name=$name; + $table->rawName=$this->quoteTableName($name); + + if($this->findColumns($table)) + { + $this->findConstraints($table); + return $table; + } + else + return null; + } + + /** + * Collects the table column metadata. + * @param CDbTableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $sql="PRAGMA table_info({$table->rawName})"; + $columns=$this->getDbConnection()->createCommand($sql)->queryAll(); + if(empty($columns)) + return false; + + foreach($columns as $column) + { + $c=$this->createColumn($column); + $table->columns[$c->name]=$c; + if($c->isPrimaryKey) + { + if($table->primaryKey===null) + $table->primaryKey=$c->name; + else if(is_string($table->primaryKey)) + $table->primaryKey=array($table->primaryKey,$c->name); + else + $table->primaryKey[]=$c->name; + } + } + if(is_string($table->primaryKey) && !strncasecmp($table->columns[$table->primaryKey]->dbType,'int',3)) + { + $table->sequenceName=''; + $table->columns[$table->primaryKey]->autoIncrement=true; + } + + return true; + } + + /** + * Collects the foreign key column details for the given table. + * @param CDbTableSchema $table the table metadata + */ + protected function findConstraints($table) + { + $foreignKeys=array(); + $sql="PRAGMA foreign_key_list({$table->rawName})"; + $keys=$this->getDbConnection()->createCommand($sql)->queryAll(); + foreach($keys as $key) + { + $column=$table->columns[$key['from']]; + $column->isForeignKey=true; + $foreignKeys[$key['from']]=array($key['table'],$key['to']); + } + $table->foreignKeys=$foreignKeys; + } + + /** + * Creates a table column. + * @param array $column column metadata + * @return CDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new CSqliteColumnSchema; + $c->name=$column['name']; + $c->rawName=$this->quoteColumnName($c->name); + $c->allowNull=!$column['notnull']; + $c->isPrimaryKey=$column['pk']!=0; + $c->isForeignKey=false; + $c->init(strtolower($column['type']),$column['dflt_value']); + return $c; + } + + /** + * Builds a SQL statement for truncating a DB table. + * @param string $table the table to be truncated. The name will be properly quoted by the method. + * @return string the SQL statement for truncating a DB table. + * @since 1.1.6 + */ + public function truncateTable($table) + { + return "DELETE FROM ".$this->quoteTableName($table); + } + + /** + * Builds a SQL statement for dropping a DB column. + * Because SQLite does not support dropping a DB column, calling this method will throw an exception. + * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method. + * @param string $column the name of the column to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a DB column. + * @since 1.1.6 + */ + public function dropColumn($table, $column) + { + throw new CDbException(Yii::t('yii', 'Dropping DB column is not supported by SQLite.')); + } + + /** + * Builds a SQL statement for renaming a column. + * Because SQLite does not support renaming a DB column, calling this method will throw an exception. + * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method. + * @param string $name the old name of the column. The name will be properly quoted by the method. + * @param string $newName the new name of the column. The name will be properly quoted by the method. + * @return string the SQL statement for renaming a DB column. + * @since 1.1.6 + */ + public function renameColumn($table, $name, $newName) + { + throw new CDbException(Yii::t('yii', 'Renaming a DB column is not supported by SQLite.')); + } + + /** + * Builds a SQL statement for adding a foreign key constraint to an existing table. + * Because SQLite does not support adding foreign key to an existing table, calling this method will throw an exception. + * @param string $name the name of the foreign key constraint. + * @param string $table the table that the foreign key constraint will be added to. + * @param string $columns the name of the column to that the constraint will be added on. If there are multiple columns, separate them with commas. + * @param string $refTable the table that the foreign key references to. + * @param string $refColumns the name of the column that the foreign key references to. If there are multiple columns, separate them with commas. + * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL + * @return string the SQL statement for adding a foreign key constraint to an existing table. + * @since 1.1.6 + */ + public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete=null, $update=null) + { + throw new CDbException(Yii::t('yii', 'Adding a foreign key constraint to an existing table is not supported by SQLite.')); + } + + /** + * Builds a SQL statement for dropping a foreign key constraint. + * Because SQLite does not support dropping a foreign key constraint, calling this method will throw an exception. + * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping a foreign key constraint. + * @since 1.1.6 + */ + public function dropForeignKey($name, $table) + { + throw new CDbException(Yii::t('yii', 'Dropping a foreign key constraint is not supported by SQLite.')); + } + + /** + * Builds a SQL statement for changing the definition of a column. + * Because SQLite does not support altering a DB column, calling this method will throw an exception. + * @param string $table the table whose column is to be changed. The table name will be properly quoted by the method. + * @param string $column the name of the column to be changed. The name will be properly quoted by the method. + * @param string $type the new column type. The {@link getColumnType} method will be invoked to convert abstract column type (if any) + * into the physical one. Anything that is not recognized as abstract type will be kept in the generated SQL. + * For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become 'varchar(255) not null'. + * @return string the SQL statement for changing the definition of a column. + * @since 1.1.6 + */ + public function alterColumn($table, $column, $type) + { + throw new CDbException(Yii::t('yii', 'Altering a DB column is not supported by SQLite.')); + } + + /** + * Builds a SQL statement for dropping an index. + * @param string $name the name of the index to be dropped. The name will be properly quoted by the method. + * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method. + * @return string the SQL statement for dropping an index. + * @since 1.1.6 + */ + public function dropIndex($name, $table) + { + return 'DROP INDEX '.$this->quoteTableName($name); + } +} |
