summaryrefslogtreecommitdiff
path: root/framework/Testing/Data/ActiveRecord/TActiveFinder.php
diff options
context:
space:
mode:
Diffstat (limited to 'framework/Testing/Data/ActiveRecord/TActiveFinder.php')
-rw-r--r--framework/Testing/Data/ActiveRecord/TActiveFinder.php1279
1 files changed, 1279 insertions, 0 deletions
diff --git a/framework/Testing/Data/ActiveRecord/TActiveFinder.php b/framework/Testing/Data/ActiveRecord/TActiveFinder.php
new file mode 100644
index 00000000..d1c35bdf
--- /dev/null
+++ b/framework/Testing/Data/ActiveRecord/TActiveFinder.php
@@ -0,0 +1,1279 @@
+<?php
+/**
+ * CActiveRecord class file.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright &copy; 2008-2009 Yii Software LLC
+ * @license http://www.yiiframework.com/license/CBaseActiveRelation*/
+
+/**
+ * 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 1127 2009-06-13 20:26:35Z qiang.xue $
+ * @package System.Testing.Data.ActiveRecord
+ * @since 1.0
+ */
+class TActiveFinder extends TComponent
+{
+ /**
+ * @var boolean join all tables all at once. Defaults to false.
+ * This property is internally used.
+ * @since 1.0.2
+ */
+ public $joinAll=false;
+ /**
+ * @var boolean whether the base model has limit or offset.
+ * This property is internally used.
+ * @since 1.0.2
+ */
+ public $baseLimited=false;
+
+ private $_joinCount=0;
+ private $_joinTree;
+ private $_builder;
+ private $_criteria; // the criteria generated via named scope
+
+ /**
+ * Constructor.
+ * A join tree is built up based on the declared relationships between active record classes.
+ * @param CActiveRecord the model that initiates the active finding process
+ * @param mixed the relation names to be actively looked for
+ * @param CDbCriteria the criteria associated with the named scopes (since version 1.0.5)
+ */
+ public function __construct($model,$with,$criteria=null)
+ {
+ $this->_criteria=$criteria;
+ $this->_builder=$model->getCommandBuilder();
+ $this->_joinTree=new TJoinElement($this,$model);
+ $this->buildJoinTree($this->_joinTree,$with);
+ }
+
+ /**
+ * Uses the most aggressive join approach.
+ * By calling this method, even if there is LIMIT/OFFSET option set for
+ * the primary table query, we will still use a single SQL statement.
+ * By default (without calling this method), the primary table will be queried
+ * by itself so that LIMIT/OFFSET can be correctly applied.
+ * @return CActiveFinder the finder object
+ * @since 1.0.2
+ */
+ public function together()
+ {
+ $this->joinAll=true;
+ return $this;
+ }
+
+ private function query($criteria,$all=false)
+ {
+ if($this->_criteria!==null)
+ {
+ $this->_criteria->mergeWith($criteria);
+ $criteria=$this->_criteria;
+ }
+
+ $this->_joinTree->find($criteria);
+ $this->_joinTree->afterFind();
+
+ if($all)
+ return array_values($this->_joinTree->records);
+ else if(count($this->_joinTree->records))
+ return reset($this->_joinTree->records);
+ else
+ return null;
+ }
+
+ /**
+ * This is the relational version of {@link CActiveRecord::find()}.
+ */
+ public function find($condition='',$params=array())
+ {
+ Prado::trace(get_class($this->_joinTree->model).'.find() eagerly','System.Testing.Data.ActiveRecord.TActiveRecord');
+ $criteria=$this->_builder->createCriteria($condition,$params);
+ return $this->query($criteria);
+ }
+
+ /**
+ * This is the relational version of {@link CActiveRecord::findAll()}.
+ */
+ public function findAll($condition='',$params=array())
+ {
+ Prado::trace(get_class($this->_joinTree->model).'.findAll() eagerly','System.Testing.Data.ActiveRecord.TActiveRecord');
+ $criteria=$this->_builder->createCriteria($condition,$params);
+ return $this->query($criteria,true);
+ }
+
+ /**
+ * This is the relational version of {@link CActiveRecord::findByPk()}.
+ */
+ public function findByPk($pk,$condition='',$params=array())
+ {
+ Prado::trace(get_class($this->_joinTree->model).'.findByPk() eagerly','System.Testing.Data.ActiveRecord.TActiveRecord');
+ $criteria=$this->_builder->createPkCriteria($this->_joinTree->model->getTableSchema(),$pk,$condition,$params);
+ return $this->query($criteria);
+ }
+
+ /**
+ * This is the relational version of {@link CActiveRecord::findAllByPk()}.
+ */
+ public function findAllByPk($pk,$condition='',$params=array())
+ {
+ Prado::trace(get_class($this->_joinTree->model).'.findAllByPk() eagerly','System.Testing.Data.ActiveRecord.TActiveRecord');
+ $criteria=$this->_builder->createPkCriteria($this->_joinTree->model->getTableSchema(),$pk,$condition,$params);
+ return $this->query($criteria,true);
+ }
+
+ /**
+ * This is the relational version of {@link CActiveRecord::findByAttributes()}.
+ */
+ public function findByAttributes($attributes,$condition='',$params=array())
+ {
+ Prado::trace(get_class($this->_joinTree->model).'.findByAttributes() eagerly','System.Testing.Data.ActiveRecord.TActiveRecord');
+ $criteria=$this->_builder->createColumnCriteria($this->_joinTree->model->getTableSchema(),$attributes,$condition,$params);
+ return $this->query($criteria);
+ }
+
+ /**
+ * This is the relational version of {@link CActiveRecord::findAllByAttributes()}.
+ */
+ public function findAllByAttributes($attributes,$condition='',$params=array())
+ {
+ Prado::trace(get_class($this->_joinTree->model).'.findAllByAttributes() eagerly','System.Testing.Data.ActiveRecord.TActiveRecord');
+ $criteria=$this->_builder->createColumnCriteria($this->_joinTree->model->getTableSchema(),$attributes,$condition,$params);
+ return $this->query($criteria,true);
+ }
+
+ /**
+ * This is the relational version of {@link CActiveRecord::findBySql()}.
+ */
+ public function findBySql($sql,$params=array())
+ {
+ Prado::trace(get_class($this->_joinTree->model).'.findBySql() eagerly','System.Testing.Data.ActiveRecord.TActiveRecord');
+ if(($row=$this->_builder->createSqlCommand($sql,$params)->queryRow())!==false)
+ {
+ $baseRecord=$this->_joinTree->model->populateRecord($row,false);
+ $this->_joinTree->findWithBase($baseRecord);
+ $this->_joinTree->afterFind();
+ return $baseRecord;
+ }
+ }
+
+ /**
+ * This is the relational version of {@link CActiveRecord::findAllBySql()}.
+ */
+ public function findAllBySql($sql,$params=array())
+ {
+ Prado::trace(get_class($this->_joinTree->model).'.findAllBySql() eagerly','System.Testing.Data.ActiveRecord.TActiveRecord');
+ if(($rows=$this->_builder->createSqlCommand($sql,$params)->queryAll())!==array())
+ {
+ $baseRecords=$this->_joinTree->model->populateRecords($rows,false);
+ $this->_joinTree->findWithBase($baseRecords);
+ $this->_joinTree->afterFind();
+ return $baseRecords;
+ }
+ else
+ return array();
+ }
+
+ /**
+ * This is the relational version of {@link CActiveRecord::count()}.
+ * @since 1.0.3
+ */
+ public function count($condition='',$params=array())
+ {
+ Prado::trace(get_class($this->_joinTree->model).'.count() eagerly','System.Testing.Data.ActiveRecord.TActiveRecord');
+ $criteria=$this->_builder->createCriteria($condition,$params);
+ if($this->_criteria!==null)
+ {
+ $this->_criteria->mergeWith($criteria);
+ $criteria=$this->_criteria;
+ }
+ return $this->_joinTree->count($criteria);
+ }
+
+ /**
+ * Finds the related objects for the specified active record.
+ * This method is internally invoked by {@link CActiveRecord} to support lazy loading.
+ * @param CActiveRecord 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();
+ }
+ }
+
+ /**
+ * Builds up the join tree representing the relationships involved in this query.
+ * @param CJoinElement the parent tree node
+ * @param mixed the names of the related objects relative to the parent tree node
+ * @param array additional query options to be merged with the relation
+ */
+ private function buildJoinTree($parent,$with,$options=null)
+ {
+ if($parent instanceof CStatElement)
+ throw new TDbException('The STAT relation "'.($parent->relation->name).'" cannot have child relations.');
+
+ if(is_string($with))
+ {
+ if(($pos=strrpos($with,'.'))!==false)
+ {
+ $parent=$this->buildJoinTree($parent,substr($with,0,$pos));
+ $with=substr($with,$pos+1);
+ }
+
+ // named scope
+ if(($pos=strpos($with,':'))!==false)
+ {
+ $scopes=explode(':',substr($with,$pos+1));
+ $with=substr($with,0,$pos);
+ }
+
+ if(isset($parent->children[$with]))
+ return $parent->children[$with];
+
+ if(($relation=$parent->model->getActiveRelation($with))===null)
+ throw new TDbException('Relation "'.$with.'" is not defined in active record class "'.get_class($parent->model).'".');
+
+ $relation=clone $relation;
+ $model=TActiveRecord::model($relation->className);
+ if(($scope=$model->defaultScope())!==array())
+ $relation->mergeWith($scope);
+ if(isset($scopes) && !empty($scopes))
+ {
+ $scs=$model->scopes();
+ foreach($scopes as $scope)
+ {
+ if(isset($scs[$scope]))
+ $relation->mergeWith($scs[$scope]);
+ else
+ throw new TDbException('Active record class "'.get_class($model).'" does not have a scope named "'.$scope.'".');
+ }
+ }
+
+ // dynamic options
+ if($options!==null)
+ $relation->mergeWith($options);
+
+ if($relation instanceof TStatRelation)
+ return new TStatElement($this,$relation,$parent);
+ else
+ {
+ $element=$parent->children[$with]=new TJoinElement($this,$relation,$parent,++$this->_joinCount);
+ 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))
+ $element=$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 1127 2009-06-13 20:26:35Z qiang.xue $
+ * @package System.Testing.Data.ActiveRecord
+ * @since 1.0
+ */
+class TJoinElement
+{
+ /**
+ * @var integer the unique ID of this tree node
+ */
+ public $id;
+ /**
+ * @var CActiveRelation the relation represented by this tree node
+ */
+ public $relation;
+ /**
+ * @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
+ * @since 1.0.4
+ */
+ public $stats=array();
+ /**
+ * @var string table alias for this join element
+ */
+ public $tableAlias;
+
+ 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 the finder
+ * @param mixed 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 the parent tree node
+ * @param integer 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->_builder=$parent->_builder;
+ $this->tableAlias=$relation->alias===null?$relation->name:$relation->alias;
+ $this->model=TActiveRecord::model($relation->className);
+ $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();
+ }
+
+ // set up column aliases, such as t1_c2
+ $table=$this->_table;
+ $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;
+ }
+ }
+
+ /**
+ * Performs the recursive finding with the criteria.
+ * @param CDbCriteria the query criteria
+ */
+ public function find($criteria=null)
+ {
+ if($this->_parent===null) // root element
+ {
+ $query=new TJoinQuery($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 TJoinQuery($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 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();
+
+ if(empty($this->children))
+ return;
+
+ $child=reset($this->children);
+ $query=new TJoinQuery($this);
+ $this->_joined=true;
+ $child->_joined=true;
+ $query->join($child);
+ if($child->relation instanceof THasManyRelation)
+ {
+ $query->limit=$child->relation->limit;
+ $query->offset=$child->relation->offset;
+ $this->_finder->baseLimited=($query->offset>=0 || $query->limit>=0);
+ $query->groups[]=$child->relation->group;
+ $query->havings[]=$child->relation->having;
+ }
+ $child->buildQuery($query);
+ $this->_finder->baseLimited=false;
+ $this->runQuery($query);
+ foreach($child->children as $c)
+ $c->find();
+ }
+
+ /**
+ * Performs the eager loading with the base records ready.
+ * @param mixed 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 TJoinQuery($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 the query criteria
+ * @return integer number of primary records.
+ * @since 1.0.3
+ */
+ public function count($criteria=null)
+ {
+ $query=new TJoinQuery($this,$criteria);
+ // ensure only one big join statement is used
+ $this->_finder->baseLimited=false;
+ $this->_finder->joinAll=true;
+ $this->buildQuery($query);
+
+ if(is_string($this->_table->primaryKey))
+ {
+ $prefix=$this->getColumnPrefix();
+ $schema=$this->_builder->getSchema();
+ $column=$prefix.$schema->quoteColumnName($this->_table->primaryKey);
+ }
+ else if($criteria->select!=='*')
+ $column=$criteria->select;
+ else
+ throw new TDbException('Unable to count records with composite primary keys. Please explicitly specify the SELECT option in the query criteria.');
+
+ $query->selects=array("COUNT(DISTINCT $column)");
+ $query->orders=$query->groups=$query->havings=array();
+ $command=$query->createCommand($this->_builder);
+ return $command->queryScalar();
+ }
+
+ /**
+ * Calls {@link CActiveRecord::afterFind} of all the records.
+ * @since 1.0.3
+ */
+ public function afterFind()
+ {
+ foreach($this->records as $record)
+ $record->afterFindInternal();
+ foreach($this->children as $child)
+ $child->afterFind();
+ }
+
+ /**
+ * Builds the join query with all descendant HAS_ONE and BELONGS_TO nodes.
+ * @param CJoinQuery the query being built up
+ */
+ public function buildQuery($query)
+ {
+ foreach($this->children as $child)
+ {
+ if($child->relation instanceof THasOneRelation || $child->relation instanceof TBelongsToRelation
+ || $this->_finder->joinAll || !$this->_finder->baseLimited && $child->relation->together)
+ {
+ $child->_joined=true;
+ $query->join($child);
+ $child->buildQuery($query);
+ }
+ }
+ }
+
+ /**
+ * Executes the join query and populates the query results.
+ * @param CJoinQuery 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 the query executed
+ * @param array 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)
+ $record->addRelatedRecord($child->relation->name,null,$child->relation instanceof THasManyRelation);
+ $this->records[$pk]=$record;
+ }
+
+ // populate child records recursively
+ foreach($this->children as $child)
+ {
+ if(!isset($query->elements[$child->id]))
+ continue;
+ $childRecord=$child->populateRecord($query,$row);
+ if($child->relation instanceof THasOneRelation || $child->relation instanceof TBelongsToRelation)
+ $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 TActiveRecord)
+ $fpk=serialize($childRecord->getPrimaryKey());
+ else
+ $fpk=0;
+ if(!isset($this->_related[$pk][$child->relation->name][$fpk]))
+ {
+ $record->addRelatedRecord($child->relation->name,$childRecord,true);
+ $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->tableAlias;
+ 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 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;
+ 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+)$/i',$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 TDbException('Active record "'.get_class($this->model).'" is trying to select an invalid column "'.$name.'". Note, the column must exist in the table or be an expression with alias.');
+ }
+ // 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->tableAlias.'.';
+ else
+ return $this->_table->rawName.'.';
+ }
+
+ /**
+ * @return string the join statement (this node joins with its parent)
+ */
+ public function getJoinCondition()
+ {
+ $parent=$this->_parent;
+ $relation=$this->relation;
+ if($this->relation instanceof TManyManyRelation)
+ {
+ if(!preg_match('/^\s*(.*?)\((.*)\)\s*$/',$this->relation->foreignKey,$matches))
+ throw new TDbException('The relation "'.($this->relation->name).'" in active record class "'.get_class($parent->model).'" is specified with an invalid foreign key. The format of the foreign key must be "joinTable(fk1,fk2,...)".');
+
+ $schema=$this->_builder->getSchema();
+ if(($joinTable=$schema->getTable($matches[1]))===null)
+ throw new TDbException('The relation "'.($this->relation->name).'" in active record class "'.get_class($parent->model).'" is not specified correctly: the join table "'.($matches[1]).'" given in the foreign key cannot be found in the database.');
+
+ $fks=preg_split('/[\s,]+/',$matches[2],-1,PREG_SPLIT_NO_EMPTY);
+
+ return $this->joinManyMany($joinTable,$fks,$parent);
+ }
+ else
+ {
+ $fks=preg_split('/[\s,]+/',$relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY);
+ if($this->relation instanceof TBelongsToRelation)
+ {
+ $pke=$this;
+ $fke=$parent;
+ }
+ else
+ {
+ $pke=$parent;
+ $fke=$this;
+ }
+ 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 the join element containing foreign keys
+ * @param array the foreign keys
+ * @param CJoinElement the join element containg primary keys
+ * @param CJoinElement 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();
+ foreach($fks as $i=>$fk)
+ {
+ if(!isset($fke->_table->columns[$fk]))
+ throw new TDbException('The relation "'.($this->relation->name).'" in active record class "'.get_class($parent->model).'" is specified with an invalid foreign key "'.$fk.'". There is no such column in the table "'.($fke->_table->name).'".');
+
+ if(isset($fke->_table->foreignKeys[$fk]))
+ $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 the join table
+ * @param array the foreign keys
+ * @param CJoinElement 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=$this->relation->name.'_'.$this->tableAlias;
+ $parentCondition=array();
+ $childCondition=array();
+ foreach($fks as $i=>$fk)
+ {
+ if(!isset($joinTable->columns[$fk]))
+ throw new TDbException('The relation "'.($this->relation->name).'" in active record class "'.get_class($parent->model).'" is specified with an invalid foreign key "'.$fk.'". There is no such column in the 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
+ throw new TDbException('The relation "'.($this->relation->name).'" in active record class "'.get_class($parent->model).'" is specified with an invalid foreign key "'.$fk.'". The foreign key does not point to either joining table.');
+ }
+ else // FK constraints not defined
+ {
+ 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 TDbException('The relation "'.($this->relation->name).'" in active record class "'.get_class($parent->model).'" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.');
+ }
+}
+
+
+/**
+ * CJoinQuery represents a JOIN SQL statement.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @version $Id: CActiveFinder.php 1127 2009-06-13 20:26:35Z qiang.xue $
+ * @package System.Testing.Data.ActiveRecord
+ * @since 1.0
+ */
+class TJoinQuery
+{
+ /**
+ * @var array list of column selections
+ */
+ public $selects=array();
+ /**
+ * @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 The root join tree.
+ * @param CDbCriteria 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;
+ }
+ 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 the element to be joined
+ */
+ public function join($element)
+ {
+ $this->selects[]=$element->getColumnSelect($element->relation->select);
+ $this->conditions[]=$element->relation->condition;
+ $this->orders[]=$element->relation->order;
+ $this->joins[]=$element->getJoinCondition();
+ $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 the command builder
+ * @return string the SQL statement
+ */
+ public function createCommand($builder)
+ {
+ $sql='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 1127 2009-06-13 20:26:35Z qiang.xue $
+ * @package System.Testing.Data.ActiveRecord
+ * @since 1.0.4
+ */
+class TStatElement
+{
+ /**
+ * @var CActiveRelation the relation represented by this tree node
+ */
+ public $relation;
+
+ private $_finder;
+ private $_parent;
+
+ /**
+ * Constructor.
+ * @param CActiveFinder the finder
+ * @param CStatRelation the STAT relation
+ * @param CJoinElement 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=TActiveRecord::model($relation->className);
+ $builder=$model->getCommandBuilder();
+ $schema=$builder->getSchema();
+ $table=$model->getTableSchema();
+ $pkTable=$this->_parent->model->getTableSchema();
+
+ $fks=preg_split('/[\s,]+/',$relation->foreignKey,-1,PREG_SPLIT_NO_EMPTY);
+ if(count($fks)!==count($pkTable->primaryKey))
+ throw new TDbException('The relation "'.($relation->name).'" in active record class "'.get_class($parent->model).'" is specified with an invalid foreign key. The columns in the key must match the primary keys of the 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 TDbException('The relation "'.($relation->name).'" in active record class "'.get_class($parent->model).'" is specified with an invalid foreign key "'.$fk.'". There is no such column in the 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 TDbException('The relation "'.($relation->name).'" in active record class "'.get_class($parent->model).'" is specified with a foreign key "'.$fk.'" that does not point to the parent table "'.($pkTable->name).'".');
+ }
+ else // FK constraints undefined
+ {
+ if(is_array($table->primaryKey)) // composite PK
+ $map[$table->primaryKey[$i]]=$fk;
+ }
+ }
+
+ $records=$this->_parent->records;
+
+ $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;
+
+ $c=$schema->quoteColumnName('c');
+ $s=$schema->quoteColumnName('s');
+
+ // 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}"
+ .$where
+ ." GROUP BY $col".$group
+ ." HAVING ".$builder->createInCondition($table,$fks[0],array_keys($records))
+ .$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}"
+ .$where
+ .' GROUP BY '.implode(', ',array_keys($cols)).$group
+ .' HAVING '.$builder->createInCondition($table,$fks,$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)
+ {
+ $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);
+ }
+
+ private function queryManyMany($joinTableName,$keys)
+ {
+ $relation=$this->relation;
+ $model=TActiveRecord::model($relation->className);
+ $table=$model->getTableSchema();
+ $builder=$model->getCommandBuilder();
+ $schema=$builder->getSchema();
+ $pkTable=$this->_parent->model->getTableSchema();
+
+ if(($joinTable=$builder->getSchema()->getTable($joinTableName))===null)
+ throw new TDbException('The relation "'.($relation->name).'" in active record class "'.get_class($this->_parent->model).'" is not specified correctly. The join table "'.$joinTableName.'" given in the foreign key cannot be found in the database.');
+
+ $fks=preg_split('/[\s,]+/',$keys,-1,PREG_SPLIT_NO_EMPTY);
+ if(count($fks)!==count($table->primaryKey)+count($pkTable->primaryKey))
+ throw new TDbException('The relation "'.($relation->name).'" in active record class "'.get_class($this->_parent->model).'" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.');
+
+ $joinCondition=array();
+ $map=array();
+ foreach($fks as $i=>$fk)
+ {
+ if(!isset($joinTable->columns[$fk]))
+ throw new TDbException('The relation "'.($relation->name).'" in active record class "'.get_class($this->_parent->model).'" is specified with an invalid foreign key "'.$fk.'". There is no such column in the 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]=$table->rawName.'.'.$schema->quoteColumnName($pk).'='.$joinTable->rawName.'.'.$schema->quoteColumnName($fk);
+ else if(!isset($map[$pk]) && $schema->compareTableNames($pkTable->rawName,$tableName))
+ $map[$pk]=$fk;
+ else
+ throw new TDbException('The relation "'.($relation->name).'" in active record class "'.get_class($this->_parent->model).'" is specified with an invalid foreign key "'.$fk.'". The foreign key does not point to either joining table.');
+ }
+ else // FK constraints not defined
+ {
+ 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]=$table->rawName.'.'.$schema->quoteColumnName($pk).'='.$joinTable->rawName.'.'.$schema->quoteColumnName($fk);
+ }
+ }
+ }
+
+ if($joinCondition===array() || $map===array())
+ throw new TDbException('The relation "'.($relation->name).'" in active record class "'.get_class($this->_parent->model).'" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.');
+
+ $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];
+ }
+ }
+
+ $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.' INNER JOIN '.$joinTable->rawName
+ .' ON ('.implode(') AND (',$joinCondition).')'
+ .$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);
+ }
+}