From f1c27f46100582a1e52a27b616bf468e849068e7 Mon Sep 17 00:00:00 2001 From: rojaro <> Date: Wed, 15 Jul 2009 07:58:26 +0000 Subject: moved System.Db to System.Testing.Data and updated classes accordingly --- .../DEPRECATED_COMPATIBILITY_REASONS_ONLY | 3 - .../Relations/TActiveRecordRelationContext.php | 230 --- framework/Db/ActiveRecord/TActiveFinder.php | 1279 -------------- framework/Db/ActiveRecord/TActiveRecord.php | 1843 -------------------- .../Db/ActiveRecord/TActiveRecordBehavior.php | 94 - .../Db/ActiveRecord/TActiveRecordCriteria.php | 254 --- framework/Db/Schema/TDbColumnSchema.php | 145 -- framework/Db/Schema/TDbCommandBuilder.php | 656 ------- framework/Db/Schema/TDbCriteria.php | 166 -- framework/Db/Schema/TDbExpression.php | 49 - framework/Db/Schema/TDbSchema.php | 193 -- framework/Db/Schema/TDbTableSchema.php | 76 - framework/Db/Schema/mssql/TMssqlColumnSchema.php | 57 - framework/Db/Schema/mssql/TMssqlCommandBuilder.php | 303 ---- framework/Db/Schema/mssql/TMssqlPdoAdapter.php | 74 - framework/Db/Schema/mssql/TMssqlSchema.php | 312 ---- framework/Db/Schema/mssql/TMssqlTableSchema.php | 35 - framework/Db/Schema/mysql/TMysqlColumnSchema.php | 46 - framework/Db/Schema/mysql/TMysqlSchema.php | 205 --- framework/Db/Schema/mysql/TMysqlTableSchema.php | 26 - framework/Db/Schema/oci/TOciColumnSchema.php | 56 - framework/Db/Schema/oci/TOciCommandBuilder.php | 122 -- framework/Db/Schema/oci/TOciSchema.php | 278 --- framework/Db/Schema/oci/TOciTableSchema.php | 28 - framework/Db/Schema/pgsql/TPgsqlColumnSchema.php | 58 - framework/Db/Schema/pgsql/TPgsqlSchema.php | 284 --- framework/Db/Schema/pgsql/TPgsqlTableSchema.php | 27 - framework/Db/Schema/sqlite/TSqliteColumnSchema.php | 35 - .../Db/Schema/sqlite/TSqliteCommandBuilder.php | 43 - framework/Db/Schema/sqlite/TSqliteSchema.php | 134 -- framework/Db/TDataSourceConfig.php | 168 -- framework/Db/TDbCommand.php | 340 ---- framework/Db/TDbConnection.php | 759 -------- framework/Db/TDbDataReader.php | 221 --- framework/Db/TDbTransaction.php | 111 -- .../DEPRECATED_COMPATIBILITY_REASONS_ONLY | 3 + .../Relations/TActiveRecordRelationContext.php | 230 +++ .../Testing/Data/ActiveRecord/TActiveFinder.php | 1279 ++++++++++++++ .../Testing/Data/ActiveRecord/TActiveRecord.php | 1843 ++++++++++++++++++++ .../Data/ActiveRecord/TActiveRecordBehavior.php | 94 + .../Data/ActiveRecord/TActiveRecordCriteria.php | 254 +++ framework/Testing/Data/Schema/TDbColumnSchema.php | 145 ++ .../Testing/Data/Schema/TDbCommandBuilder.php | 656 +++++++ framework/Testing/Data/Schema/TDbCriteria.php | 166 ++ framework/Testing/Data/Schema/TDbExpression.php | 49 + framework/Testing/Data/Schema/TDbSchema.php | 193 ++ framework/Testing/Data/Schema/TDbTableSchema.php | 76 + .../Data/Schema/mssql/TMssqlColumnSchema.php | 57 + .../Data/Schema/mssql/TMssqlCommandBuilder.php | 303 ++++ .../Testing/Data/Schema/mssql/TMssqlPdoAdapter.php | 74 + .../Testing/Data/Schema/mssql/TMssqlSchema.php | 312 ++++ .../Data/Schema/mssql/TMssqlTableSchema.php | 35 + .../Data/Schema/mysql/TMysqlColumnSchema.php | 46 + .../Testing/Data/Schema/mysql/TMysqlSchema.php | 205 +++ .../Data/Schema/mysql/TMysqlTableSchema.php | 26 + .../Testing/Data/Schema/oci/TOciColumnSchema.php | 56 + .../Testing/Data/Schema/oci/TOciCommandBuilder.php | 122 ++ framework/Testing/Data/Schema/oci/TOciSchema.php | 278 +++ .../Testing/Data/Schema/oci/TOciTableSchema.php | 28 + .../Data/Schema/pgsql/TPgsqlColumnSchema.php | 58 + .../Testing/Data/Schema/pgsql/TPgsqlSchema.php | 284 +++ .../Data/Schema/pgsql/TPgsqlTableSchema.php | 27 + .../Data/Schema/sqlite/TSqliteColumnSchema.php | 35 + .../Data/Schema/sqlite/TSqliteCommandBuilder.php | 43 + .../Testing/Data/Schema/sqlite/TSqliteSchema.php | 134 ++ framework/Testing/Data/TDataSourceConfig.php | 168 ++ framework/Testing/Data/TDbCommand.php | 344 ++++ framework/Testing/Data/TDbConnection.php | 759 ++++++++ framework/Testing/Data/TDbDataReader.php | 221 +++ framework/Testing/Data/TDbTransaction.php | 111 ++ 70 files changed, 8714 insertions(+), 8710 deletions(-) delete mode 100644 framework/Db/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY delete mode 100644 framework/Db/ActiveRecord/Relations/TActiveRecordRelationContext.php delete mode 100644 framework/Db/ActiveRecord/TActiveFinder.php delete mode 100644 framework/Db/ActiveRecord/TActiveRecord.php delete mode 100644 framework/Db/ActiveRecord/TActiveRecordBehavior.php delete mode 100644 framework/Db/ActiveRecord/TActiveRecordCriteria.php delete mode 100755 framework/Db/Schema/TDbColumnSchema.php delete mode 100755 framework/Db/Schema/TDbCommandBuilder.php delete mode 100755 framework/Db/Schema/TDbCriteria.php delete mode 100755 framework/Db/Schema/TDbExpression.php delete mode 100755 framework/Db/Schema/TDbSchema.php delete mode 100755 framework/Db/Schema/TDbTableSchema.php delete mode 100755 framework/Db/Schema/mssql/TMssqlColumnSchema.php delete mode 100755 framework/Db/Schema/mssql/TMssqlCommandBuilder.php delete mode 100755 framework/Db/Schema/mssql/TMssqlPdoAdapter.php delete mode 100755 framework/Db/Schema/mssql/TMssqlSchema.php delete mode 100755 framework/Db/Schema/mssql/TMssqlTableSchema.php delete mode 100755 framework/Db/Schema/mysql/TMysqlColumnSchema.php delete mode 100755 framework/Db/Schema/mysql/TMysqlSchema.php delete mode 100755 framework/Db/Schema/mysql/TMysqlTableSchema.php delete mode 100755 framework/Db/Schema/oci/TOciColumnSchema.php delete mode 100755 framework/Db/Schema/oci/TOciCommandBuilder.php delete mode 100755 framework/Db/Schema/oci/TOciSchema.php delete mode 100755 framework/Db/Schema/oci/TOciTableSchema.php delete mode 100755 framework/Db/Schema/pgsql/TPgsqlColumnSchema.php delete mode 100755 framework/Db/Schema/pgsql/TPgsqlSchema.php delete mode 100755 framework/Db/Schema/pgsql/TPgsqlTableSchema.php delete mode 100755 framework/Db/Schema/sqlite/TSqliteColumnSchema.php delete mode 100755 framework/Db/Schema/sqlite/TSqliteCommandBuilder.php delete mode 100755 framework/Db/Schema/sqlite/TSqliteSchema.php delete mode 100755 framework/Db/TDataSourceConfig.php delete mode 100755 framework/Db/TDbCommand.php delete mode 100755 framework/Db/TDbConnection.php delete mode 100755 framework/Db/TDbDataReader.php delete mode 100755 framework/Db/TDbTransaction.php create mode 100644 framework/Testing/Data/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY create mode 100644 framework/Testing/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php create mode 100644 framework/Testing/Data/ActiveRecord/TActiveFinder.php create mode 100644 framework/Testing/Data/ActiveRecord/TActiveRecord.php create mode 100644 framework/Testing/Data/ActiveRecord/TActiveRecordBehavior.php create mode 100644 framework/Testing/Data/ActiveRecord/TActiveRecordCriteria.php create mode 100755 framework/Testing/Data/Schema/TDbColumnSchema.php create mode 100755 framework/Testing/Data/Schema/TDbCommandBuilder.php create mode 100755 framework/Testing/Data/Schema/TDbCriteria.php create mode 100755 framework/Testing/Data/Schema/TDbExpression.php create mode 100755 framework/Testing/Data/Schema/TDbSchema.php create mode 100755 framework/Testing/Data/Schema/TDbTableSchema.php create mode 100755 framework/Testing/Data/Schema/mssql/TMssqlColumnSchema.php create mode 100755 framework/Testing/Data/Schema/mssql/TMssqlCommandBuilder.php create mode 100755 framework/Testing/Data/Schema/mssql/TMssqlPdoAdapter.php create mode 100755 framework/Testing/Data/Schema/mssql/TMssqlSchema.php create mode 100755 framework/Testing/Data/Schema/mssql/TMssqlTableSchema.php create mode 100755 framework/Testing/Data/Schema/mysql/TMysqlColumnSchema.php create mode 100755 framework/Testing/Data/Schema/mysql/TMysqlSchema.php create mode 100755 framework/Testing/Data/Schema/mysql/TMysqlTableSchema.php create mode 100755 framework/Testing/Data/Schema/oci/TOciColumnSchema.php create mode 100755 framework/Testing/Data/Schema/oci/TOciCommandBuilder.php create mode 100755 framework/Testing/Data/Schema/oci/TOciSchema.php create mode 100755 framework/Testing/Data/Schema/oci/TOciTableSchema.php create mode 100755 framework/Testing/Data/Schema/pgsql/TPgsqlColumnSchema.php create mode 100755 framework/Testing/Data/Schema/pgsql/TPgsqlSchema.php create mode 100755 framework/Testing/Data/Schema/pgsql/TPgsqlTableSchema.php create mode 100755 framework/Testing/Data/Schema/sqlite/TSqliteColumnSchema.php create mode 100755 framework/Testing/Data/Schema/sqlite/TSqliteCommandBuilder.php create mode 100755 framework/Testing/Data/Schema/sqlite/TSqliteSchema.php create mode 100755 framework/Testing/Data/TDataSourceConfig.php create mode 100755 framework/Testing/Data/TDbCommand.php create mode 100755 framework/Testing/Data/TDbConnection.php create mode 100755 framework/Testing/Data/TDbDataReader.php create mode 100755 framework/Testing/Data/TDbTransaction.php (limited to 'framework') diff --git a/framework/Db/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY b/framework/Db/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY deleted file mode 100644 index 8c7a4073..00000000 --- a/framework/Db/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY +++ /dev/null @@ -1,3 +0,0 @@ -Please note that this file is used for old Prado ActiveRecord applications which use $record->withAnotherRecord - syntax. - -You shouln't use that syntax anymore as it's deprecated in this portation of Yii ActiveRecords. diff --git a/framework/Db/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Db/ActiveRecord/Relations/TActiveRecordRelationContext.php deleted file mode 100644 index 696bb5b1..00000000 --- a/framework/Db/ActiveRecord/Relations/TActiveRecordRelationContext.php +++ /dev/null @@ -1,230 +0,0 @@ - - * @link http://www.pradosoft.com/ - * @copyright Copyright © 2005-2008 PradoSoft - * @license http://www.pradosoft.com/license/ - * @version $Id$ - * @package System.Data.ActiveRecord.Relations - */ - -/** - * TActiveRecordRelationContext holds information regarding record relationships - * such as record relation property name, query criteria and foreign object record - * class names. - * - * This class is use internally by passing a context to the TActiveRecordRelation - * constructor. - * - * @author Wei Zhuo - * @version $Id$ - * @package System.Data.ActiveRecord.Relations - * @since 3.1 - */ -class TActiveRecordRelationContext -{ - private $_property; - private $_record; - private $_relation; //data from an entry of TActiveRecord::$RELATION - private $_fkeys; - - public function __construct($record, $property=null, $relation=null) - { - $this->_record=$record; - $this->_property=$property; - $this->_relation=$relation; - } - - /** - * @return boolean true if the relation is defined in TActiveRecord::$RELATIONS - * @since 3.1.2 - */ - public function hasRecordRelation() - { - return $this->_relation!==null; - } - - public function getPropertyValue() - { - $obj = $this->getSourceRecord(); - return $obj->getColumnValue($this->getProperty()); - } - - /** - * @return string name of the record property that the relationship results will be assigned to. - */ - public function getProperty() - { - return $this->_property; - } - - /** - * @return TActiveRecord the active record instance that queried for its related records. - */ - public function getSourceRecord() - { - return $this->_record; - } - - /** - * @return array foreign key of this relations, the keys is dependent on the - * relationship type. - * @since 3.1.2 - */ - public function getRelationForeignKeys() - { - if($this->_fkeys===null) - $this->_fkeys=$this->getRelationHandler()->getRelationForeignKeys(); - return $this->_fkeys; - } - - /** - * @return string HAS_MANY, HAS_ONE, or BELONGS_TO - */ - public function getRelationType() - { - return $this->_relation[0]; - } - - /** - * @return string foreign record class name. - */ - public function getForeignRecordClass() - { - return $this->_relation[1]; - } - - /** - * @return string foreign key field names, comma delimited. - * @since 3.1.2 - */ - public function getFkField() - { - return $this->_relation[2]; - } - - /** - * @return string the query condition for the relation as specified in RELATIONS - * @since 3.1.2 - */ - public function getCondition() - { - return isset($this->_relation[3])?$this->_relation[3]:null; - } - - /** - * @return array the query parameters for the relation as specified in RELATIONS - * @since 3.1.2 - */ - public function getParameters() - { - return isset($this->_relation[4])?$this->_relation[4]:array(); - } - - /** - * @return boolean true if the 3rd element of an TActiveRecord::$RELATION entry is set. - * @since 3.1.2 - */ - public function hasFkField() - { - $notManyToMany = $this->getRelationType() !== TActiveRecord::MANY_TO_MANY; - return $notManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]); - } - - /** - * @return string the M-N relationship association table name. - */ - public function getAssociationTable() - { - return $this->_relation[2]; - } - - /** - * @return boolean true if the relationship is HAS_MANY and requires an association table. - */ - public function hasAssociationTable() - { - $isManyToMany = $this->getRelationType() === TActiveRecord::MANY_TO_MANY; - return $isManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]); - } - - /** - * @return TActiveRecord corresponding relationship foreign object finder instance. - */ - public function getForeignRecordFinder() - { - return TActiveRecord::finder($this->getForeignRecordClass()); - } - - /** - * Creates and return the TActiveRecordRelation handler for specific relationships. - * An instance of TActiveRecordHasOne, TActiveRecordBelongsTo, TActiveRecordHasMany, - * or TActiveRecordHasManyAssocation will be returned. - * @param TActiveRecordCriteria search criteria - * @return TActiveRecordRelation record relationship handler instnace. - * @throws TActiveRecordException if property is not defined or missing. - */ - public function getRelationHandler($criteria=null) - { - if(!$this->hasRecordRelation()) - { - throw new TActiveRecordException('ar_undefined_relation_prop', - $this->_property, get_class($this->_record), 'RELATIONS'); - } - if($criteria===null) - $criteria = new TActiveRecordCriteria($this->getCondition(), $this->getParameters()); - switch($this->getRelationType()) - { - case TActiveRecord::HAS_MANY: - Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasMany'); - return new TActiveRecordHasMany($this, $criteria); - case TActiveRecord::MANY_TO_MANY: - Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasManyAssociation'); - return new TActiveRecordHasManyAssociation($this, $criteria); - case TActiveRecord::HAS_ONE: - Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasOne'); - return new TActiveRecordHasOne($this, $criteria); - case TActiveRecord::BELONGS_TO: - Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordBelongsTo'); - return new TActiveRecordBelongsTo($this, $criteria); - default: - throw new TActiveRecordException('ar_invalid_relationship'); - } - } - - /** - * @return TActiveRecordRelationCommand - */ - public function updateAssociatedRecords($updateBelongsTo=false) - { - $success=true; - foreach($this->_record->getRecordRelations() as $data) - { - list($property, $relation) = $data; - $belongsTo = $relation[0]==TActiveRecord::BELONGS_TO; - if(($updateBelongsTo && $belongsTo) || (!$updateBelongsTo && !$belongsTo)) - { - $obj = $this->getSourceRecord(); - if(!$this->isEmptyFkObject($obj->getColumnValue($property))) - { - $context = new TActiveRecordRelationContext($this->getSourceRecord(),$property,$relation); - $success = $context->getRelationHandler()->updateAssociatedRecords() && $success; - } - } - } - return $success; - } - - protected function isEmptyFkObject($obj) - { - if(is_object($obj)) - return $obj instanceof TList ? $obj->count() === 0 : false; - else if(is_array($obj)) - return count($obj)===0; - else - return empty($obj); - } -} - diff --git a/framework/Db/ActiveRecord/TActiveFinder.php b/framework/Db/ActiveRecord/TActiveFinder.php deleted file mode 100644 index d55074c0..00000000 --- a/framework/Db/ActiveRecord/TActiveFinder.php +++ /dev/null @@ -1,1279 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 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 - * @version $Id: CActiveFinder.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.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.Db.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.Db.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.Db.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.Db.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.Db.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.Db.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.Db.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.Db.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.Db.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 - * @version $Id: CActiveFinder.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.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_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 - * @version $Id: CActiveFinder.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.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 - * @version $Id: CActiveFinder.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.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($iprimaryKey)) - { - $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); - } -} diff --git a/framework/Db/ActiveRecord/TActiveRecord.php b/framework/Db/ActiveRecord/TActiveRecord.php deleted file mode 100644 index c0ffc32e..00000000 --- a/framework/Db/ActiveRecord/TActiveRecord.php +++ /dev/null @@ -1,1843 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 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. - * - * @author Qiang Xue - * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0 - */ - -Prado::using('System.Base.TModel'); -Prado::using('System.Db.*'); -Prado::using('System.Db.ActiveRecord.*'); -Prado::using('System.Base.TEvent'); -Prado::using('System.Db.ActiveRecord.TActiveRecordCriteria'); - -abstract class TActiveRecord extends TModel -{ - const BELONGS_TO='TBelongsToRelation'; - const HAS_ONE='THasOneRelation'; - const HAS_MANY='THasManyRelation'; - const MANY_MANY='TManyManyRelation'; - const STAT='TStatRelation'; - - /** - * @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) - - /** - * Returns the instance of a active record finder for a particular class. - * The finder objects are static instances for each ActiveRecord class. - * This means that event handlers bound to these finder instances are class wide. - * Create a new instance of the ActiveRecord class if you wish to bound the - * event handlers to object instance. - * @param string active record class name. - * @return TActiveRecord active record finder instance. - */ - - /** - * Constructor. - * @param string 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->attachBehaviors($this->behaviors()); - $this->afterConstruct(); - } - - /** - * PHP sleep magic method. - * This method ensures that the model meta data reference is set to null. - */ - 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 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 property name - * @param mixed property value - */ - public function __set($name,$value) - { - if($this->setAttribute($name,$value)===false) - 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 the property name or the event name - * @return boolean whether the property value is null - * @since 1.0.1 - */ - 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 the property name or the event name - * @since 1.0.1 - */ - 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 the method name - * @param array method parameters - * @return mixed the method return value - * @since 1.0.5 - * - * @TODO: implement __call to parent - * - */ - 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 the relation name (see {@link relations}) - * @param boolean whether to reload the related objects from database. Defaults to false. - * @param array additional parameters that customize the query conditions as specified in the relation declaration. - * This parameter has been available since version 1.0.5. - * @return mixed the related object(s). - * @throws CDbException if the relation is not specified in {@link relations}. - * @since 1.0.2 - */ - 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 TDbException(get_class($this).' does not have relation "'.$name.'".'); - - Prado::trace('lazy loading '.get_class($this).'.'.$name,'System.Db.ActiveRecord.TActiveRecord'); - $relation=$md->relations[$name]; - if($this->getIsNewRecord() && ($relation instanceof THasOneRelation || $relation instanceof THasManyRelation)) - return $relation instanceof THasOneRelation ? null : array(); - - if($params!==array()) // dynamic query - { - $exists=isset($this->_related[$name]) || array_key_exists($name,$this->_related); - if($exists) - $save=$this->_related[$name]; - unset($this->_related[$name]); - $r=array($name=>$params); - } - else - $r=$name; - - $finder=new TActiveFinder($this,$r); - $finder->lazyFind($this); - - if(!isset($this->_related[$name])) - { - if($relation instanceof THasManyRelation) - $this->_related[$name]=array(); - else if($relation instanceof TStatRelation) - $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 the relation name - * @return booolean a value indicating whether the named related object(s) has been loaded. - * @since 1.0.3 - */ - 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 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. - * @since 1.0.5 - */ - public function getDbCriteria($createIfNull=true) - { - if($this->_c===null) - { - if(($c=$this->defaultScope())!==array() || $createIfNull) - $this->_c=new TDbCriteria($c); - } - return $this->_c; - } - - /** - * Returns the default named scope that should be applied to all queries implicitly for this model. - * 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}. - * @since 1.0.5 - */ - public function defaultScope() - { - return array(); - } - - /** - * 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, - *
-	 * public static function model($className=__CLASS__)
-	 * {
-	 *     return parent::model($className);
-	 * }
-	 * 
- * - * @param string 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->attachBehaviors($model->behaviors()); - $model->_md=new TActiveRecordMetaData($model); - return $model; - } - } - - /** - * @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; - } - - /** - * 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. - * @since 1.0.4 - */ - 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: - *
    - *
  • BELONGS_TO: e.g. a member belongs to a team;
  • - *
  • HAS_ONE: e.g. a member has at most one profile;
  • - *
  • HAS_MANY: e.g. a team has many members;
  • - *
  • MANY_MANY: e.g. a member has many skills and a skill belongs to a member.
  • - *
- * - * By declaring these relations, CActiveRecord can bring back related objects - * in either lazy loading or eager loading approach, and you save the effort of - * writing complex JOIN SQL statements. - * - * Each kind of related objects is defined in this method as an array with the following elements: - *
-	 * 'varName'=>array('relationType', 'className', 'foreignKey', ...additional options)
-	 * 
- * 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 'foreignKey' states the foreign key that relates the two kinds of active record. - * Note, for composite foreign keys, they must be listed together, separating with space or comma; - * and for foreign keys used in MANY_MANY relation, the joining table must be declared as well - * (e.g. 'joinTable(fk1, fk2)'). - * - * Additional options may be specified as name-value pairs in the rest array elements: - *
    - *
  • '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(??.name) AS name_count).
  • - *
  • 'condition': string, the WHERE clause. Defaults to empty. Note, column references need to - * be disambiguated with prefix '??.' (e.g. ??.age>20)
  • - *
  • 'order': string, the ORDER BY clause. Defaults to empty. Note, column references need to - * be disambiguated with prefix '??.' (e.g. ??.age DESC)
  • - *
  • '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.
  • - *
  • 'joinType': type of join. Defaults to 'LEFT OUTER JOIN'.
  • - *
  • 'alias': the alias for the table associated with this relationship. - * This option has been available since version 1.0.1. It defaults to null, - * meaning the table alias is the same as the relation name.
  • - *
  • 'params': the parameters to be bound to the generated SQL statement. - * This should be given as an array of name-value pairs. This option has been - * available since version 1.0.3.
  • - *
  • 'on': the ON clause. The condition specified here will be appended - * to the joining condition using the AND operator. This option has been - * available since version 1.0.2.
  • - *
- * - * The following options are available for certain relations when lazy loading: - *
    - *
  • 'group': string, the GROUP BY clause. Defaults to empty. Note, column references need to - * be disambiguated with prefix '??.' (e.g. ??.age). This option only applies to HAS_MANY and MANY_MANY relations.
  • - *
  • 'having': string, the HAVING clause. Defaults to empty. Note, column references need to - * be disambiguated with prefix '??.' (e.g. ??.age). This option only applies to HAS_MANY and MANY_MANY relations.
  • - *
  • 'limit': limit of the rows to be selected. This option does not apply to BELONGS_TO relation.
  • - *
  • 'offset': offset of the rows to be selected. This option does not apply to BELONGS_TO relation.
  • - *
- * - * Below is an example declaring related objects for 'Post' active record class: - *
-	 * return array(
-	 *     'author'=>array(self::BELONGS_TO, 'User', 'authorID'),
-	 *     'comments'=>array(self::HAS_MANY, 'Comment', 'postID', 'with'=>'author', 'order'=>'createTime DESC'),
-	 *     'tags'=>array(self::MANY_MANY, 'Tag', 'PostTag(postID, tagID)', 'order'=>'name'),
-	 * );
-	 * 
- * - * @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'. - *
-	 * return array(
-	 *     'published'=>array(
-	 *           'condition'=>'status=1',
-	 *     ),
-	 *     'recently'=>array(
-	 *           'order'=>'createTime DESC',
-	 *           'limit'=>5,
-	 *     ),
-	 * );
-	 * 
- * If the above scopes are declared in a 'Post' model, we can perform the following - * queries: - *
-	 * $posts=Post::model()->published()->findAll();
-	 * $posts=Post::model()->published()->recently()->findAll();
-	 * $posts=Post::model()->published()->with('comments')->findAll();
-	 * 
- * 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}. - * @since 1.0.5 - */ - 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. - * @since 1.0.1 - */ - public function attributeNames() - { - return array_keys($this->getMetaData()->columns); - } - - /** - * 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. - * - * @TODO Get default db connection if theres none set - * - */ - public function getDbConnection() - { - if(self::$db!==null) - return self::$db; - else - { - self::$db=Prado::getApplication()->getModule($this->getConnectionId())->getDbConnection(); - if(self::$db instanceof TDbConnection) - { - self::$db->setActive(true); - return self::$db; - } - else - throw new TDbException('ActiveRecord requires a db connection.'); - } - } - - /** - * Returns connection id (must be overloaded by child classes) - * @return string Id of TDbConnection module - */ - abstract public function getConnectionId(); - - /** - * @param string 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; - } - - /** - * @return CDbTableSchema the metadata of the table that this AR belongs to - */ - public function getTableSchema() - { - return $this->getMetaData()->tableSchema; - } - - /** - * @return CDbCommandBuilder the command builder used by this AR - */ - public function getCommandBuilder() - { - return $this->getDbConnection()->getSchema()->getCommandBuilder(); - } - - /** - * @param string 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 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 the attribute name - * @param mixed 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; - } - - /** - * Adds a related object to this record. - * This method is used internally by {@link CActiveFinder} to populate related objects. - * @param string attribute name - * @param mixed the related record - * @param boolean whether the relation is HAS_MANY/MANY_MANY. - */ - public function addRelatedRecord($name,$record,$multiple) - { - if($multiple) - { - if(!isset($this->_related[$name])) - $this->_related[$name]=array(); - if($record instanceof TActiveRecord) - $this->_related[$name][]=$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 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) - $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 whether to perform validation before saving the record. - * If the validation fails, the record will not be saved to database. - * @param array 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; - } - - /** - * @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; - } - - /** - * @param boolean 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. - * @param CEvent the event parameter - * @since 1.0.2 - */ - public function onBeforeSave($event) - { - $this->raiseEvent('onBeforeSave',$event); - } - - /** - * This event is raised after the record is saved. - * @param CEvent the event parameter - * @since 1.0.2 - */ - public function onAfterSave($event) - { - $this->raiseEvent('onAfterSave',$event); - } - - /** - * This event is raised before the record is deleted. - * @param CEvent the event parameter - * @since 1.0.2 - */ - public function onBeforeDelete($event) - { - $this->raiseEvent('onBeforeDelete',$event); - } - - /** - * This event is raised after the record is deleted. - * @param CEvent the event parameter - * @since 1.0.2 - */ - public function onAfterDelete($event) - { - $this->raiseEvent('onAfterDelete',$event); - } - - /** - * This event is raised after the record instance is created by new operator. - * @param CEvent the event parameter - * @since 1.0.2 - */ - public function onAfterConstruct($event) - { - $this->raiseEvent('onAfterConstruct',$event); - } - - /** - * This event is raised after the record is instantiated by a find method. - * @param CEvent the event parameter - * @since 1.0.2 - */ - 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() - { - $this->onBeforeSave(new TEvent($this)); - return true; - } - - /** - * This method is invoked after saving a record. - * 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() - { - $this->onAfterSave(new TEvent($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() - { - $this->onBeforeDelete(new TEvent($this)); - 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() - { - $this->onAfterDelete(new TEvent($this)); - } - - /** - * This method is invoked after a record instance is created by new operator. - * The default implementation raises the {@link onAfterConstruct} event. - * You may override this method to do postprocessing after record creation. - * Make sure you call the parent implementation so that the event is raised properly. - */ - protected function afterConstruct() - { - $this->onAfterConstruct(new TEvent($this)); - } - - /** - * 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() - { - $this->onAfterFind(new TEvent($this)); - } - - /** - * Calls {@link afterFind}. - * This method is internally used. - * @since 1.0.3 - */ - 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 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 TDbException('The active record cannot be inserted to database because it is not new.'); - if($this->beforeSave()) - { - Prado::trace(get_class($this).'.insert()','System.Db.ActiveRecord.TActiveRecord'); - $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->afterSave(); - $this->setIsNewRecord(false); - $this->setScenario('update'); - return true; - } - else - $this->afterSave(); - } - else - 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 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 TDbException('The active record cannot be updated because it is new.'); - if($this->beforeSave()) - { - Prado::trace(get_class($this).'.update()','System.Db.ActiveRecord.TActiveRecord'); - $this->updateByPk($this->getPrimaryKey(),$this->getAttributes($attributes)); - $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. It thus has better performance. - * Note, 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: - *
-	 * $postRecord=Post::model()->findByPk($postID);
-	 * $postRecord->attributes=$_POST['post'];
-	 * $postRecord->save();
-	 * 
- * @param array 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()) - { - Prado::trace(get_class($this).'.saveAttributes()','System.Db.ActiveRecord.TActiveRecord'); - $values=array(); - foreach($attributes as $name=>$value) - { - if(is_integer($name)) - $values[$value]=$this->$value; - else - $values[$name]=$this->$name=$value; - } - return $this->updateByPk($this->getPrimaryKey(),$values)>0; - } - else - throw new TDbException('The active record cannot be updated because it is new.'); - } - - /** - * 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()) - { - Prado::trace(get_class($this).'.delete()','System.Db.ActiveRecord.TActiveRecord'); - if($this->beforeDelete()) - { - $result=$this->deleteByPk($this->getPrimaryKey())>0; - $this->afterDelete(); - return $result; - } - else - return false; - } - else - throw new TDbException('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() - { - Prado::trace(get_class($this).'.refresh()','System.Db.ActiveRecord.TActiveRecord'); - if(!$this->getIsNewRecord() && ($record=$this->findByPk($this->getPrimaryKey()))!==null) - { - $this->_attributes=array(); - $this->_related=array(); - foreach($this->getMetaData()->columns as $name=>$column) - $this->$name=$record->$name; - return true; - } - else - return false; - } - - /** - * Compares this active record with another one. - * The comparison is made by comparing the primary key values of the two active records. - * @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(); - } - - /** - * @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; - } - - private function query($criteria,$all=false) - { - $this->applyScopes($criteria); - $command=$this->getCommandBuilder()->createFindCommand($this->getTableSchema(),$criteria); - return $all ? $this->populateRecords($command->queryAll()) : $this->populateRecord($command->queryRow()); - } - - private function applyScopes(&$criteria) - { - if(($c=$this->getDbCriteria(false))!==null) - { - $c->mergeWith($criteria); - $criteria=$c; - $this->_c=null; - } - } - - /** - * Finds a single active record with the specified condition. - * @param mixed 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 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()) - { - Prado::trace(get_class($this).'.find()','System.Db.ActiveRecord.TActiveRecord'); - $criteria=$this->getCommandBuilder()->createCriteria($condition,$params); - $criteria->limit=1; - return $this->query($criteria); - } - - /** - * Finds all active records satisfying the specified condition. - * See {@link find()} for detailed explanation about $condition and $params. - * @param mixed query condition or criteria. - * @param array 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()) - { - Prado::trace(get_class($this).'.findAll()','System.Db.ActiveRecord.TActiveRecord'); - $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 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 query condition or criteria. - * @param array 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()) - { - Prado::trace(get_class($this).'.findByPk()','System.Db.ActiveRecord.TActiveRecord'); - $criteria=$this->getCommandBuilder()->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); - $criteria->limit=1; - 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 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 query condition or criteria. - * @param array 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()) - { - Prado::trace(get_class($this).'.findAllByPk()','System.Db.ActiveRecord.TActiveRecord'); - $criteria=$this->getCommandBuilder()->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); - 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 list of attribute values (indexed by attribute names) that the active records should match. - * @param mixed query condition or criteria. - * @param array 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()) - { - Prado::trace(get_class($this).'.findByAttributes()','System.Db.ActiveRecord.TActiveRecord'); - $criteria=$this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(),$attributes,$condition,$params); - $criteria->limit=1; - 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 list of attribute values (indexed by attribute names) that the active records should match. - * @param mixed query condition or criteria. - * @param array 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()) - { - Prado::trace(get_class($this).'.findAllByAttributes()','System.Db.ActiveRecord.TActiveRecord'); - $criteria=$this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(),$attributes,$condition,$params); - return $this->query($criteria,true); - } - - /** - * Finds a single active record with the specified SQL statement. - * @param string the SQL statement - * @param array parameters to be bound to the SQL statement - * @return CActiveRecord the record found. Null if none is found. - */ - public function findBySql($sql,$params=array()) - { - Prado::trace(get_class($this).'.findBySql()','System.Db.ActiveRecord.TActiveRecord'); - $command=$this->getCommandBuilder()->createSqlCommand($sql,$params); - return $this->populateRecord($command->queryRow()); - } - - /** - * Finds all active records using the specified SQL statement. - * @param string the SQL statement - * @param array 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()) - { - Prado::trace(get_class($this).'.findAllBySql()','System.Db.ActiveRecord.TActiveRecord'); - $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 query condition or criteria. - * @param array parameters to be bound to an SQL statement. - * @return integer the number of rows satisfying the specified query condition. - */ - public function count($condition='',$params=array()) - { - Prado::trace(get_class($this).'.count()','System.Db.ActiveRecord.TActiveRecord'); - $builder=$this->getCommandBuilder(); - $criteria=$builder->createCriteria($condition,$params); - $this->applyScopes($criteria); - return $builder->createCountCommand($this->getTableSchema(),$criteria)->queryScalar(); - } - - /** - * Finds the number of rows using the given SQL statement. - * See {@link find()} for detailed explanation about $condition and $params. - * @param string the SQL statement - * @param array parameters to be bound to the SQL statement - * @return integer the number of rows using the given SQL statement. - */ - public function countBySql($sql,$params=array()) - { - Prado::trace(get_class($this).'.countBySql()','System.Db.ActiveRecord.TActiveRecord'); - 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 query condition or criteria. - * @param array parameters to be bound to an SQL statement. - * @return boolean whether there is row satisfying the specified condition. - */ - public function exists($condition,$params=array()) - { - Prado::trace(get_class($this).'.exists()','System.Db.ActiveRecord.TActiveRecord'); - $builder=$this->getCommandBuilder(); - $criteria=$builder->createCriteria($condition,$params); - $table=$this->getTableSchema(); - $criteria->select=reset($table->columns)->rawName; - $criteria->limit=1; - $this->applyScopes($criteria); - return $builder->createFindCommand($table,$criteria)->queryRow()!==false; - } - - /** - * 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, - *
-	 * // 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();
-	 * 
- * 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, - *
-	 * Post::model()->with(array(
-	 *     'author'=>array('select'=>'id, name'),
-	 *     'comments'=>array('condition'=>'approved=1', 'order'=>'createTime'),
-	 * ))->findAll();
-	 * 
- * - * This method returns a {@link CActiveFinder} instance that provides - * a set of find methods similar to that of CActiveRecord. - * - * Note, the possible parameters to this method have been changed since version 1.0.2. - * Previously, it was not possible to specify on-th-fly query options, - * and child-relations were specified as hierarchical arrays. - * - * @return CActiveFinder the active finder instance. If no parameter is passed in, the object itself will be returned. - */ - 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]; - $finder=new TActiveFinder($this,$with,$this->getDbCriteria(false)); - $this->_c=null; - return $finder; - } - else - 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 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 list of attributes (name=>$value) to be updated - * @param mixed query condition or criteria. - * @param array parameters to be bound to an SQL statement. - * @return integer the number of rows being updated - */ - public function updateByPk($pk,$attributes,$condition='',$params=array()) - { - Prado::trace(get_class($this).'.updateByPk()','System.Db.ActiveRecord.TActiveRecord'); - $builder=$this->getCommandBuilder(); - $table=$this->getTableSchema(); - $criteria=$builder->createPkCriteria($table,$pk,$condition,$params); - $this->applyScopes($criteria); - $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 list of attributes (name=>$value) to be updated - * @param mixed query condition or criteria. - * @param array parameters to be bound to an SQL statement. - * @return integer the number of rows being updated - */ - public function updateAll($attributes,$condition='',$params=array()) - { - Prado::trace(get_class($this).'.updateAll()','System.Db.ActiveRecord.TActiveRecord'); - $builder=$this->getCommandBuilder(); - $criteria=$builder->createCriteria($condition,$params); - $this->applyScopes($criteria); - $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 the counters to be updated (column name=>increment value) - * @param mixed query condition or criteria. - * @param array parameters to be bound to an SQL statement. - * @return integer the number of rows being updated - */ - public function updateCounters($counters,$condition='',$params=array()) - { - Prado::trace(get_class($this).'.updateCounters()','System.Db.ActiveRecord.TActiveRecord'); - $builder=$this->getCommandBuilder(); - $criteria=$builder->createCriteria($condition,$params); - $this->applyScopes($criteria); - $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 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 query condition or criteria. - * @param array parameters to be bound to an SQL statement. - * @return integer the number of rows deleted - */ - public function deleteByPk($pk,$condition='',$params=array()) - { - Prado::trace(get_class($this).'.deleteByPk()','System.Db.ActiveRecord.TActiveRecord'); - $builder=$this->getCommandBuilder(); - $criteria=$builder->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); - $this->applyScopes($criteria); - $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 query condition or criteria. - * @param array parameters to be bound to an SQL statement. - * @return integer the number of rows deleted - */ - public function deleteAll($condition='',$params=array()) - { - Prado::trace(get_class($this).'.deleteAll()','System.Db.ActiveRecord.TActiveRecord'); - $builder=$this->getCommandBuilder(); - $criteria=$builder->createCriteria($condition,$params); - $this->applyScopes($criteria); - $command=$builder->createDeleteCommand($this->getTableSchema(),$criteria); - return $command->execute(); - } - - /** - * Creates an active record with the given attributes. - * This method is internally used by the find methods. - * @param array attribute values (column name=>column value) - * @param boolean whether to call {@link afterFind} after the record is populated. - * This parameter is added in version 1.0.3. - * @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->_md=$this->getMetaData(); - foreach($attributes as $name=>$value) - { - if(property_exists($record,$name)) - $record->$name=$value; - else if(isset($record->_md->columns[$name])) - $record->_attributes[$name]=$value; - } - $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 list of attribute values for the active records. - * @param boolean whether to call {@link afterFind} after each record is populated. - * This parameter is added in version 1.0.3. - * @return array list of active records. - */ - public function populateRecords($data,$callAfterFind=true) - { - $records=array(); - $md=$this->getMetaData(); - $table=$md->tableSchema; - foreach($data as $attributes) - { - $record=$this->instantiate($attributes); - $record->_md=$md; - foreach($attributes as $name=>$value) - { - if(property_exists($record,$name)) - $record->$name=$value; - else if(isset($record->_md->columns[$name])) - $record->_attributes[$name]=$value; - } - $record->attachBehaviors($record->behaviors()); - if($callAfterFind) - $record->afterFind(); - $records[]=$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 list of attribute values for the active records. - * @return CActiveRecord the active record - * @since 1.0.2 - */ - protected function instantiate($attributes) - { - $class=get_class($this); - $model=new $class(null); - $model->setScenario('update'); - return $model; - } - - /** - * Returns whether there is an element at the specified offset. - * This method is required by the interface ArrayAccess. - * @param mixed the offset to check on - * @return boolean - * @since 1.0.2 - */ - public function offsetExists($offset) - { - return isset($this->getMetaData()->columns[$offset]); - } -} - - -/** - * CBaseActiveRelation is the base class for all active relations. - * @author Qiang Xue - * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0.4 - */ -class TBaseActiveRelation extends TComponent -{ - /** - * @var string name of the related object - */ - public $name; - /** - * @var string name of the related active record class - */ - public $className; - /** - * @var string 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 '??.'. - */ - public $select='*'; - /** - * @var string WHERE clause. For {@link CActiveRelation} descendant classes, column names - * referenced in the condition should be disambiguated with prefix '??.'. - */ - 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 '??.'. - */ - public $group=''; - /** - * @var string HAVING clause. For {@link CActiveRelation} descendant classes, column names - * referenced in this property should be disambiguated with prefix '??.'. - */ - public $having=''; - /** - * @var string ORDER BY clause. For {@link CActiveRelation} descendant classes, column names - * referenced in this property should be disambiguated with prefix '??.'. - */ - public $order=''; - - /** - * Constructor. - * @param string name of the relation - * @param string name of the related active record class - * @param string foreign key for this relation - * @param array 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 the dynamically specified criteria - * @since 1.0.5 - */ - public function mergeWith($criteria) - { - 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']; - } - - 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['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 - * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0.4 - */ -class TStatRelation extends TBaseActiveRelation -{ - /** - * @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 the dynamically specified criteria - * @since 1.0.5 - */ - public function mergeWith($criteria) - { - parent::mergeWith($criteria); - - 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 - * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0 - */ -class TActiveRelation extends TBaseActiveRelation -{ - /** - * @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. - * @since 1.0.2 - */ - 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. - * @since 1.0.1 - */ - 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(); - - /** - * Merges this relation with a criteria specified dynamically. - * @param array the dynamically specified criteria - * @since 1.0.5 - */ - public function mergeWith($criteria) - { - 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 - * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0 - */ -class TBelongsToRelation extends TActiveRelation -{ -} - - -/** - * CHasOneRelation represents the parameters specifying a HAS_ONE relation. - * @author Qiang Xue - * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0 - */ -class THasOneRelation extends TActiveRelation -{ -} - - -/** - * CHasManyRelation represents the parameters specifying a HAS_MANY relation. - * @author Qiang Xue - * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0 - */ -class THasManyRelation extends TActiveRelation -{ - /** - * @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 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. Defaults to true, meaning the table will be joined - * together with the primary table. Note that in version 1.0.x, the default value of this property was false. - */ - public $together=true; - - /** - * Merges this relation with a criteria specified dynamically. - * @param array the dynamically specified criteria - * @since 1.0.5 - */ - public function mergeWith($criteria) - { - parent::mergeWith($criteria); - if(isset($criteria['limit']) && $criteria['limit']>0) - $this->limit=$criteria['limit']; - - if(isset($criteria['offset']) && $criteria['offset']>=0) - $this->offset=$criteria['offset']; - } -} - - -/** - * CManyManyRelation represents the parameters specifying a MANY_MANY relation. - * @author Qiang Xue - * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0 - */ -class TManyManyRelation extends THasManyRelation -{ -} - - -/** - * CActiveRecordMetaData represents the meta-data for an Active Record class. - * - * @author Qiang Xue - * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0 - */ -class TActiveRecordMetaData -{ - /** - * @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 the model instance - */ - public function __construct($model) - { - $this->_model=$model; - - $tableName=$model->tableName(); - if(($table=$model->getDbConnection()->getSchema()->getTable($tableName))===null) - throw new TDbException('The table "'.$tableName.'" for active record class "'.get_class($model).'" cannot be found in the database.'); - - if($table->primaryKey===null) - $table->primaryKey=$model->primaryKey(); - $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) - { - 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 TDbException('Active record "'.get_class($model).'" has an invalid configuration for relation "'.$name.'". It must specify the relation type, the related active record class and the foreign key.'); - } - } -} - -/** - * TActiveRecordInvalidFinderResult class. - * TActiveRecordInvalidFinderResult defines the enumerable type for possible results - * if an invalid {@link TActiveRecord::__call magic-finder} invoked. - * - * The following enumerable values are defined: - * - Null: return null (default) - * - Exception: throws a TActiveRecordException - * - * @author Yves Berkholz - * @version $Id: TActiveRecord.php 2649 2009-05-11 07:44:55Z godzilla80@gmx.net $ - * @package System.Data.ActiveRecord - * @see TActiveRecordManager::setInvalidFinderResult - * @see TActiveRecordConfig::setInvalidFinderResult - * @see TActiveRecord::setInvalidFinderResult - * @since 3.1.5 - */ -class TActiveRecordInvalidFinderResult extends TEnumerable -{ - const Null = 'Null'; - const Exception = 'Exception'; -} diff --git a/framework/Db/ActiveRecord/TActiveRecordBehavior.php b/framework/Db/ActiveRecord/TActiveRecordBehavior.php deleted file mode 100644 index 1e4ccbae..00000000 --- a/framework/Db/ActiveRecord/TActiveRecordBehavior.php +++ /dev/null @@ -1,94 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 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}. - * - * @author Qiang Xue - * @version $Id: CActiveRecordBehavior.php 564 2009-01-21 22:07:10Z qiang.xue $ - * @package System.Db.ActiveRecord - * @since 1.0.2 - */ -class TActiveRecordBehavior extends TModelBehavior -{ - /** - * 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', - 'onAfterConstruct'=>'afterConstruct', - '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}. - * @param CEvent 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 CEvent 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}. - * @param CEvent 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 parameter - */ - public function afterDelete($event) - { - } - - /** - * Responds to {@link CActiveRecord::onAfterConstruct} event. - * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. - * @param CEvent event parameter - */ - public function afterConstruct($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 parameter - */ - public function afterFind($event) - { - } -} diff --git a/framework/Db/ActiveRecord/TActiveRecordCriteria.php b/framework/Db/ActiveRecord/TActiveRecordCriteria.php deleted file mode 100644 index 9dded69a..00000000 --- a/framework/Db/ActiveRecord/TActiveRecordCriteria.php +++ /dev/null @@ -1,254 +0,0 @@ - - */ - -Prado::using('System.Db.Schema.TDbCriteria'); - -class TSqlCriteria extends TDbCriteria -{ - - private $_TDbCriteria = null; - - public function __construct($condition=null, $parameters=array()) - { - if(!is_array($parameters) && func_num_args() > 1) - $parameters = array_slice(func_get_args(), 1); - - $this->_TDbCriteria = $this; - - $this->_TDbCriteria->params = new ArrayObject(); - - $param = array( - 'condition' => ($condition !== null) ? $condition : '', - 'params' => $parameters - ); - } - - public function getCondition() - { - return $this->_TDbCriteria->condition; - } - - public function setCondition($value) - { - if(empty($value)) - return; - - // supporting the following SELECT-syntax: - // [ORDER BY {col_name | expr | position} - // [ASC | DESC], ...] - // [LIMIT {[offset,] row_count | row_count OFFSET offset}] - // See: http://dev.mysql.com/doc/refman/5.0/en/select.html - - - if(preg_match('/ORDER\s+BY\s+(.*?)(?=LIMIT)|ORDER\s+BY\s+(.*?)$/i', $value, $matches) > 0) { - // condition contains ORDER BY - $value = str_replace($matches[0], '', $value); - if(strlen($matches[1]) > 0) { - $this->_TDbCriteria->setOrdersBy($matches[1]); - } else if(strlen($matches[2]) > 0) { - $this->setOrdersBy($matches[2]); - } - } - - if(preg_match('/LIMIT\s+([\d\s,]+)/i', $value, $matches) > 0) { - // condition contains limit - $value = str_replace($matches[0], '', $value); - // remove limit from query - if(strpos($matches[1], ',')) { // both offset and limit given - list($offset, $limit) = explode(',', $matches[1]); - $this->_TDbCriteria->limit = (int)$limit; - $this->_TDbCriteria->offset = (int)$offset; - } else { // only limit given - $this->_TDbCriteria->limit = (int)$matches[1]; - } - } - - if(preg_match('/OFFSET\s+(\d+)/i', $value, $matches) > 0) { - // condition contains offset - $value = str_replace($matches[0], '', $value); - // remove offset from query - $this->_TDbCriteria->offset = (int)$matches[1]; // set offset in criteria - } - - $this->_TDbCriteria->condition = trim($value); - } - - public function getParameters() - { - return $this->_TDbCriteria->params; - } - - public function setParameters($value) - { - if(is_array($value)) - { - $this->_TDbCriteria->params = $value; - } - elseif ($value instanceof ArrayAccess) - { - $this->_TDbCriteria->params = (array)$value; - } - else - throw new TException('Value must be an array or of type ArrayAccess.'); - } - - public function getIsNamedParameters() - { - foreach($this->_TDbCriteria->params as $key=>$val) - return is_string($key); - } - - public function getLimit() - { - return ($this->_TDbCriteria->limit != -1) ? $this->_TDbCriteria->limit : null; - } - - public function setLimit($value) - { - $this->_TDbCriteria->limit = (int)$value; - } - - public function getOffset() - { - return ($this->_TDbCriteria->offset != -1) ? $this->_TDbCriteria->offset : null; - } - - public function setOffset($value) - { - $this->_TDbCriteria->offset = (int)$value; - } - - public function getOrdersBy() - { - if(empty($this->_TDbCriteria->order)) - return array(); - - $value=trim(preg_replace('/\s+/',' ', $this->_TDbCriteria->order)); - $orderBys=array(); - foreach(explode(',',$value) as $orderBy) - { - $vs=explode(' ',trim($orderBy)); - - $orderBys[$vs[0]]=isset($vs[1])?$vs[1]:'asc'; - } - return $orderBys; - } - - public function setOrdersBy($value) - { - if(is_string($value)) - $this->TDbCriteria->order = $value; - elseif (is_array($value) || $value instanceof Traversable) - { - $str = ''; - foreach($value as $key => $val) - $str .= (($str != '') ? ', ': '').$key.' '.$val; - - $this->_TDbCriteria->order = $str; - } - } - - public function __toString() - { - $str = ''; - if(strlen((string)$this->getCondition()) > 0) - $str .= '"'.(string)$this->getCondition().'"'; - $params = array(); - foreach($this->getParameters() as $k=>$v) - $params[] = "{$k} => ${v}"; - if(count($params) > 0) - $str .= ', "'.implode(', ',$params).'"'; - $orders = array(); - foreach($this->getOrdersBy() as $k=>$v) - $orders[] = "{$k} => ${v}"; - if(count($orders) > 0) - $str .= ', "'.implode(', ',$orders).'"'; - if($this->getLimit() !==null) - $str .= ', '.$this->_limit; - if($this->getOffset() !== null) - $str .= ', '.$this->_offset; - return $str; - } - - /** - * Returns a property value or an event handler list by property or event name. - * Do not call this method. This is a PHP magic method that we override - * to allow using the following syntax to read a property: - * - * $value=$component->PropertyName; - * - * and to obtain the event handler list for an event, - * - * $eventHandlerList=$component->EventName; - * - * @param string the property name or the event name - * @return mixed the property value or the event handler list - * @throws TInvalidOperationException if the property/event is not defined. - */ - public function __get($name) - { - $getter='get'.$name; - if(method_exists($this,$getter)) - { - // getting a property - return $this->$getter(); - } - else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) - { - // getting an event (handler list) - $name=strtolower($name); - if(!isset($this->_e[$name])) - $this->_e[$name]=new TList; - return $this->_e[$name]; - } - else - { - throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); - } - } - - /** - * Sets value of a component property. - * Do not call this method. This is a PHP magic method that we override - * to allow using the following syntax to set a property or attach an event handler. - * - * $this->PropertyName=$value; - * $this->EventName=$handler; - * - * @param string the property name or event name - * @param mixed the property value or event handler - * @throws TInvalidOperationException If the property is not defined or read-only. - */ - public function __set($name,$value) - { - $setter='set'.$name; - if(method_exists($this,$setter)) - { - $this->$setter($value); - } - else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) - { - $this->attachEventHandler($name,$value); - } - else if(method_exists($this,'get'.$name)) - { - throw new TInvalidOperationException('component_property_readonly',get_class($this),$name); - } - else - { - throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); - } - } - -} - -class TActiveRecordCriteria extends TSqlCriteria -{ - -} diff --git a/framework/Db/Schema/TDbColumnSchema.php b/framework/Db/Schema/TDbColumnSchema.php deleted file mode 100755 index f2c4faaa..00000000 --- a/framework/Db/Schema/TDbColumnSchema.php +++ /dev/null @@ -1,145 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbExpression'); - -/** - * TDbColumnSchema class describes the column meta data of a database table. - * - * @author Qiang Xue - * @version $Id: TDbColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema - * @since 1.0 - */ -class TDbColumnSchema extends TComponent -{ - /** - * @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; - - - /** - * 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 the column's DB type - * @param mixed 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 DB type - */ - protected function extractType($dbType) - { - if(stripos($dbType,'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 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 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 input value - * @return mixed converted value - */ - public function typecast($value) - { - if(gettype($value)===$this->type || $value===null || $value instanceof TDbExpression) - return $value; - if($value==='') - return $this->type==='string' ? '' : null; - switch($this->type) - { - case 'integer': return (integer)$value; - case 'boolean': return (boolean)$value; - case 'double': return (double)$value; - case 'string': return (string)$value; - default: return $value; - } - } -} diff --git a/framework/Db/Schema/TDbCommandBuilder.php b/framework/Db/Schema/TDbCommandBuilder.php deleted file mode 100755 index 7c25888d..00000000 --- a/framework/Db/Schema/TDbCommandBuilder.php +++ /dev/null @@ -1,656 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbSchema'); -prado::using('System.Db.Schema.TDbCriteria'); - -/** - * TDbCommandBuilder provides basic methods to create query commands for tables. - * - * @author Qiang Xue - * @version $Id: TDbCommandBuilder.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema - * @since 1.0 - */ -class TDbCommandBuilder extends TComponent -{ - const PARAM_PREFIX=':yp'; - - private $_schema; - private $_connection; - - /** - * @param TDbSchema the schema for this command builder - */ - public function __construct($schema) - { - $this->_schema=$schema; - $this->_connection=$schema->getDbConnection(); - } - - /** - * @return TDbConnection database connection. - */ - public function getDbConnection() - { - return $this->_connection; - } - - /** - * @return TDbSchema the schema for this command builder. - */ - public function getSchema() - { - return $this->_schema; - } - - /** - * Returns the last insertion ID for the specified table. - * @param mixed the table schema ({@link TDbTableSchema}) 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 the table schema ({@link TDbTableSchema}) or the table name (string). - * @param TDbCriteria the query criteria - * @return TDbCommand query command. - */ - public function createFindCommand($table,$criteria) - { - $this->ensureTable($table); - $select=is_array($criteria->select) ? implode(', ',$criteria->select) : $criteria->select; - $sql="SELECT {$select} 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 a COUNT(*) command for a single table. - * @param mixed the table schema ({@link TDbTableSchema}) or the table name (string). - * @param TDbCriteria the query criteria - * @return TDbCommand query command. - */ - public function createCountCommand($table,$criteria) - { - $this->ensureTable($table); - $criteria->select='COUNT(*)'; - return $this->createFindCommand($table,$criteria); - } - - /** - * Creates a DELETE command. - * @param mixed the table schema ({@link TDbTableSchema}) or the table name (string). - * @param TDbCriteria the query criteria - * @return TDbCommand 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 the table schema ({@link TDbTableSchema}) or the table name (string). - * @param array data to be inserted (column name=>column value). If a key is not a valid column name, the corresponding value will be ignored. - * @return TDbCommand 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 TDbExpression) - $placeholders[]=(string)$value; - 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).')'; - $command=$this->_connection->createCommand($sql); - - foreach($values as $name=>$value) - $command->bindValue($name,$value); - - return $command; - } - - /** - * Creates an UPDATE command. - * @param mixed the table schema ({@link TDbTableSchema}) or the table name (string). - * @param array list of columns to be updated (name=>value) - * @param TDbCriteria the query criteria - * @return TDbCommand 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 TDbExpression) - $fields[]=$column->rawName.'='.(string)$value; - 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 TDbException('No columns are being updated for table "{0}".', - $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 the table schema ({@link TDbTableSchema}) or the table name (string). - * @param TDbCriteria the query criteria - * @param array counters to be updated (counter increments/decrements indexed by column names.) - * @return TDbCommand 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 TDbException('No counter columns are being updated for table "{0}".', - $table->name); - } - - /** - * Creates a command based on a given SQL statement. - * @param string the explicitly specified SQL statement - * @param array parameters that will be bound to the SQL statement - * @return TDbCommand 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 the SQL statement to be altered - * @param string 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 the SQL statement without WHERE clause - * @param string 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 statement without ORDER BY. - * @param string 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 query string without LIMIT and OFFSET. - * @param integer maximum number of rows, -1 to ignore limit. - * @param integer 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 query string without GROUP BY. - * @param string 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 query string without HAVING - * @param string HAVING - * @return string SQL with HAVING - * @since 1.0.1 - */ - public function applyHaving($sql,$having) - { - if($having!=='') - return $sql.' HAVING '.$having; - else - return $sql; - } - - /** - * Binds parameter values for an SQL command. - * @param TDbCommand database command - * @param array 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 the table schema ({@link TDbTableSchema}) or the table name (string). - * @param mixed 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 TDbCriteria} object; - * Otherwise, it should be an instance of {@link TDbCriteria}. - * @param array 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 TDbCriteria::params} to set parameters. - * @return TDbCriteria the created query criteria - * @throws CException if the condition is not string, array and TDbCriteria - */ - public function createCriteria($condition='',$params=array()) - { - if(is_array($condition)) - $criteria=new TDbCriteria($condition); - else if($condition instanceof TDbCriteria) - $criteria=clone $condition; - else - { - $criteria=new TDbCriteria; - $criteria->condition=$condition; - $criteria->params=$params; - } - return $criteria; - } - - /** - * Creates a query criteria with the specified primary key. - * @param mixed the table schema ({@link TDbTableSchema}) or the table name (string). - * @param mixed 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 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 TDbCriteria}; - * Otherwise, it should be an instance of {@link TDbCriteria}. - * @param array 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 TDbCriteria::params} to set parameters. - * @return TDbCriteria the created query criteria - */ - public function createPkCriteria($table,$pk,$condition='',$params=array()) - { - $this->ensureTable($table); - $criteria=$this->createCriteria($condition,$params); - 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); - 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 the table schema ({@link TDbTableSchema}) or the table name (string). - * @param array list of primary key values to be selected within - * @param string 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 the table schema ({@link TDbTableSchema}) or the table name (string). - * @param array column values that should be matched in the query (name=>value) - * @param mixed 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 TDbCriteria}; - * Otherwise, it should be an instance of {@link TDbCriteria}. - * @param array 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 TDbCriteria::params} to set parameters. - * @return TDbCriteria the created query criteria - */ - public function createColumnCriteria($table,$columns,$condition='',$params=array()) - { - $this->ensureTable($table); - $criteria=$this->createCriteria($condition,$params); - $bindByPosition=isset($criteria->params[0]); - $conditions=array(); - $values=array(); - $i=0; - foreach($columns as $name=>$value) - { - if(($column=$table->getColumn($name))!==null) - { - if($value!==null) - { - if($bindByPosition) - { - $conditions[]=$table->rawName.'.'.$column->rawName.'=?'; - $values[]=$value; - } - else - { - $conditions[]=$table->rawName.'.'.$column->rawName.'='.self::PARAM_PREFIX.$i; - $values[self::PARAM_PREFIX.$i]=$value; - $i++; - } - } - else - $conditions[]=$table->rawName.'.'.$column->rawName.' IS NULL'; - } - else - throw new TDbException('Table "{0}" does not have a column named "{1}".', - $table->name,$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 the table schema ({@link TDbTableSchema}) or the table name (string). - * @param array list of column names for potential search condition. - * @param mixed search keywords. This can be either a string with space-separated keywords or an array of keywords. - * @param string optional column prefix (with dot at the end). If null, the table name will be used as the prefix. - * @param boolean whether the search is case-sensitive. Defaults to true. This parameter - * has been available since version 1.0.4. - * @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 TDbException('Table "{0}" does not have a column named "{0}".', - $table->name,$name); - $condition=array(); - foreach($keywords as $keyword) - { - 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 the table schema ({@link TDbTableSchema}) or the table name (string). - * @param mixed 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 list of key values to be selected within - * @param string column prefix (ended with dot). If null, it will be the table name - * @return string the expression for selection - * @since 1.0.4 - */ - 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 TDbException('Table "{0}" does not have a column named "{1}".', - $table->name, $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 TDbException('Table "{0}" does not have a column named "{1}".', - $table->name, $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 TDbException('The value for the column "{1}" is not supplied when querying the table "{0}".', - $table->name,$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 TDbException('Column name must be either a string or an array.'); - } - - /** - * Generates the expression for selecting rows with specified composite key values. - * @param TDbTableSchema the table schema - * @param array list of primary key values to be selected within - * @param string column prefix (ended with dot) - * @return string the expression for selection - * @since 1.0.4 - */ - 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 schema ({@link TDbTableSchema}) or table name (string). - * If this refers to a valid table name, this parameter will be returned with the corresponding table schema. - * @throws TDbException if the table name is not valid - * @since 1.0.4 - */ - protected function ensureTable(&$table) - { - if(is_string($table) && ($table=$this->_schema->getTable($tableName=$table))===null) - throw new TDbException('Table "{0}" does not exist.', - $tableName); - } -} diff --git a/framework/Db/Schema/TDbCriteria.php b/framework/Db/Schema/TDbCriteria.php deleted file mode 100755 index 3dab036a..00000000 --- a/framework/Db/Schema/TDbCriteria.php +++ /dev/null @@ -1,166 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -/** - * TDbCriteria represents a query criteria, such as conditions, ordering by, limit/offset. - * - * @author Qiang Xue - * @version $Id: TDbCriteria.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package system.db.schema - * @since 1.0 - */ -class TDbCriteria -{ - /** - * @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 string query condition. This refers to the WHERE clause in an SQL statement. - * For example, age>31 AND team=1. - */ - public $condition=''; - /** - * @var array list of query parameter values indexed by parameter placeholders. - * For example, array(':name'=>'Dan', ':age'=>31). - */ - 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, 'projectID, teamID'. - */ - public $group=''; - /** - * @var string how to join with other tables. This refers to the JOIN clause in an SQL statement. - * For example, 'LEFT JOIN users ON users.id=authorID'. - */ - public $join=''; - /** - * @var string the condition to be applied with GROUP-BY clause. - * For example, 'SUM(revenue)<50000'. - * @since 1.0.1 - */ - public $having=''; - - /** - * Constructor. - * @param array criteria initial property values (indexed by property name) - */ - public function __construct($data=array()) - { - foreach($data as $name=>$value) - $this->$name=$value; - } - - /** - * 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 TDbCriteria the criteria to be merged with. - * @param boolean whether to use 'AND' to merge condition and having options. - * If false, 'OR' will be used instead. Defaults to 'AND'. This parameter has been - * available since version 1.0.6. - * @since 1.0.5 - */ - 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($this->order!==$criteria->order) - { - if($this->order==='') - $this->order=$criteria->order; - else if($criteria->order!=='') - $this->order.=', '.$criteria->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})"; - } - } - - /** - * @return array the array representation of the criteria - * @since 1.0.6 - */ - public function toArray() - { - $result=array(); - foreach(array('select', 'condition', 'params', 'limit', 'offset', 'order', 'group', 'join', 'having') as $name) - $result[$name]=$this->$name; - return $result; - } -} diff --git a/framework/Db/Schema/TDbExpression.php b/framework/Db/Schema/TDbExpression.php deleted file mode 100755 index 86b9a1ef..00000000 --- a/framework/Db/Schema/TDbExpression.php +++ /dev/null @@ -1,49 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -/** - * TDbExpression represents a DB expression that does not need escaping. - * TDbExpression is mainly used in {@link CActiveRecord} as attribute values. - * When inserting or updating a {@link CActiveRecord}, attribute values of - * type TDbExpression 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. - * - * @author Qiang Xue - * @version $Id: TDbExpression.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package system.db.schema - * @since 1.0.2 - */ -class TDbExpression extends TComponent -{ - /** - * @var string the DB expression - */ - public $expression; - - /** - * Constructor. - * @param string the DB expression - */ - public function __construct($expression) - { - $this->expression=$expression; - } - - /** - * 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/TDbSchema.php b/framework/Db/Schema/TDbSchema.php deleted file mode 100755 index 6837b7e5..00000000 --- a/framework/Db/Schema/TDbSchema.php +++ /dev/null @@ -1,193 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbCommandBuilder'); - -/** - * TDbSchema is the base class for retrieving metadata information. - * - * @author Qiang Xue - * @version $Id: TDbSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema - * @since 1.0 - */ -abstract class TDbSchema extends TComponent -{ - private $_tableNames=array(); - private $_tables=array(); - private $_connection; - private $_builder; - private $_cacheExclude=array(); - - /** - * Creates a table instance representing the metadata for the named table. - * @return TDbTableSchema driver dependent table metadata, null if the table does not exist. - */ - abstract protected function createTable($name); - - /** - * Constructor. - * @param TDbConnection database connection. - */ - public function __construct($conn) - { - $conn->setActive(true); - $this->_connection=$conn; - foreach($conn->schemaCachingExclude as $name) - $this->_cacheExclude[$name]=true; - } - - /** - * @return TDbConnection database connection. The connection is active. - */ - public function getDbConnection() - { - return $this->_connection; - } - - /** - * Obtains the metadata for the named table. - * @param string table name - * @return TDbTableSchema table metadata. Null if the named table does not exist. - */ - public function getTable($name) - { - if(isset($this->_tables[$name])) - return $this->_tables[$name]; - else if(!isset($this->_cacheExclude[$name]) && ($duration=$this->_connection->schemaCachingDuration)>0 && ($cache=prado::getApplication()->getCache())!==null) - { - $key='prado:dbschema'.$this->_connection->connectionString.':'.$this->_connection->username.':'.$name; - if(($table=$cache->get($key))===false) - { - $table=$this->createTable($name); - $cache->set($key,$table,$duration); - } - return $this->_tables[$name]=$table; - } - else - return $this->_tables[$name]=$this->createTable($name); - } - - /** - * Returns the metadata for all tables in the database. - * @param string 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 TDbTableSchema} (or its child class). - * The array keys are table names. - * @since 1.0.2 - */ - public function getTables($schema='') - { - $tables=array(); - foreach($this->getTableNames($schema) as $name) - $tables[$name]=$this->getTable($name); - return $tables; - } - - /** - * Returns all table names in the database. - * @param string 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. - * @since 1.0.2 - */ - public function getTableNames($schema='') - { - if(!isset($this->_tableNames[$schema])) - $this->_tableNames[$schema]=$this->findTableNames($schema); - return $this->_tableNames[$schema]; - } - - /** - * @return TDbCommandBuilder 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() - { - $this->_tables=array(); - $this->_builder=null; - } - - /** - * Quotes a table name for use in a query. - * @param string table name - * @return string the properly quoted table name - */ - public function quoteTableName($name) - { - return "'".$name."'"; - } - - /** - * Quotes a column name for use in a query. - * @param string column name - * @return string the properly quoted column name - */ - public function quoteColumnName($name) - { - return '"'.$name.'"'; - } - - /** - * Compares two table names. - * The table names can be either quoted or unquoted. This method - * will consider both cases. - * @param string table name 1 - * @param string 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); - return $name1===$name2; - } - - /** - * Creates a command builder for the database. - * This method may be overridden by child classes to create a DBMS-specific command builder. - * @return TDbCommandBuilder command builder instance - */ - protected function createCommandBuilder() - { - return new TDbCommandBuilder($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 implemenation simply throws an exception. - * @param string 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. - * @since 1.0.2 - */ - protected function findTableNames($schema='') - { - throw new TDbException('{0} does not support fetching all table names.', - get_class($this)); - } -} diff --git a/framework/Db/Schema/TDbTableSchema.php b/framework/Db/Schema/TDbTableSchema.php deleted file mode 100755 index eaeda1b4..00000000 --- a/framework/Db/Schema/TDbTableSchema.php +++ /dev/null @@ -1,76 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -/** - * TDbTableSchema 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. - * - * TDbTableSchema provides the following information about a table: - *
    - *
  • {@link name}
  • - *
  • {@link rawName}
  • - *
  • {@link columns}
  • - *
  • {@link primaryKey}
  • - *
  • {@link foreignKeys}
  • - *
  • {@link sequenceName}
  • - *
- * - * @author Qiang Xue - * @version $Id: TDbTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package system.db.schema - * @since 1.0 - */ -class TDbTableSchema extends TComponent -{ - /** - * @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 TDbColumnSchema 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 column name - * @return TDbColumnSchema 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/TMssqlColumnSchema.php b/framework/Db/Schema/mssql/TMssqlColumnSchema.php deleted file mode 100755 index 0c5c6ac4..00000000 --- a/framework/Db/Schema/mssql/TMssqlColumnSchema.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @author Christophe Boulain - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbColumnSchema'); - -/** - * TMssqlColumnSchema class describes the column meta data of a MSSQL table. - * - * @author Qiang Xue - * @author Christophe Boulain - * @version $Id: TMssqlColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.mssql - * @since 1.0.4 - */ -class TMssqlColumnSchema extends TDbColumnSchema -{ - /** - * Extracts the PHP type from DB type. - * @param string DB type - */ - protected function extractType($dbType) - { - if(strpos($dbType,'bigint')!==false || strpos($dbType,'float')!==false || strpos($dbType,'real')!==false) - $this->type='double'; - else if(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'; - } - - 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 the column's DB type - */ - protected function extractLimit($dbType) - { - } -} diff --git a/framework/Db/Schema/mssql/TMssqlCommandBuilder.php b/framework/Db/Schema/mssql/TMssqlCommandBuilder.php deleted file mode 100755 index f0b5c7f4..00000000 --- a/framework/Db/Schema/mssql/TMssqlCommandBuilder.php +++ /dev/null @@ -1,303 +0,0 @@ - - * @author Christophe Boulain - * @author Wei Zhuo - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using ('System.Db.schame.TDbCommandBuilder'); - -/** - * TMssqlCommandBuilder provides basic methods to create query commands for tables for Mssql Servers. - * - * @author Qiang Xue - * @author Christophe Boulain - * @author Wei Zhuo - * @version $Id: TMssqlCommandBuilder.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package system.db.schema.mssql - * @since 1.0.4 - */ -class TMssqlCommandBuilder extends TDbCommandBuilder -{ - /** - * Returns the last insertion ID for the specified table. - * Override parent implemantation since PDO mssql driver does not provide this method - * @param TDbTableSchema the table metadata - * @return mixed last insertion id. Null is returned if no sequence name. - */ - public function getLastInsertID($table) - { - if($table->sequenceName!==null) - return $this->getDbConnection()->createCommand('SELECT SCOPE_IDENTITY()')->queryScalar(); - else - return null; - } - - /** - * Creates a COUNT(*) command for a single table. - * Override parent implementation to remove the order clause of criteria if it exists - * @param TDbTableSchema the table metadata - * @param TDbCriteria the query criteria - * @return TDbCommand query command. - */ - public function createCountCommand($table,$criteria) - { - $criteria->order=''; - return parent::createCountCommand($table, $criteria); - } - - /** - * 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 TDbTableSchema the table metadata - * @param TDbCriteria the query criteria - * @return TDbCommand query command. - */ - public function createFindCommand($table,$criteria) - { - $criteria=$this->checkCriteria($table,$criteria); - return parent::createFindCommand($table,$criteria); - - } - - /** - * Creates an UPDATE command. - * Override parent implementation because mssql don't want to update an identity column - * @param TDbTableSchema the table metadata - * @param array list of columns to be updated (name=>value) - * @param TDbCriteria the query criteria - * @return TDbCommand update command. - */ - public function createUpdateCommand($table,$data,$criteria) - { - $criteria=$this->checkCriteria($table,$criteria); - $fields=array(); - $values=array(); - $bindByPosition=isset($criteria->params[0]); - foreach($data as $name=>$value) - { - if(($column=$table->getColumn($name))!==null) - { - if ($table->sequenceName !== null && $column->isPrimaryKey === true) continue; - if($value instanceof TDbExpression) - $fields[]=$column->rawName.'='.(string)$value; - else if($bindByPosition) - { - $fields[]=$column->rawName.'=?'; - $values[]=$column->typecast($value); - } - else - { - $fields[]=$column->rawName.'=:'.$name; - $values[':'.$name]=$column->typecast($value); - } - } - } - if($fields===array()) - throw new TDbException('No columns are being updated for table "{0}".', - $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 TDbTableSchema the table metadata - * @param TDbCriteria the query criteria - * @return TDbCommand 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 TDbTableSchema the table metadata - * @param TDbCriteria the query criteria - * @param array counters to be updated (counter increments/decrements indexed by column names.) - * @return TDbCommand 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 - * - * - * 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) - * - * - * Regular expressions are used to alter the SQL query. The resulting SQL query - * may be malformed for complex queries. The following restrictions apply - * - *
    - *
  • - * In particular, commas should NOT - * be used as part of the ordering expression or identifier. Commas must only be - * used for separating the ordering clauses. - *
  • - *
  • - * 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. - *
  • - *
  • - * No clauses should follow the ORDER BY clause, e.g. no COMPUTE or FOR clauses. - *
  • - * - * @param string SQL query string. - * @param integer maximum number of rows, -1 to ignore limit. - * @param integer row offset, -1 to ignore offset. - * @return string SQL with limit and offset. - * - * @author Wei Zhuo - */ - 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 query - * @param integer $limit > 0 - * @param integer $offset > 0 - * @return sql modified sql query applied with limit and offset. - * - * @author Wei Zhuo - */ - 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); - $reverseOrdering = $this->joinOrdering($this->reverseDirection($ordering)); - $sql = "SELECT * FROM (SELECT TOP {$limit} * FROM ($sql) as [__inner top table__] {$reverseOrdering}) as [__outer top table__] {$orginalOrdering}"; - return $sql; - } - - /** - * Base on simplified syntax http://msdn2.microsoft.com/en-us/library/aa259187(SQL.80).aspx - * - * @param string $sql - * @return array ordering expression as key and ordering direction as value - * - * @author Wei Zhuo - */ - 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) - { - $ordering[$subs[1][0]] = $subs[2][0]; - } - //else what? - } - else - $ordering[trim($part)] = 'ASC'; - } - } - return $ordering; - } - - /** - * @param array ordering obtained from findOrdering() - * @return string concat the orderings - * - * @author Wei Zhuo - */ - protected function joinOrdering($orders) - { - if(count($orders)>0) - { - $str=array(); - foreach($orders as $column => $direction) - $str[] = $column.' '.$direction; - return 'ORDER BY '.implode(', ', $str); - } - } - - /** - * @param array original ordering - * @return array ordering with reversed direction. - * - * @author Wei Zhuo - */ - 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 TMssqlTableSchema table schema - * @param TDbCriteria criteria - * @return TDbCrireria 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; - } -} diff --git a/framework/Db/Schema/mssql/TMssqlPdoAdapter.php b/framework/Db/Schema/mssql/TMssqlPdoAdapter.php deleted file mode 100755 index c9d4597f..00000000 --- a/framework/Db/Schema/mssql/TMssqlPdoAdapter.php +++ /dev/null @@ -1,74 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 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 - * @version $Id: TMssqlPdoAdapter.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package system.db.schema.mssql - * @since 1.0.4 - */ -class TMssqlPdoAdapter extends PDO -{ - /** - * Get the last inserted id value - * MSSQL doesn't support sequence, so, argument is ignored - * - * @param string sequence name. Defaults to null - * @return int last inserted id - */ - public function lastInsertId ($sequence=NULL) - { - return $this->query('SELECT SCOPE_IDENTITY()')->fetchColumn(); - } - - /** - * 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/TMssqlSchema.php b/framework/Db/Schema/mssql/TMssqlSchema.php deleted file mode 100755 index 063e8994..00000000 --- a/framework/Db/Schema/mssql/TMssqlSchema.php +++ /dev/null @@ -1,312 +0,0 @@ - - * @author Christophe Boulain - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbSchema'); - -/** - * TMssqlSchema is the class for retrieving metadata information from a MS SQL Server database. - * - * @author Qiang Xue - * @author Christophe Boulain - * @version $Id: TMssqlSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.mssql - * @since 1.0.4 - */ -class TMssqlSchema extends TDbSchema -{ - const DEFAULT_SCHEMA='dbo'; - - - /** - * Quotes a table name for use in a query. - * @param string table name - * @return string the properly quoted table name - */ - public function quoteTableName($name) - { - if (strpos($name,'.')===false) - return '['.$name.']'; - $names=explode('.',$name); - foreach ($names as &$n) - $n = '['.$n.']'; - return implode('.',$names); - } - - /** - * Quotes a column name for use in a query. - * @param string column name - * @return string the properly quoted column name - */ - public function quoteColumnName($name) - { - return '['.$name.']'; - } - - /** - * Compares two table names. - * The table names can be either quoted or unquoted. This method - * will consider both cases. - * @param string table name 1 - * @param string 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); - $name1=str_replace(array('[',']'),'',$name2); - return parent::compareTableNames(strtolower($name1),strtolower($name2)); - } - - /** - * Creates a table instance representing the metadata for the named table. - * @return CMysqlTableSchema driver dependent table metadata. Null if the table does not exist. - */ - protected function createTable($name) - { - $table=new TMssqlTableSchema; - $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 CMysqlTableSchema the table instance - * @param string 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 TMssqlTableSchema 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 = <<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 TMssqlTableSchema 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 = <<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 CMysqlTableSchema the table metadata - * @return boolean whether the table exists in the database - */ - protected function findColumns($table) - { - $where=array(); - $where[]="TABLE_NAME='".$table->name."'"; - if (isset($table->catalogName)) - $where[]="TABLE_CATALOG='".$table->catalogName."'"; - if (isset($table->schemaName)) - $where[]="TABLE_SCHEMA='".$table->schemaName."'"; - $sql="SELECT *, columnproperty(object_id(table_schema+'.'+table_name), column_name, 'IsIdentity') as IsIdentity ". - "FROM INFORMATION_SCHEMA.COLUMNS 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 ($column['IsIdentity']==1 && $table->sequenceName===null) - $table->sequenceName=''; - - } - return true; - } - - /** - * Creates a table column. - * @param array column metadata - * @return TDbColumnSchema normalized column metadata - */ - protected function createColumn($column) - { - $c=new TMssqlColumnSchema; - $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->init($column['DATA_TYPE'],$column['COLUMN_DEFAULT']); - return $c; - } - - /** - * Returns all table names in the database. - * @return array all table names in the database. - * @since 1.0.4 - */ - protected function findTableNames($schema='') - { - if($schema==='') - $schema=self::DEFAULT_SCHEMA; - $sql=<<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 TDbCommandBuilder command builder instance - */ - protected function createCommandBuilder() - { - return new TMssqlCommandBuilder($this); - } -} diff --git a/framework/Db/Schema/mssql/TMssqlTableSchema.php b/framework/Db/Schema/mssql/TMssqlTableSchema.php deleted file mode 100755 index ed1b7c8a..00000000 --- a/framework/Db/Schema/mssql/TMssqlTableSchema.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @author Christophe Boulain - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.TDbTableSchema'); - -/** - * TMssqlTableSchema represents the metadata for a MSSQL table. - * - * @author Qiang Xue - * @author Christophe Boulain - * @version $Id: TMssqlTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package system.Db.schema.mssql - * @since 1.0.4 - */ -class TMssqlTableSchema extends TDbTableSchema -{ - /** - * @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/TMysqlColumnSchema.php b/framework/Db/Schema/mysql/TMysqlColumnSchema.php deleted file mode 100755 index d3bb230c..00000000 --- a/framework/Db/Schema/mysql/TMysqlColumnSchema.php +++ /dev/null @@ -1,46 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbColumnSchema'); - -/** - * TMysqlColumnSchema class describes the column meta data of a MySQL table. - * - * @author Qiang Xue - * @version $Id: TMysqlColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.mysql - * @since 1.0 - */ -class TMysqlColumnSchema extends TDbColumnSchema -{ - /** - * Extracts the PHP type from DB type. - * @param string DB type - */ - protected function extractType($dbType) - { - if(strpos($dbType,'bigint')!==false || strpos($dbType,'float')!==false || strpos($dbType,'double')!==false) - $this->type='double'; - else if(strpos($dbType,'bool')!==false || $dbType==='tinyint(1)') - $this->type='boolean'; - else if(strpos($dbType,'int')!==false || strpos($dbType,'bit')!==false) - $this->type='integer'; - else - $this->type='string'; - } - - protected function extractDefault($defaultValue) - { - if($this->dbType==='timestamp' && $defaultValue==='CURRENT_TIMESTAMP') - $this->defaultValue=null; - else - parent::extractDefault($defaultValue); - } -} diff --git a/framework/Db/Schema/mysql/TMysqlSchema.php b/framework/Db/Schema/mysql/TMysqlSchema.php deleted file mode 100755 index 8061e7a8..00000000 --- a/framework/Db/Schema/mysql/TMysqlSchema.php +++ /dev/null @@ -1,205 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbSchema'); -prado::using('System.Db.Schema.mysql.TMysqlTableSchema'); -prado::using('System.Db.Schema.mysql.TMysqlColumnSchema'); - -/** - * TMysqlSchema is the class for retrieving metadata information from a MySQL database (version 4.1.x and 5.x). - * - * @author Qiang Xue - * @version $Id: TMysqlSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.mysql - * @since 1.0 - */ -class TMysqlSchema extends TDbSchema -{ - private $_tableNames; - private $_schemaNames; - - /** - * Quotes a table name for use in a query. - * @param string table name - * @return string the properly quoted table name - */ - public function quoteTableName($name) - { - return '`'.$name.'`'; - } - - /** - * Quotes a column name for use in a query. - * @param string column name - * @return string the properly quoted column name - */ - public function quoteColumnName($name) - { - return '`'.$name.'`'; - } - - /** - * Compares two table names. - * The table names can be either quoted or unquoted. This method - * will consider both cases. - * @param string table name 1 - * @param string 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)); - } - - /** - * Creates a table instance representing the metadata for the named table. - * @return TMysqlTableSchema driver dependent table metadata. Null if the table does not exist. - */ - protected function createTable($name) - { - $table=new TMysqlTableSchema; - $this->resolveTableNames($table,$name); - - if($this->findColumns($table)) - { - $this->findConstraints($table); - return $table; - } - else - return null; - } - - /** - * Generates various kinds of table names. - * @param TMysqlTableSchema the table instance - * @param string 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 TMysqlTableSchema 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(strpos(strtolower($column['Extra']),'auto_increment')!==false) - $table->sequenceName=''; - } - } - return true; - } - - /** - * Creates a table column. - * @param array column metadata - * @return TDbColumnSchema normalized column metadata - */ - protected function createColumn($column) - { - $c=new TMysqlColumnSchema; - $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']); - 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 TMysqlTableSchema 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; - } - $foreign = array(); - 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. - * @return array all table names in the database. - * @since 1.0.2 - */ - 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; - } -} diff --git a/framework/Db/Schema/mysql/TMysqlTableSchema.php b/framework/Db/Schema/mysql/TMysqlTableSchema.php deleted file mode 100755 index 8bed3314..00000000 --- a/framework/Db/Schema/mysql/TMysqlTableSchema.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ -prado::using('System.Db.Schema.TDbTableSchema'); -/** - * TMysqlTableSchema represents the metadata for a MySQL table. - * - * @author Qiang Xue - * @version $Id: TMysqlTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.mysql - * @since 1.0 - */ -class TMysqlTableSchema extends TDbTableSchema -{ - /** - * @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/TOciColumnSchema.php b/framework/Db/Schema/oci/TOciColumnSchema.php deleted file mode 100755 index d609d14d..00000000 --- a/framework/Db/Schema/oci/TOciColumnSchema.php +++ /dev/null @@ -1,56 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbColumnSchema'); - -/** - * TOciColumnSchema class describes the column meta data of a Oracle table. - * - * @author Ricardo Grana - * @version $Id: TOciColumnSchema.php - * @package System.Db.Schema.oci - * @since 1.0.5 - */ -class TOciColumnSchema extends TDbColumnSchema -{ - /** - * Extracts the PHP type from DB type. - * @param string DB type - */ - protected function extractOraType($dbType){ - if(strpos($dbType,'FLOAT')!==false) return 'double'; - - if ((strpos($dbType,'NUMBER')!==false) or - (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 'string'; - } - } - protected function extractType($dbType) - { - $this->type=$this->extractOraType($dbType); - } - - protected function extractDefault($defaultValue) - { - if(strpos($dbType,'timestamp')!==false) - $this->defaultValue=null; - else - parent::extractDefault($defaultValue); - } -} diff --git a/framework/Db/Schema/oci/TOciCommandBuilder.php b/framework/Db/Schema/oci/TOciCommandBuilder.php deleted file mode 100755 index 807b7271..00000000 --- a/framework/Db/Schema/oci/TOciCommandBuilder.php +++ /dev/null @@ -1,122 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbCommandBuilder'); - -/** - * TOciCommandBuilder provides basic methods to create query commands for tables. - * - * @author Ricardo Grana - * @version $Id: TOciCommandBuilder.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.oci - * @since 1.0.5 - */ -class TOciCommandBuilder extends TDbCommandBuilder -{ - /** - * @var integer the last insertion ID - */ - public $returnID; - - /** - * Returns the last insertion ID for the specified table. - * @param mixed the table schema ({@link TDbTableSchema}) 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 query string without LIMIT and OFFSET. - * @param integer maximum number of rows, -1 to ignore limit. - * @param integer 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 = <<column value). If a key is not a valid column name, the corresponding value will be ignored. - * @return TDbCommand insert command - */ - public function createInsertCommand($table,$data) - { - $this->ensureTable($table); - $fields=array(); - $values=array(); - $placeholders=array(); - foreach($data as $name=>$value) - { - if(($column=$table->getColumn($name))!==null && ($value!==null || $column->allowNull)) - { - $fields[]=$column->rawName; - if($value instanceof TDbExpression) - $placeholders[]=(string)$value; - else - { - $placeholders[]=':'.$name; - $values[':'.$name]=$column->typecast($value); - } - } - } - - $sql="INSERT INTO {$table->rawName} (".implode(', ',$fields).') VALUES ('.implode(', ',$placeholders).')'; - - if(is_string($table->primaryKey)) - { - $sql.=" RETURNING ".$table->primaryKey." 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; - } -} diff --git a/framework/Db/Schema/oci/TOciSchema.php b/framework/Db/Schema/oci/TOciSchema.php deleted file mode 100755 index 50de0626..00000000 --- a/framework/Db/Schema/oci/TOciSchema.php +++ /dev/null @@ -1,278 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbSchema'); - -/** - * TOciSchema is the class for retrieving metadata information from a PostgreSQL database. - * - * @author Ricardo Grana - * @version $Id: TOciSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.oci - * @since 1.0.5 - */ -class TOciSchema extends TDbSchema -{ - private $_defaultSchema = ''; - private $_sequences=array(); - - /** - * Quotes a table name for use in a query. - * @param string table name - * @return string the properly quoted table name - */ - public function quoteTableName($name) - { - return $name; - } - - /** - * Quotes a column name for use in a query. - * @param string column name - * @return string the properly quoted column name - */ - public function quoteColumnName($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 TDbCommandBuilder command builder instance - */ - protected function createCommandBuilder() - { - return new TOciCommandBuilder($this); - } - - /** - * @param string 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 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]); - } - - /** - * Creates a table instance representing the metadata for the named table. - * @return TDbTableSchema driver dependent table metadata. - */ - protected function createTable($name) - { - $table=new TOciTableSchema; - $this->resolveTableNames($table,$name); - - if(!$this->findColumns($table)) - return null; - $this->findConstraints($table); - - return $table; - } - - /** - * Generates various kinds of table names. - * @param TOciTableSchema the table instance - * @param string 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 TOciTableSchema the table metadata - * @return boolean whether the table exists in the database - */ - protected function findColumns($table) - { - list($schemaName,$tableName) = $this->getSchemaTableName($table->name); - - $sql=<< 0 then ',' || a.data_scale else '' end - || ')' - when data_type = 'DATE' 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' - 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; - } - } - return true; - } - - /** - * Creates a table column. - * @param array column metadata - * @return TDbColumnSchema normalized column metadata - */ - protected function createColumn($column) - { - $c=new TOciColumnSchema; - $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 TOciTableSchema the table metadata - */ - protected function findConstraints($table) - { - $sql=<< '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"], array($row["COLUMN_REF"])); - if(isset($table->columns[$name])) - $table->columns[$name]->isForeignKey=true; - } - - } - } - - - /** - * Returns all table names in the database. - * @return array all table names in the database. - */ - protected function findTableNames($schema='') - { - if($schema==='') - { - $sql=<<getDbConnection()->createCommand($sql); - } - else - { - $sql=<<getDbConnection()->createCommand($sql); - $command->bindParam(':schema',$schema); - } - - $rows=$command->queryAll(); - $names=array(); - foreach($rows as $row) - { - if($schema===$this->getDefaultSchema()) - $names[]=$row['table_name']; - else - $names[]=$row['schema_name'].'.'.$row['table_name']; - } - return $names; - } -} diff --git a/framework/Db/Schema/oci/TOciTableSchema.php b/framework/Db/Schema/oci/TOciTableSchema.php deleted file mode 100755 index 9fe73624..00000000 --- a/framework/Db/Schema/oci/TOciTableSchema.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbTableSchame'); - -/** - * TOciTableSchema represents the metadata for a Ora table. - * - * @author Ricardo Grana - * @version $Id: TOciTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.oci - * @since 1.0.5 - */ -class TOciTableSchema extends TDbTableSchema -{ - /** - * @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/TPgsqlColumnSchema.php b/framework/Db/Schema/pgsql/TPgsqlColumnSchema.php deleted file mode 100755 index 3677cd44..00000000 --- a/framework/Db/Schema/pgsql/TPgsqlColumnSchema.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbColumnSchema'); - -/** - * TPgsqlColumnSchema class describes the column meta data of a PostgreSQL table. - * - * @author Qiang Xue - * @version $Id: TPgsqlColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.pgsql - * @since 1.0 - */ -class TPgsqlColumnSchema extends TDbColumnSchema -{ - /** - * Extracts the PHP type from DB type. - * @param string DB type - */ - protected function extractType($dbType) - { - if(strpos($dbType,'integer')!==false || strpos($dbType,'oid')===0) - $this->type='integer'; - else if(strpos($dbType,'bool')!==false) - $this->type='boolean'; - else if(preg_match('/(real|float|double)/',$dbType)) - $this->type='double'; - else - $this->type='string'; - } - - /** - * Extracts the default value for the column. - * The value is typecasted to correct PHP type. - * @param mixed 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/TPgsqlSchema.php b/framework/Db/Schema/pgsql/TPgsqlSchema.php deleted file mode 100755 index 5f009208..00000000 --- a/framework/Db/Schema/pgsql/TPgsqlSchema.php +++ /dev/null @@ -1,284 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbSchema'); - -/** - * TPgsqlSchema is the class for retrieving metadata information from a PostgreSQL database. - * - * @author Qiang Xue - * @version $Id: TPgsqlSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.pgsql - * @since 1.0 - */ -class TPgsqlSchema extends TDbSchema -{ - const DEFAULT_SCHEMA='public'; - private $_sequences=array(); - - /** - * Quotes a table name for use in a query. - * @param string table name - * @return string the properly quoted table name - */ - public function quoteTableName($name) - { - return '"'.$name.'"'; - } - - /** - * Creates a table instance representing the metadata for the named table. - * @return TDbTableSchema driver dependent table metadata. - */ - protected function createTable($name) - { - $table=new TPgsqlTableSchema; - $this->resolveTableNames($table,$name); - if(!$this->findColumns($table)) - return null; - $this->findConstraints($table); - - if(is_string($table->primaryKey) && isset($this->_sequences[$table->primaryKey])) - $table->sequenceName=$this->_sequences[$table->primaryKey]; - - return $table; - } - - /** - * Generates various kinds of table names. - * @param TPgsqlTableSchema the table instance - * @param string 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 TPgsqlTableSchema the table metadata - * @return boolean whether the table exists in the database - */ - protected function findColumns($table) - { - $sql=<< 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[$c->name]=$matches[1]; - else - $this->_sequences[$c->name]=$table->schemaName.'.'.$matches[1]; - } - } - return true; - } - - /** - * Creates a table column. - * @param array column metadata - * @return TDbColumnSchema normalized column metadata - */ - protected function createColumn($column) - { - $c=new TPgsqlColumnSchema; - $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 TPgsqlTableSchema the table metadata - */ - protected function findConstraints($table) - { - $sql=<<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 TPgsqlTableSchema the table metadata - * @param string pgsql primary key index list - */ - protected function findPrimaryKey($table,$indices) - { - $indices=implode(', ',preg_split('/\s+/',$indices)); - $sql=<<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 TPgsqlTableSchema the table metadata - * @param string 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. - * @return array all table names in the database. - * @since 1.0.2 - */ - protected function findTableNames($schema='') - { - if($schema==='') - $schema=self::DEFAULT_SCHEMA; - $sql=<<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['schema_name'].'.'.$row['table_name']; - } - return $names; - } -} diff --git a/framework/Db/Schema/pgsql/TPgsqlTableSchema.php b/framework/Db/Schema/pgsql/TPgsqlTableSchema.php deleted file mode 100755 index 9c14da28..00000000 --- a/framework/Db/Schema/pgsql/TPgsqlTableSchema.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbTableSchema'); - -/** - * TPgsqlTable represents the metadata for a PostgreSQL table. - * - * @author Qiang Xue - * @version $Id: TPgsqlTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.pgsql - * @since 1.0 - */ -class TPgsqlTableSchema extends TDbTableSchema -{ - /** - * @var string name of the schema that this table belongs to. - */ - public $schemaName; -} diff --git a/framework/Db/Schema/sqlite/TSqliteColumnSchema.php b/framework/Db/Schema/sqlite/TSqliteColumnSchema.php deleted file mode 100755 index ca6858eb..00000000 --- a/framework/Db/Schema/sqlite/TSqliteColumnSchema.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbColumnSchema'); - -/** - * TSqliteColumnSchema class describes the column meta data of a SQLite table. - * - * @author Qiang Xue - * @version $Id: TSqliteColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.sqlite - * @since 1.0 - */ -class TSqliteColumnSchema extends TDbColumnSchema -{ - /** - * Extracts the default value for the column. - * The value is typecasted to correct PHP type. - * @param mixed 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($defaultValue); - } -} diff --git a/framework/Db/Schema/sqlite/TSqliteCommandBuilder.php b/framework/Db/Schema/sqlite/TSqliteCommandBuilder.php deleted file mode 100755 index 69ffd8a8..00000000 --- a/framework/Db/Schema/sqlite/TSqliteCommandBuilder.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbCommandBuilder'); - -/** - * TSqliteCommandBuilder provides basic methods to create query commands for SQLite tables. - * - * @author Qiang Xue - * @version $Id: TSqliteCommandBuilder.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.sqlite - * @since 1.0 - */ -class TSqliteCommandBuilder extends TDbCommandBuilder -{ - /** - * 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 TDbTableSchema the table schema - * @param array list of primary key values to be selected within - * @param string column prefix (ended with dot) - * @return string the expression for selection - * @since 1.0.4 - */ - 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/TSqliteSchema.php b/framework/Db/Schema/sqlite/TSqliteSchema.php deleted file mode 100755 index 0e3d4f9a..00000000 --- a/framework/Db/Schema/sqlite/TSqliteSchema.php +++ /dev/null @@ -1,134 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using('System.Db.Schema.TDbSchema'); -prado::using('System.Db.Schema.TDbTableSchema'); -prado::using('System.Db.Schema.sqlite.TSqliteColumnSchema'); -prado::using('System.Db.Schema.sqlite.TSqliteCommandBuilder'); - -/** - * TSqliteSchema is the class for retrieving metadata information from a SQLite (2/3) database. - * - * @author Qiang Xue - * @version $Id: TSqliteSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ - * @package System.Db.Schema.sqlite - * @since 1.0 - */ -class TSqliteSchema extends TDbSchema -{ - /** - * Returns all table names in the database. - * @param string the schema of the tables. This is not used for sqlite database. - * @return array all table names in the database. - * @since 1.0.2 - */ - 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 TSqliteCommandBuilder command builder instance - */ - protected function createCommandBuilder() - { - return new TSqliteCommandBuilder($this); - } - - /** - * Creates a table instance representing the metadata for the named table. - * @return TDbTableSchema driver dependent table metadata. Null if the table does not exist. - */ - protected function createTable($name) - { - $db=$this->getDbConnection(); - - $table=new TDbTableSchema; - $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 TDbTableSchema 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=''; - - return true; - } - - /** - * Collects the foreign key column details for the given table. - * @param TDbTableSchema 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 metadata - * @return TDbColumnSchema normalized column metadata - */ - protected function createColumn($column) - { - $c=new TSqliteColumnSchema; - $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; - } -} diff --git a/framework/Db/TDataSourceConfig.php b/framework/Db/TDataSourceConfig.php deleted file mode 100755 index 56d87bc0..00000000 --- a/framework/Db/TDataSourceConfig.php +++ /dev/null @@ -1,168 +0,0 @@ - - * @author Christophe Boulain configuration for database connections. - * - * Example usage: mysql connection - * - * - * - * - * - * - * - * - * Usage in php: - * - * class Home extends TPage - * { - * function onLoad($param) - * { - * $db = $this->Application->Modules['db1']->DbConnection; - * $db->createCommand('...'); //... - * } - * } - * - * - * The properties of are those of the class TDbConnection. - * Set {@link setConnectionClass} attribute for a custom database connection class - * that extends the TDbConnection class. - * - * @author Wei Zhuo - * @version $Id$ - * @package System.Data - * @since 3.1 - */ -class TDataSourceConfig extends TModule -{ - private $_connID=''; - private $_conn; - private $_connClass='System.Db.TDbConnection'; - - /** - * Initalize the database connection properties from attributes in tag. - * @param TXmlDocument xml configuration. - */ - public function init($xml) - { - if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) - { - if(isset($xml['database']) && is_array($xml['database'])) - { - $db=$this->getDbConnection(); - foreach($xml['database'] as $name=>$value) - $db->setSubProperty($name,$value); - } - } - else - { - if($prop=$xml->getElementByTagName('database')) - { - $db=$this->getDbConnection(); - foreach($prop->getAttributes() as $name=>$value) - $db->setSubproperty($name,$value); - } - } - } - - /** - * The module ID of another TDataSourceConfig. The {@link getDbConnection DbConnection} - * property of this configuration will equal to {@link getDbConnection DbConnection} - * of the given TDataSourceConfig module. - * @param string module ID. - */ - public function setConnectionID($value) - { - $this->_connID=$value; - } - - /** - * @return string connection module ID. - */ - public function getConnectionID() - { - return $this->_connID; - } - - /** - * Gets the TDbConnection from another module if {@link setConnectionID ConnectionID} - * is supplied and valid. Otherwise, a connection of type given by - * {@link setConnectionClass ConnectionClass} is created. - * @return TDbConnection database connection. - */ - public function getDbConnection() - { - if($this->_conn===null) - { - if($this->_connID!=='') - $this->_conn = $this->findConnectionByID($this->getConnectionID()); - else - $this->_conn = Prado::createComponent($this->getConnectionClass()); - } - return $this->_conn; - } - - /** - * Alias for getDbConnection(). - * @return TDbConnection database connection. - */ - public function getDatabase() - { - return $this->getDbConnection(); - } - - /** - * @param string Database connection class name to be created. - */ - public function getConnectionClass() - { - return $this->_connClass; - } - - /** - * The database connection class name to be created when {@link getDbConnection} - * method is called and {@link setConnectionID ConnectionID} is null. The - * {@link setConnectionClass ConnectionClass} property must be set before - * calling {@link getDbConnection} if you wish to create the connection using the - * given class name. - * @param string Database connection class name. - * @throws TConfigurationException when database connection is already established. - */ - public function setConnectionClass($value) - { - if($this->_conn!==null) - throw new TConfigurationException('datasource_dbconnection_exists', $value); - $this->_connClass=$value; - } - - /** - * Finds the database connection instance from the Application modules. - * @param string Database connection module ID. - * @return TDbConnection database connection. - * @throws TConfigurationException when module is not of TDbConnection or TDataSourceConfig. - */ - protected function findConnectionByID($id) - { - $conn = $this->getApplication()->getModule($id); - if($conn instanceof TDbConnection) - return $conn; - else if($conn instanceof TDataSourceConfig) - return $conn->getDbConnection(); - else - throw new TConfigurationException('datasource_dbconnection_invalid',$id); - } -} diff --git a/framework/Db/TDbCommand.php b/framework/Db/TDbCommand.php deleted file mode 100755 index 6c3a0074..00000000 --- a/framework/Db/TDbCommand.php +++ /dev/null @@ -1,340 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -/** - * TDbCommand represents an SQL statement to execute against a database. - * - * It is usually created by calling {@link TDbConnection::createCommand}. - * The SQL statement to be executed may be set via {@link setText Text}. - * - * To execute a non-query SQL (such as insert, delete, update), call - * {@link execute}. To execute an SQL statement that returns result data set - * (such as SELECT), use {@link query} or its convenient versions {@link queryRow}, - * {@link queryColumn}, or {@link queryScalar}. - * - * If an SQL statement returns results (such as a SELECT SQL), the results - * can be accessed via the returned {@link TDbDataReader}. - * - * TDbCommand supports SQL statment preparation and parameter binding. - * Call {@link bindParam} to bind a PHP variable to a parameter in SQL. - * Call {@link bindValue} to bind a value to an SQL parameter. - * When binding a parameter, the SQL statement is automatically prepared. - * You may also call {@link prepare} to explicitly prepare an SQL statement. - * - * @author Qiang Xue - * @version $Id$ - * @package system.db - * @since 1.0 - */ -class TDbCommand extends TComponent -{ - private $_connection; - private $_text=''; - private $_statement=null; - private $_params; - - /** - * Constructor. - * @param TDbConnection the database connection - * @param string the SQL statement to be executed - */ - public function __construct(TDbConnection $connection,$text) - { - $this->_connection=$connection; - $this->setText($text); - } - - /** - * Set the statement to null when serializing. - */ - public function __sleep() - { - $this->_statement=null; - return array_keys(get_object_vars($this)); - } - - /** - * @return string the SQL statement to be executed - */ - public function getText() - { - return $this->_text; - } - - /** - * Specifies the SQL statement to be executed. - * Any previous execution will be terminated or cancel. - * @param string the SQL statement to be executed - */ - public function setText($value) - { - $this->_text=$value; - $this->cancel(); - } - - /** - * @return TDbConnection the connection associated with this command - */ - public function getConnection() - { - return $this->_connection; - } - - /** - * @return PDOStatement the underlying PDOStatement for this command - * It could be null if the statement is not prepared yet. - */ - public function getPdoStatement() - { - return $this->_statement; - } - - /** - * Prepares the SQL statement to be executed. - * For complex SQL statement that is to be executed multiple times, - * this may improve performance. - * For SQL statement with binding parameters, this method is invoked - * automatically. - */ - public function prepare() - { - if($this->_statement==null) - { - try - { - $this->_statement=$this->getConnection()->getPdoInstance()->prepare($this->getText()); - $this->_params=array(); - } - catch(Exception $e) - { - Prado::log('Error in preparing SQL: '.$this->getText(),TLogger::ERROR,'system.Db.TDbCommand'); - throw new TDbException('TDbCommand failed to prepare the SQL statement: {0}', - $e->getMessage()); - } - } - } - - /** - * Cancels the execution of the SQL statement. - */ - public function cancel() - { - $this->_statement=null; - } - - /** - * Binds a parameter to the SQL statement to be executed. - * @param mixed Parameter identifier. For a prepared statement - * using named placeholders, this will be a parameter name of - * the form :name. For a prepared statement using question mark - * placeholders, this will be the 1-indexed position of the parameter. - * @param mixed Name of the PHP variable to bind to the SQL statement parameter - * @param int SQL data type of the parameter. If null, the type is determined by the PHP type of the value. - * @param int length of the data type - * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php - */ - public function bindParameter($name, &$value, $dataType=null, $length=null) - { - $this->prepare(); - if($dataType===null) - $this->_statement->bindParam($name,$value,$this->_connection->getPdoType(gettype($value))); - else if($length===null) - $this->_statement->bindParam($name,$value,$dataType); - else - $this->_statement->bindParam($name,$value,$dataType,$length); - if($this->_connection->enableParamLogging) - $this->_params[]=$name.'=['.gettype($value).']'; - } - - - /** - * Binds a value to a parameter. - * @param mixed Parameter identifier. For a prepared statement - * using named placeholders, this will be a parameter name of - * the form :name. For a prepared statement using question mark - * placeholders, this will be the 1-indexed position of the parameter. - * @param mixed The value to bind to the parameter - * @param int SQL data type of the parameter. If null, the type is determined by the PHP type of the value. - * @see http://www.php.net/manual/en/function.PDOStatement-bindValue.php - */ - public function bindValue($name, $value, $dataType=null) - { - $this->prepare(); - if($dataType===null) - $this->_statement->bindValue($name,$value,$this->_connection->getPdoType(gettype($value))); - else - $this->_statement->bindValue($name,$value,$dataType); - if($this->_connection->enableParamLogging) - $this->_params[]=$name.'='.var_export($value,true); - } - - /** - * Executes the SQL statement. - * This method is meant only for executing non-query SQL statement. - * No result set will be returned. - * @return integer number of rows affected by the execution. - * @throws CException execution failed - */ - public function execute() - { - $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; - Prado::trace('Executing SQL: '.$this->getText().$params,'system.Db.TDbCommand'); - try - { - /*if($this->_connection->enableProfiling) - Yii::beginProfile('system.db.TDbCommand.execute('.$this->getText().')','system.db.TDbCommand.execute'); - */ - if($this->_statement instanceof PDOStatement) - { - $this->_statement->execute(); - $n=$this->_statement->rowCount(); - } - else - $n=$this->getConnection()->getPdoInstance()->exec($this->getText()); - - /*if($this->_connection->enableProfiling) - Yii::endProfile('system.db.TDbCommand.execute('.$this->getText().')','system.db.TDbCommand.execute'); - */ - return $n; - } - catch(Exception $e) - { - /*if($this->_connection->enableProfiling) - Yii::endProfile('system.db.TDbCommand.execute('.$this->getText().')','system.db.TDbCommand.execute'); - */ - Prado::log('Error in executing SQL: '.$this->getText().$params,TLogger::ERROR,'system.db.TDbCommand'); - throw new TDbException('TDbCommand failed to execute the SQL statement: {0}', - $e->getMessage()); - } - } - - /** - * @return String prepared SQL text for debugging purposes. - */ - public function getDebugStatementText() - { - if(Prado::getApplication()->getMode() === TApplicationMode::Debug) - { - $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; - return $this->_statement instanceof PDOStatement ? - $this->_statement->queryString.$params - : $this->getText().$params; - - } - } - - /** - * Executes the SQL statement and returns query result. - * This method is for executing an SQL query that returns result set. - * @return TDbDataReader the reader object for fetching the query result - * @throws CException execution failed - */ - public function query() - { - return $this->queryInternal('',0); - } - - /** - * Executes the SQL statement and returns all rows. - * @param boolean whether each row should be returned as an associated array with - * column names as the keys or the array keys are column indexes (0-based). - * @return array all rows of the query result. Each array element is an array representing a row. - * An empty array is returned if the query results in nothing. - * @throws CException execution failed - */ - public function queryAll($fetchAssociative=true) - { - return $this->queryInternal('fetchAll',$fetchAssociative ? PDO::FETCH_ASSOC : PDO::FETCH_NUM); - } - - /** - * Executes the SQL statement and returns the first row of the result. - * This is a convenient method of {@link query} when only the first row of data is needed. - * @param boolean whether the row should be returned as an associated array with - * column names as the keys or the array keys are column indexes (0-based). - * @return array the first row of the query result, false if no result. - * @throws CException execution failed - */ - public function queryRow($fetchAssociative=true) - { - return $this->queryInternal('fetch',$fetchAssociative ? PDO::FETCH_ASSOC : PDO::FETCH_NUM); - } - - /** - * Executes the SQL statement and returns the value of the first column in the first row of data. - * This is a convenient method of {@link query} when only a single scalar - * value is needed (e.g. obtaining the count of the records). - * @return mixed the value of the first column in the first row of the query result. False is returned if there is no value. - * @throws CException execution failed - */ - public function queryScalar() - { - $result=$this->queryInternal('fetchColumn',0); - if(is_resource($result) && get_resource_type($result)==='stream') - return stream_get_contents($result); - else - return $result; - } - - /** - * Executes the SQL statement and returns the first column of the result. - * This is a convenient method of {@link query} when only the first column of data is needed. - * Note, the column returned will contain the first element in each row of result. - * @return array the first column of the query result. Empty array if no result. - * @throws CException execution failed - */ - public function queryColumn() - { - return $this->queryInternal('fetchAll',PDO::FETCH_COLUMN); - } - - /** - * @param string method of PDOStatement to be called - * @param mixed the first parameter to be passed to the method - * @return mixed the method execution result - */ - private function queryInternal($method,$mode) - { - $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; - Prado::trace('Querying SQL: '.$this->getText().$params,'system.Db.TDbCommand'); - try - { - /*if($this->_connection->enableProfiling) - Yii::beginProfile('system.db.TDbCommand.query('.$this->getText().')','system.db.TDbCommand.query'); - */ - if($this->_statement instanceof PDOStatement) - $this->_statement->execute(); - else - $this->_statement=$this->getConnection()->getPdoInstance()->query($this->getText()); - - if($method==='') - $result=new TDbDataReader($this); - else - { - $result=$this->_statement->{$method}($mode); - $this->_statement->closeCursor(); - } - - /*if($this->_connection->enableProfiling) - Yii::endProfile('system.db.TDbCommand.query('.$this->getText().')','system.db.TDbCommand.query'); - */ - return $result; - } - catch(Exception $e) - { -/* if($this->_connection->enableProfiling) - Yii::endProfile('system.db.TDbCommand.query('.$this->getText().')','system.db.TDbCommand.query'); - */ - Prado::log('Error in querying SQL: '.$this->getText().$params,TLogger::ERROR,'system.Db.TDbCommand'); - throw new TDbException('TDbCommand failed to execute the SQL statement: {0}', - $e->getMessage()); - } - } -} diff --git a/framework/Db/TDbConnection.php b/framework/Db/TDbConnection.php deleted file mode 100755 index d377b552..00000000 --- a/framework/Db/TDbConnection.php +++ /dev/null @@ -1,759 +0,0 @@ - - * @author Christophe Boulain - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -prado::using ('System.Db.TDbCommand'); -prado::using ('System.Db.TDbTransaction'); - -/** - * TDbConnection represents a connection to a database. - * - * This is a port of {@link http://www.yiiframework.com Yii} {@link http://www.yiiframework.com/ CDbConnection} - * - * TDbConnection works together with {@link TDbCommand}, {@link TDbDataReader} - * and {@link TDbTransaction} 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 TDbConnection instance and establish - * the actual connection: - *
    - * $connection=new TDbConnection($dsn,$username,$password);
    - * $connection->active=true;
    - * 
    - * - * After the DB connection is established, one can execute an SQL statement like the following: - *
    - * $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) ...
    - * 
    - * - * One can do prepared SQL execution and bind parameters to the prepared SQL: - *
    - * $command=$connection->createCommand($sqlStatement);
    - * $command->bindParam($name1,$value1);
    - * $command->bindParam($name2,$value2);
    - * $command->execute();
    - * 
    - * - * To use transaction, do like the following: - *
    - * $transaction=$connection->beginTransaction();
    - * try
    - * {
    - *    $connection->createCommand($sql1)->execute();
    - *    $connection->createCommand($sql2)->execute();
    - *    //.... other SQL executions
    - *    $transaction->commit();
    - * }
    - * catch(Exception $e)
    - * {
    - *    $transaction->rollBack();
    - * }
    - * 
    - * - * TDbConnection also provides a set of methods to support setting and querying - * of certain DBMS attributes, such as {@link getNullConversion nullConversion}. - * - * @author Qiang Xue - * @author Christophe.Boulain - * @version $Id$ - * @package System.Db - * @since 3.2 - */ -class TDbConnection extends TComponent -{ - /** - * @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 - */ - 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 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 TDbConnection 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. - */ - 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. - */ - public $emulatePrepare=false; - /** - * @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; - - 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 The Data Source Name, or DSN, contains the information required to connect to the database. - * @param string The user name for the DSN string. - * @param string 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. - */ - public function __sleep() - { - $this->close(); - return array_keys(get_object_vars($this)); - } - - /** - * @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 TDbConnection 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() - { - if($this->autoConnect) - $this->setActive(true); - } - - /** - * @return boolean whether the DB connection is established - */ - public function getActive() - { - return $this->_active; - } - - /** - * Open or close the DB connection. - * @param boolean 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(); - } - } - - /** - * 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 TDbException('TDbConnection.connectionString cannot be empty.'); - try - { - Prado::trace('Opening DB connection','system.Db.TDbConnection'); - $this->_pdo=$this->createPdoInstance(); - $this->initConnection($this->_pdo); - $this->_active=true; - } - catch(PDOException $e) - { - throw new TDbException('TDbConnection failed to open the DB connection: {0}',$e->getMessage()); - } - } - } - - /** - * Closes the currently active DB connection. - * It does nothing if the connection is already closed. - */ - protected function close() - { - Prado::trace('Closing DB connection','system.Db.TDbConnection'); - $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 - * @since 1.0.4 - */ - protected function createPdoInstance() - { - $pdoClass='PDO'; - if(($pos=strpos($this->connectionString,':'))!==false) - { - $driver=strtolower(substr($this->connectionString,0,$pos)); - if($driver==='mssql' || $driver==='dblib') - { - prado::using('System.Db.Schema.mssql.TMssqlPdoAdapter'); - $pdoClass='TMssqlPdoAdapter'; - } - } - 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 the PDO instance - */ - protected function initConnection($pdo) - { - $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - if($this->emulatePrepare && constant('PDO::ATTR_EMULATE_PREPARES')) - $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,true); - if($this->charset!==null) - { - switch ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)) - { - case 'mysql': - $stmt = $pdo->prepare('SET CHARACTER SET ?'); - break; - case 'pgsql': - $stmt = $pdo->prepare('SET client_encoding TO ?'); - break; - case 'sqlite': - $stmt = $pdo->prepare ('SET NAMES ?'); - break; - } - $stmt->execute(array($this->charset)); - } - } - - /** - * @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 string SQL statement associated with the new command. - * @return TDbCommand the DB command - * @throws CException if the connection is not active - */ - public function createCommand($sql) - { - if($this->getActive()) - return new TDbCommand($this,$sql); - else - throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); - } - - /** - * @return TDbTransaction 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 TDbTransaction the transaction initiated - * @throws CException if the connection is not active - */ - public function beginTransaction() - { - if($this->getActive()) - { - $this->_pdo->beginTransaction(); - return $this->_transaction=new TDbTransaction($this); - } - else - throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); - } - - /** - * @return TDbSchema the database schema for the current connection - * @throws CException if the connection is not active yet - */ - public function getSchema() - { - if($this->_schema!==null) - return $this->_schema; - else - { - if(!$this->getActive()) - throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); - $driver=$this->getDriverName(); - switch(strtolower($driver)) - { - case 'pgsql': // PostgreSQL - prado::using('System.Db.Schema.pgsql.TPgsqlSchema'); - return $this->_schema=new TPgsqlSchema($this); - case 'mysqli': // MySQL - case 'mysql': - prado::using('System.Db.Schema.mysql.TMysqlSchema'); - return $this->_schema=new TMysqlSchema($this); - case 'sqlite': // sqlite 3 - case 'sqlite2': // sqlite 2 - prado::using('System.Db.Schema.sqlite.TSqliteSchema'); - return $this->_schema=new TSqliteSchema($this); - case 'mssql': // Mssql driver on windows hosts - case 'dblib': // dblib drivers on linux (and maybe others os) hosts - prado::using('System.Db.Schema.mssql.TMssqlSchema'); - return $this->_schema=new TMssqlSchema($this); - case 'oci': // Oracle driver - prado::using('System.Db.Schema.oci.TOciSchema'); - return $this->_schema=new TOciSchema($this); - case 'ibm': - default: - throw new TDbException('TDbConnection does not support reading schema for {0} database.', - $driver); - } - } - } - - /** - * Returns the SQL command builder for the current DB connection. - * @return TDbCommandBuilder the command builder - */ - public function getCommandBuilder() - { - return $this->getSchema()->getCommandBuilder(); - } - - /** - * Returns the ID of the last inserted row or sequence value. - * @param string 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='') - { - if($this->getActive()) - return $this->_pdo->lastInsertId($sequenceName); - else - throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); - } - - /** - * Quotes a string value for use in a query. - * @param string string to be quoted - * @return string the properly quoted string - * @see http://www.php.net/manual/en/function.PDO-quote.php - */ - public function quoteString($str) - { - if($this->getActive()) - return $this->_pdo->quote($str); - else - throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); - } - - /** - * Quotes a table name for use in a query. - * @param string 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. - * @param string 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 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; - } - - /** - * @return TDbColumnCaseMode the case of the column names - */ - public function getColumnCase() - { - switch($this->getAttribute(PDO::ATTR_CASE)) - { - case PDO::CASE_NATURAL: - return TDbColumnCaseMode::Preserved; - case PDO::CASE_LOWER: - return TDbColumnCaseMode::LowerCase; - case PDO::CASE_UPPER: - return TDbColumnCaseMode::UpperCase; - } - } - - /** - * @param TDbColumnCaseMode the case of the column names - */ - public function setColumnCase($value) - { - switch(TPropertyValue::ensureEnum($value,'TDbColumnCaseMode')) - { - case TDbColumnCaseMode::Preserved: - $value=PDO::CASE_NATURAL; - break; - case TDbColumnCaseMode::LowerCase: - $value=PDO::CASE_LOWER; - break; - case TDbColumnCaseMode::UpperCase: - $value=PDO::CASE_UPPER; - break; - } - $this->setAttribute(PDO::ATTR_CASE,$value); - } - /** - * @return TDbNullConversionMode how the null and empty strings are converted - */ - public function getNullConversion() - { - switch($this->getAttribute(PDO::ATTR_ORACLE_NULLS)) - { - case PDO::NULL_NATURAL: - return TDbNullConversionMode::Preserved; - case PDO::NULL_EMPTY_STRING: - return TDbNullConversionMode::EmptyStringToNull; - case PDO::NULL_TO_STRING: - return TDbNullConversionMode::NullToEmptyString; - } - } - - /** - * @param TDbNullConversionMode how the null and empty strings are converted - */ - public function setNullConversion($value) - { - switch(TPropertyValue::ensureEnum($value,'TDbNullConversionMode')) - { - case TDbNullConversionMode::Preserved: - $value=PDO::NULL_NATURAL; - break; - case TDbNullConversionMode::EmptyStringToNull: - $value=PDO::NULL_EMPTY_STRING; - break; - case TDbNullConversionMode::NullToEmptyString: - $value=PDO::NULL_TO_STRING; - break; - } - $this->setAttribute(PDO::ATTR_ORACLE_NULLS,$value); - } - - /** - * @return boolean whether creating or updating a DB record will be automatically committed. - * Some DBMS (such as sqlite) may not support this feature. - */ - public function getAutoCommit() - { - return $this->getAttribute(PDO::ATTR_AUTOCOMMIT); - } - - /** - * @param boolean whether creating or updating a DB record will be automatically committed. - * Some DBMS (such as sqlite) may not support this feature. - */ - public function setAutoCommit($value) - { - $this->setAttribute(PDO::ATTR_AUTOCOMMIT,$value); - } - - /** - * @return boolean whether the connection is persistent or not - * Some DBMS (such as sqlite) may not support this feature. - */ - public function getPersistent() - { - return $this->getAttribute(PDO::ATTR_PERSISTENT); - } - - /** - * @param boolean whether the connection is persistent or not - * Some DBMS (such as sqlite) may not support this feature. - */ - public function setPersistent($value) - { - return $this->setAttribute(PDO::ATTR_PERSISTENT,$value); - } - - /** - * @return string name of the DB driver - */ - public function getDriverName() - { - return $this->getAttribute(PDO::ATTR_DRIVER_NAME); - } - - /** - * @return string the version information of the DB driver - */ - public function getClientVersion() - { - return $this->getAttribute(PDO::ATTR_CLIENT_VERSION); - } - - /** - * @return string the status of the connection - * Some DBMS (such as sqlite) may not support this feature. - */ - public function getConnectionStatus() - { - return $this->getAttribute(PDO::ATTR_CONNECTION_STATUS); - } - - /** - * @return boolean whether the connection performs data prefetching - */ - public function getPrefetch() - { - return $this->getAttribute(PDO::ATTR_PREFETCH); - } - - /** - * @return string the information of DBMS server - */ - public function getServerInfo() - { - return $this->getAttribute(PDO::ATTR_SERVER_INFO); - } - - /** - * @return string the version information of DBMS server - */ - public function getServerVersion() - { - return $this->getAttribute(PDO::ATTR_SERVER_VERSION); - } - - /** - * @return int timeout settings for the connection - */ - public function getTimeout() - { - return $this->getAttribute(PDO::ATTR_TIMEOUT); - } - - /** - * Obtains a specific DB connection attribute information. - * @param int 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) - { - if($this->getActive()) - return $this->_pdo->getAttribute($name); - else - throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); - } - - /** - * Sets an attribute on the database connection. - * @param int the attribute to be set - * @param mixed 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 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. - * @since 1.0.6 - */ - public function getStats() - { - /*$logger=Yii::getLogger(); - $timings=$logger->getProfilingResults(null,'system.Db.TDbCommand.query'); - $count=count($timings); - $time=array_sum($timings); - $timings=$logger->getProfilingResults(null,'system.Db.TDbCommand.execute'); - $count+=count($timings); - $time+=array_sum($timings); - return array($count,$time);*/ - } - - /** - * Getters & Setters to provide BC with prado-3.1 - */ - public function getConnectionString() { return $this->connectionString; } - public function getUsername () { return $this->username; } - public function getPassword () { return $this->password; } - public function getCharset () { return $this->charset; } - public function getSchemaCachingDuration() { return $this->schemaCachingDuration; } - public function getSchemaCachingExclude () { return $this->schemaCachingExclude; } - public function getAutoConnect () { return $this->autoConnect; } - public function getEmulatePrepare () { return $this->emulatePrepare; } - public function getEnableParamLogging () { return $this->enableParamLogging; } - public function getEnableProfiling () { return $this->enableProfiling; } - - public function setConnectionString($value) { $this->connectionString=TPropertyValue::ensureString($value); } - public function setUsername ($value) { $this->username=TPropertyValue::ensureString($value); } - public function setPassword ($value) { $this->password=TPropertyValue::ensureString($value); } - public function setCharset ($value) { $this->charset=TPropertyValue::ensureString($value); } - public function setSchemaCachingDuration ($value) { $this->schemaCachingDuration=TPropertyValue::ensureInteger($value); } - public function setSchemaCachingExclude ($value) { $this->username=TPropertyValue::ensureArray($value); } - public function setAutoConnect ($value) { $this->autoConnect = TPropertyValue::ensureBoolean($value); } - public function setEnablePrepare ($value) { $this->emulatePrepare = TPropertyValue::ensureBoolean($value); } - public function setEnableParamLogging ($value) { $this->enableParamLogging = TPropertyValue::ensureBoolean($value); } - public function setEnableProfiling ($value) { $this->enableProfiling = TPropertyValue::ensureBoolean ($value) ; } -} - - -/** - * TDbColumnCaseMode - * - * @author Qiang Xue - * @version $Id$ - * @package System.Db - * @since 3.0 - */ -class TDbColumnCaseMode extends TEnumerable -{ - /** - * Column name cases are kept as is from the database - */ - const Preserved='Preserved'; - /** - * Column names are converted to lower case - */ - const LowerCase='LowerCase'; - /** - * Column names are converted to upper case - */ - const UpperCase='UpperCase'; -} - -/** - * TDbNullConversionMode - * - * @author Qiang Xue - * @version $Id$ - * @package System.Db - * @since 3.0 - */ -class TDbNullConversionMode extends TEnumerable -{ - /** - * No conversion is performed for null and empty values. - */ - const Preserved='Preserved'; - /** - * NULL is converted to empty string - */ - const NullToEmptyString='NullToEmptyString'; - /** - * Empty string is converted to NULL - */ - const EmptyStringToNull='EmptyStringToNull'; -} diff --git a/framework/Db/TDbDataReader.php b/framework/Db/TDbDataReader.php deleted file mode 100755 index 87fee5ed..00000000 --- a/framework/Db/TDbDataReader.php +++ /dev/null @@ -1,221 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -/** - * TDbDataReader 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 TDbDataReader by using foreach: - *
    - * foreach($reader as $row)
    - *     // $row represents a row of data
    - * 
    - * Since TDbDataReader 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. - * - * @author Qiang Xue - * @version $Id$ - * @package system.db - * @since 1.0 - */ -class TDbDataReader extends TComponent implements Iterator -{ - private $_statement; - private $_closed=false; - private $_row; - private $_index=-1; - - /** - * Constructor. - * @param TDbCommand the command generating the query result - */ - public function __construct(TDbCommand $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 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 Name of the PHP variable to which the column will be bound. - * @param int 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); - } - - /** - * @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 int 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 class name of the object to be created and populated - * @param array 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. - */ - public function nextResult() - { - return $this->_statement->nextRowset(); - } - - /** - * 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; - } - - /** - * @return boolean whether the reader is closed or not. - */ - public function getIsClosed() - { - return $this->_closed; - } - - /** - * @return int number of rows contained in the result. - * Note, most DBMS may not give a meaningful count. - * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. - */ - public function getRowCount() - { - return $this->_statement->rowCount(); - } - - /** - * @return int the number of columns in the result set. - * Note, even there's no row in the reader, this still gives correct column number. - */ - 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 TDbException('dbdatareader_rewind_invalid'); - } - - /** - * 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/TDbTransaction.php b/framework/Db/TDbTransaction.php deleted file mode 100755 index 357dd645..00000000 --- a/framework/Db/TDbTransaction.php +++ /dev/null @@ -1,111 +0,0 @@ - - * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2009 Yii Software LLC - * @license http://www.yiiframework.com/license/ - */ - -Prado::using('System.Db.TDbDataReader'); - - -/** - * TDbTransaction represents a DB transaction. - * - * It is usually created by calling {@link TDbConnection::beginTransaction}. - * - * The following code is a common scenario of using transactions: - *
    - * $transaction=$connection->beginTransaction();
    - * try
    - * {
    - *    $connection->createCommand($sql1)->execute();
    - *    $connection->createCommand($sql2)->execute();
    - *    //.... other SQL executions
    - *    $transaction->commit();
    - * }
    - * catch(Exception $e)
    - * {
    - *    $transaction->rollBack();
    - * }
    - * 
    - * - * @author Qiang Xue - * @version $Id$ - * @package system.db - * @since 1.0 - */ -class TDbTransaction extends TComponent -{ - private $_connection=null; - private $_active; - - /** - * Constructor. - * @param TDbConnection the connection associated with this transaction - * @see TDbConnection::beginTransaction - */ - public function __construct(TDbConnection $connection) - { - $this->_connection=$connection; - $this->setActive(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()) - { - Prado::trace('Committing transaction','system.Db.TDbTransaction'); - $this->_connection->getPdoInstance()->commit(); - $this->_active=false; - } - else - throw new TDbException('dbtransaction_transaction_inactive'); - } - - /** - * 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()) - { - Prado::trace('Rolling back transaction','system.Fb.TDbTransaction'); - $this->_connection->getPdoInstance()->rollBack(); - $this->_active=false; - } - else - throw new TDbException('dbtransaction_transaction_inactive'); - } - - /** - * @return TDbConnection 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 whether this transaction is active - */ - protected function setActive($value) - { - $this->_active=TPropertyValue::ensureBoolean($value); - } -} diff --git a/framework/Testing/Data/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY b/framework/Testing/Data/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY new file mode 100644 index 00000000..8c7a4073 --- /dev/null +++ b/framework/Testing/Data/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY @@ -0,0 +1,3 @@ +Please note that this file is used for old Prado ActiveRecord applications which use $record->withAnotherRecord - syntax. + +You shouln't use that syntax anymore as it's deprecated in this portation of Yii ActiveRecords. diff --git a/framework/Testing/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Testing/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php new file mode 100644 index 00000000..f13043df --- /dev/null +++ b/framework/Testing/Data/ActiveRecord/Relations/TActiveRecordRelationContext.php @@ -0,0 +1,230 @@ + + * @link http://www.pradosoft.com/ + * @copyright Copyright © 2005-2008 PradoSoft + * @license http://www.pradosoft.com/license/ + * @version $Id$ + * @package System.Testing.Data.ActiveRecord.Relations + */ + +/** + * TActiveRecordRelationContext holds information regarding record relationships + * such as record relation property name, query criteria and foreign object record + * class names. + * + * This class is use internally by passing a context to the TActiveRecordRelation + * constructor. + * + * @author Wei Zhuo + * @version $Id$ + * @package System.Testing.Data.ActiveRecord.Relations + * @since 3.1 + */ +class TActiveRecordRelationContext +{ + private $_property; + private $_record; + private $_relation; //data from an entry of TActiveRecord::$RELATION + private $_fkeys; + + public function __construct($record, $property=null, $relation=null) + { + $this->_record=$record; + $this->_property=$property; + $this->_relation=$relation; + } + + /** + * @return boolean true if the relation is defined in TActiveRecord::$RELATIONS + * @since 3.1.2 + */ + public function hasRecordRelation() + { + return $this->_relation!==null; + } + + public function getPropertyValue() + { + $obj = $this->getSourceRecord(); + return $obj->getColumnValue($this->getProperty()); + } + + /** + * @return string name of the record property that the relationship results will be assigned to. + */ + public function getProperty() + { + return $this->_property; + } + + /** + * @return TActiveRecord the active record instance that queried for its related records. + */ + public function getSourceRecord() + { + return $this->_record; + } + + /** + * @return array foreign key of this relations, the keys is dependent on the + * relationship type. + * @since 3.1.2 + */ + public function getRelationForeignKeys() + { + if($this->_fkeys===null) + $this->_fkeys=$this->getRelationHandler()->getRelationForeignKeys(); + return $this->_fkeys; + } + + /** + * @return string HAS_MANY, HAS_ONE, or BELONGS_TO + */ + public function getRelationType() + { + return $this->_relation[0]; + } + + /** + * @return string foreign record class name. + */ + public function getForeignRecordClass() + { + return $this->_relation[1]; + } + + /** + * @return string foreign key field names, comma delimited. + * @since 3.1.2 + */ + public function getFkField() + { + return $this->_relation[2]; + } + + /** + * @return string the query condition for the relation as specified in RELATIONS + * @since 3.1.2 + */ + public function getCondition() + { + return isset($this->_relation[3])?$this->_relation[3]:null; + } + + /** + * @return array the query parameters for the relation as specified in RELATIONS + * @since 3.1.2 + */ + public function getParameters() + { + return isset($this->_relation[4])?$this->_relation[4]:array(); + } + + /** + * @return boolean true if the 3rd element of an TActiveRecord::$RELATION entry is set. + * @since 3.1.2 + */ + public function hasFkField() + { + $notManyToMany = $this->getRelationType() !== TActiveRecord::MANY_TO_MANY; + return $notManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]); + } + + /** + * @return string the M-N relationship association table name. + */ + public function getAssociationTable() + { + return $this->_relation[2]; + } + + /** + * @return boolean true if the relationship is HAS_MANY and requires an association table. + */ + public function hasAssociationTable() + { + $isManyToMany = $this->getRelationType() === TActiveRecord::MANY_TO_MANY; + return $isManyToMany && isset($this->_relation[2]) && !empty($this->_relation[2]); + } + + /** + * @return TActiveRecord corresponding relationship foreign object finder instance. + */ + public function getForeignRecordFinder() + { + return TActiveRecord::finder($this->getForeignRecordClass()); + } + + /** + * Creates and return the TActiveRecordRelation handler for specific relationships. + * An instance of TActiveRecordHasOne, TActiveRecordBelongsTo, TActiveRecordHasMany, + * or TActiveRecordHasManyAssocation will be returned. + * @param TActiveRecordCriteria search criteria + * @return TActiveRecordRelation record relationship handler instnace. + * @throws TActiveRecordException if property is not defined or missing. + */ + public function getRelationHandler($criteria=null) + { + if(!$this->hasRecordRelation()) + { + throw new TActiveRecordException('ar_undefined_relation_prop', + $this->_property, get_class($this->_record), 'RELATIONS'); + } + if($criteria===null) + $criteria = new TActiveRecordCriteria($this->getCondition(), $this->getParameters()); + switch($this->getRelationType()) + { + case TActiveRecord::HAS_MANY: + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasMany'); + return new TActiveRecordHasMany($this, $criteria); + case TActiveRecord::MANY_TO_MANY: + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasManyAssociation'); + return new TActiveRecordHasManyAssociation($this, $criteria); + case TActiveRecord::HAS_ONE: + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordHasOne'); + return new TActiveRecordHasOne($this, $criteria); + case TActiveRecord::BELONGS_TO: + Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordBelongsTo'); + return new TActiveRecordBelongsTo($this, $criteria); + default: + throw new TActiveRecordException('ar_invalid_relationship'); + } + } + + /** + * @return TActiveRecordRelationCommand + */ + public function updateAssociatedRecords($updateBelongsTo=false) + { + $success=true; + foreach($this->_record->getRecordRelations() as $data) + { + list($property, $relation) = $data; + $belongsTo = $relation[0]==TActiveRecord::BELONGS_TO; + if(($updateBelongsTo && $belongsTo) || (!$updateBelongsTo && !$belongsTo)) + { + $obj = $this->getSourceRecord(); + if(!$this->isEmptyFkObject($obj->getColumnValue($property))) + { + $context = new TActiveRecordRelationContext($this->getSourceRecord(),$property,$relation); + $success = $context->getRelationHandler()->updateAssociatedRecords() && $success; + } + } + } + return $success; + } + + protected function isEmptyFkObject($obj) + { + if(is_object($obj)) + return $obj instanceof TList ? $obj->count() === 0 : false; + else if(is_array($obj)) + return count($obj)===0; + else + return empty($obj); + } +} + 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 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 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 + * @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 + * @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_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 + * @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 + * @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($iprimaryKey)) + { + $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); + } +} diff --git a/framework/Testing/Data/ActiveRecord/TActiveRecord.php b/framework/Testing/Data/ActiveRecord/TActiveRecord.php new file mode 100644 index 00000000..8d6c5e10 --- /dev/null +++ b/framework/Testing/Data/ActiveRecord/TActiveRecord.php @@ -0,0 +1,1843 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 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. + * + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ + +Prado::using('System.Base.TModel'); +Prado::using('System.Testing.Data.*'); +Prado::using('System.Testing.Data.ActiveRecord.*'); +Prado::using('System.Base.TEvent'); +Prado::using('System.Testing.Data.ActiveRecord.TActiveRecordCriteria'); + +abstract class TActiveRecord extends TModel +{ + const BELONGS_TO='TBelongsToRelation'; + const HAS_ONE='THasOneRelation'; + const HAS_MANY='THasManyRelation'; + const MANY_MANY='TManyManyRelation'; + const STAT='TStatRelation'; + + /** + * @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) + + /** + * Returns the instance of a active record finder for a particular class. + * The finder objects are static instances for each ActiveRecord class. + * This means that event handlers bound to these finder instances are class wide. + * Create a new instance of the ActiveRecord class if you wish to bound the + * event handlers to object instance. + * @param string active record class name. + * @return TActiveRecord active record finder instance. + */ + + /** + * Constructor. + * @param string 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->attachBehaviors($this->behaviors()); + $this->afterConstruct(); + } + + /** + * PHP sleep magic method. + * This method ensures that the model meta data reference is set to null. + */ + 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 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 property name + * @param mixed property value + */ + public function __set($name,$value) + { + if($this->setAttribute($name,$value)===false) + 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 the property name or the event name + * @return boolean whether the property value is null + * @since 1.0.1 + */ + 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 the property name or the event name + * @since 1.0.1 + */ + 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 the method name + * @param array method parameters + * @return mixed the method return value + * @since 1.0.5 + * + * @TODO: implement __call to parent + * + */ + 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 the relation name (see {@link relations}) + * @param boolean whether to reload the related objects from database. Defaults to false. + * @param array additional parameters that customize the query conditions as specified in the relation declaration. + * This parameter has been available since version 1.0.5. + * @return mixed the related object(s). + * @throws CDbException if the relation is not specified in {@link relations}. + * @since 1.0.2 + */ + 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 TDbException(get_class($this).' does not have relation "'.$name.'".'); + + Prado::trace('lazy loading '.get_class($this).'.'.$name,'System.Testing.Data.ActiveRecord.TActiveRecord'); + $relation=$md->relations[$name]; + if($this->getIsNewRecord() && ($relation instanceof THasOneRelation || $relation instanceof THasManyRelation)) + return $relation instanceof THasOneRelation ? null : array(); + + if($params!==array()) // dynamic query + { + $exists=isset($this->_related[$name]) || array_key_exists($name,$this->_related); + if($exists) + $save=$this->_related[$name]; + unset($this->_related[$name]); + $r=array($name=>$params); + } + else + $r=$name; + + $finder=new TActiveFinder($this,$r); + $finder->lazyFind($this); + + if(!isset($this->_related[$name])) + { + if($relation instanceof THasManyRelation) + $this->_related[$name]=array(); + else if($relation instanceof TStatRelation) + $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 the relation name + * @return booolean a value indicating whether the named related object(s) has been loaded. + * @since 1.0.3 + */ + 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 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. + * @since 1.0.5 + */ + public function getDbCriteria($createIfNull=true) + { + if($this->_c===null) + { + if(($c=$this->defaultScope())!==array() || $createIfNull) + $this->_c=new TDbCriteria($c); + } + return $this->_c; + } + + /** + * Returns the default named scope that should be applied to all queries implicitly for this model. + * 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}. + * @since 1.0.5 + */ + public function defaultScope() + { + return array(); + } + + /** + * 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, + *
    +	 * public static function model($className=__CLASS__)
    +	 * {
    +	 *     return parent::model($className);
    +	 * }
    +	 * 
    + * + * @param string 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->attachBehaviors($model->behaviors()); + $model->_md=new TActiveRecordMetaData($model); + return $model; + } + } + + /** + * @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; + } + + /** + * 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. + * @since 1.0.4 + */ + 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: + *
      + *
    • BELONGS_TO: e.g. a member belongs to a team;
    • + *
    • HAS_ONE: e.g. a member has at most one profile;
    • + *
    • HAS_MANY: e.g. a team has many members;
    • + *
    • MANY_MANY: e.g. a member has many skills and a skill belongs to a member.
    • + *
    + * + * By declaring these relations, CActiveRecord can bring back related objects + * in either lazy loading or eager loading approach, and you save the effort of + * writing complex JOIN SQL statements. + * + * Each kind of related objects is defined in this method as an array with the following elements: + *
    +	 * 'varName'=>array('relationType', 'className', 'foreignKey', ...additional options)
    +	 * 
    + * 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 'foreignKey' states the foreign key that relates the two kinds of active record. + * Note, for composite foreign keys, they must be listed together, separating with space or comma; + * and for foreign keys used in MANY_MANY relation, the joining table must be declared as well + * (e.g. 'joinTable(fk1, fk2)'). + * + * Additional options may be specified as name-value pairs in the rest array elements: + *
      + *
    • '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(??.name) AS name_count).
    • + *
    • 'condition': string, the WHERE clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix '??.' (e.g. ??.age>20)
    • + *
    • 'order': string, the ORDER BY clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix '??.' (e.g. ??.age DESC)
    • + *
    • '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.
    • + *
    • 'joinType': type of join. Defaults to 'LEFT OUTER JOIN'.
    • + *
    • 'alias': the alias for the table associated with this relationship. + * This option has been available since version 1.0.1. It defaults to null, + * meaning the table alias is the same as the relation name.
    • + *
    • 'params': the parameters to be bound to the generated SQL statement. + * This should be given as an array of name-value pairs. This option has been + * available since version 1.0.3.
    • + *
    • 'on': the ON clause. The condition specified here will be appended + * to the joining condition using the AND operator. This option has been + * available since version 1.0.2.
    • + *
    + * + * The following options are available for certain relations when lazy loading: + *
      + *
    • 'group': string, the GROUP BY clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix '??.' (e.g. ??.age). This option only applies to HAS_MANY and MANY_MANY relations.
    • + *
    • 'having': string, the HAVING clause. Defaults to empty. Note, column references need to + * be disambiguated with prefix '??.' (e.g. ??.age). This option only applies to HAS_MANY and MANY_MANY relations.
    • + *
    • 'limit': limit of the rows to be selected. This option does not apply to BELONGS_TO relation.
    • + *
    • 'offset': offset of the rows to be selected. This option does not apply to BELONGS_TO relation.
    • + *
    + * + * Below is an example declaring related objects for 'Post' active record class: + *
    +	 * return array(
    +	 *     'author'=>array(self::BELONGS_TO, 'User', 'authorID'),
    +	 *     'comments'=>array(self::HAS_MANY, 'Comment', 'postID', 'with'=>'author', 'order'=>'createTime DESC'),
    +	 *     'tags'=>array(self::MANY_MANY, 'Tag', 'PostTag(postID, tagID)', 'order'=>'name'),
    +	 * );
    +	 * 
    + * + * @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'. + *
    +	 * return array(
    +	 *     'published'=>array(
    +	 *           'condition'=>'status=1',
    +	 *     ),
    +	 *     'recently'=>array(
    +	 *           'order'=>'createTime DESC',
    +	 *           'limit'=>5,
    +	 *     ),
    +	 * );
    +	 * 
    + * If the above scopes are declared in a 'Post' model, we can perform the following + * queries: + *
    +	 * $posts=Post::model()->published()->findAll();
    +	 * $posts=Post::model()->published()->recently()->findAll();
    +	 * $posts=Post::model()->published()->with('comments')->findAll();
    +	 * 
    + * 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}. + * @since 1.0.5 + */ + 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. + * @since 1.0.1 + */ + public function attributeNames() + { + return array_keys($this->getMetaData()->columns); + } + + /** + * 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. + * + * @TODO Get default db connection if theres none set + * + */ + public function getDbConnection() + { + if(self::$db!==null) + return self::$db; + else + { + self::$db=Prado::getApplication()->getModule($this->getConnectionId())->getDbConnection(); + if(self::$db instanceof TDbConnection) + { + self::$db->setActive(true); + return self::$db; + } + else + throw new TDbException('ActiveRecord requires a db connection.'); + } + } + + /** + * Returns connection id (must be overloaded by child classes) + * @return string Id of TDbConnection module + */ + abstract public function getConnectionId(); + + /** + * @param string 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; + } + + /** + * @return CDbTableSchema the metadata of the table that this AR belongs to + */ + public function getTableSchema() + { + return $this->getMetaData()->tableSchema; + } + + /** + * @return CDbCommandBuilder the command builder used by this AR + */ + public function getCommandBuilder() + { + return $this->getDbConnection()->getSchema()->getCommandBuilder(); + } + + /** + * @param string 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 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 the attribute name + * @param mixed 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; + } + + /** + * Adds a related object to this record. + * This method is used internally by {@link CActiveFinder} to populate related objects. + * @param string attribute name + * @param mixed the related record + * @param boolean whether the relation is HAS_MANY/MANY_MANY. + */ + public function addRelatedRecord($name,$record,$multiple) + { + if($multiple) + { + if(!isset($this->_related[$name])) + $this->_related[$name]=array(); + if($record instanceof TActiveRecord) + $this->_related[$name][]=$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 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) + $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 whether to perform validation before saving the record. + * If the validation fails, the record will not be saved to database. + * @param array 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; + } + + /** + * @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; + } + + /** + * @param boolean 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. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onBeforeSave($event) + { + $this->raiseEvent('onBeforeSave',$event); + } + + /** + * This event is raised after the record is saved. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onAfterSave($event) + { + $this->raiseEvent('onAfterSave',$event); + } + + /** + * This event is raised before the record is deleted. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onBeforeDelete($event) + { + $this->raiseEvent('onBeforeDelete',$event); + } + + /** + * This event is raised after the record is deleted. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onAfterDelete($event) + { + $this->raiseEvent('onAfterDelete',$event); + } + + /** + * This event is raised after the record instance is created by new operator. + * @param CEvent the event parameter + * @since 1.0.2 + */ + public function onAfterConstruct($event) + { + $this->raiseEvent('onAfterConstruct',$event); + } + + /** + * This event is raised after the record is instantiated by a find method. + * @param CEvent the event parameter + * @since 1.0.2 + */ + 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() + { + $this->onBeforeSave(new TEvent($this)); + return true; + } + + /** + * This method is invoked after saving a record. + * 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() + { + $this->onAfterSave(new TEvent($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() + { + $this->onBeforeDelete(new TEvent($this)); + 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() + { + $this->onAfterDelete(new TEvent($this)); + } + + /** + * This method is invoked after a record instance is created by new operator. + * The default implementation raises the {@link onAfterConstruct} event. + * You may override this method to do postprocessing after record creation. + * Make sure you call the parent implementation so that the event is raised properly. + */ + protected function afterConstruct() + { + $this->onAfterConstruct(new TEvent($this)); + } + + /** + * 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() + { + $this->onAfterFind(new TEvent($this)); + } + + /** + * Calls {@link afterFind}. + * This method is internally used. + * @since 1.0.3 + */ + 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 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 TDbException('The active record cannot be inserted to database because it is not new.'); + if($this->beforeSave()) + { + Prado::trace(get_class($this).'.insert()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $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->afterSave(); + $this->setIsNewRecord(false); + $this->setScenario('update'); + return true; + } + else + $this->afterSave(); + } + else + 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 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 TDbException('The active record cannot be updated because it is new.'); + if($this->beforeSave()) + { + Prado::trace(get_class($this).'.update()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $this->updateByPk($this->getPrimaryKey(),$this->getAttributes($attributes)); + $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. It thus has better performance. + * Note, 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: + *
    +	 * $postRecord=Post::model()->findByPk($postID);
    +	 * $postRecord->attributes=$_POST['post'];
    +	 * $postRecord->save();
    +	 * 
    + * @param array 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()) + { + Prado::trace(get_class($this).'.saveAttributes()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $values=array(); + foreach($attributes as $name=>$value) + { + if(is_integer($name)) + $values[$value]=$this->$value; + else + $values[$name]=$this->$name=$value; + } + return $this->updateByPk($this->getPrimaryKey(),$values)>0; + } + else + throw new TDbException('The active record cannot be updated because it is new.'); + } + + /** + * 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()) + { + Prado::trace(get_class($this).'.delete()','System.Testing.Data.ActiveRecord.TActiveRecord'); + if($this->beforeDelete()) + { + $result=$this->deleteByPk($this->getPrimaryKey())>0; + $this->afterDelete(); + return $result; + } + else + return false; + } + else + throw new TDbException('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() + { + Prado::trace(get_class($this).'.refresh()','System.Testing.Data.ActiveRecord.TActiveRecord'); + if(!$this->getIsNewRecord() && ($record=$this->findByPk($this->getPrimaryKey()))!==null) + { + $this->_attributes=array(); + $this->_related=array(); + foreach($this->getMetaData()->columns as $name=>$column) + $this->$name=$record->$name; + return true; + } + else + return false; + } + + /** + * Compares this active record with another one. + * The comparison is made by comparing the primary key values of the two active records. + * @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(); + } + + /** + * @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; + } + + private function query($criteria,$all=false) + { + $this->applyScopes($criteria); + $command=$this->getCommandBuilder()->createFindCommand($this->getTableSchema(),$criteria); + return $all ? $this->populateRecords($command->queryAll()) : $this->populateRecord($command->queryRow()); + } + + private function applyScopes(&$criteria) + { + if(($c=$this->getDbCriteria(false))!==null) + { + $c->mergeWith($criteria); + $criteria=$c; + $this->_c=null; + } + } + + /** + * Finds a single active record with the specified condition. + * @param mixed 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 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()) + { + Prado::trace(get_class($this).'.find()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createCriteria($condition,$params); + $criteria->limit=1; + return $this->query($criteria); + } + + /** + * Finds all active records satisfying the specified condition. + * See {@link find()} for detailed explanation about $condition and $params. + * @param mixed query condition or criteria. + * @param array 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()) + { + Prado::trace(get_class($this).'.findAll()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $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 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 query condition or criteria. + * @param array 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()) + { + Prado::trace(get_class($this).'.findByPk()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); + $criteria->limit=1; + 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 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 query condition or criteria. + * @param array 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()) + { + Prado::trace(get_class($this).'.findAllByPk()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); + 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 list of attribute values (indexed by attribute names) that the active records should match. + * @param mixed query condition or criteria. + * @param array 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()) + { + Prado::trace(get_class($this).'.findByAttributes()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(),$attributes,$condition,$params); + $criteria->limit=1; + 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 list of attribute values (indexed by attribute names) that the active records should match. + * @param mixed query condition or criteria. + * @param array 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()) + { + Prado::trace(get_class($this).'.findAllByAttributes()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $criteria=$this->getCommandBuilder()->createColumnCriteria($this->getTableSchema(),$attributes,$condition,$params); + return $this->query($criteria,true); + } + + /** + * Finds a single active record with the specified SQL statement. + * @param string the SQL statement + * @param array parameters to be bound to the SQL statement + * @return CActiveRecord the record found. Null if none is found. + */ + public function findBySql($sql,$params=array()) + { + Prado::trace(get_class($this).'.findBySql()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $command=$this->getCommandBuilder()->createSqlCommand($sql,$params); + return $this->populateRecord($command->queryRow()); + } + + /** + * Finds all active records using the specified SQL statement. + * @param string the SQL statement + * @param array 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()) + { + Prado::trace(get_class($this).'.findAllBySql()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $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 query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows satisfying the specified query condition. + */ + public function count($condition='',$params=array()) + { + Prado::trace(get_class($this).'.count()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $this->applyScopes($criteria); + return $builder->createCountCommand($this->getTableSchema(),$criteria)->queryScalar(); + } + + /** + * Finds the number of rows using the given SQL statement. + * See {@link find()} for detailed explanation about $condition and $params. + * @param string the SQL statement + * @param array parameters to be bound to the SQL statement + * @return integer the number of rows using the given SQL statement. + */ + public function countBySql($sql,$params=array()) + { + Prado::trace(get_class($this).'.countBySql()','System.Testing.Data.ActiveRecord.TActiveRecord'); + 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 query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return boolean whether there is row satisfying the specified condition. + */ + public function exists($condition,$params=array()) + { + Prado::trace(get_class($this).'.exists()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $table=$this->getTableSchema(); + $criteria->select=reset($table->columns)->rawName; + $criteria->limit=1; + $this->applyScopes($criteria); + return $builder->createFindCommand($table,$criteria)->queryRow()!==false; + } + + /** + * 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, + *
    +	 * // 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();
    +	 * 
    + * 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, + *
    +	 * Post::model()->with(array(
    +	 *     'author'=>array('select'=>'id, name'),
    +	 *     'comments'=>array('condition'=>'approved=1', 'order'=>'createTime'),
    +	 * ))->findAll();
    +	 * 
    + * + * This method returns a {@link CActiveFinder} instance that provides + * a set of find methods similar to that of CActiveRecord. + * + * Note, the possible parameters to this method have been changed since version 1.0.2. + * Previously, it was not possible to specify on-th-fly query options, + * and child-relations were specified as hierarchical arrays. + * + * @return CActiveFinder the active finder instance. If no parameter is passed in, the object itself will be returned. + */ + 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]; + $finder=new TActiveFinder($this,$with,$this->getDbCriteria(false)); + $this->_c=null; + return $finder; + } + else + 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 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 list of attributes (name=>$value) to be updated + * @param mixed query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows being updated + */ + public function updateByPk($pk,$attributes,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.updateByPk()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $table=$this->getTableSchema(); + $criteria=$builder->createPkCriteria($table,$pk,$condition,$params); + $this->applyScopes($criteria); + $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 list of attributes (name=>$value) to be updated + * @param mixed query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows being updated + */ + public function updateAll($attributes,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.updateAll()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $this->applyScopes($criteria); + $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 the counters to be updated (column name=>increment value) + * @param mixed query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows being updated + */ + public function updateCounters($counters,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.updateCounters()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $this->applyScopes($criteria); + $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 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 query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows deleted + */ + public function deleteByPk($pk,$condition='',$params=array()) + { + Prado::trace(get_class($this).'.deleteByPk()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createPkCriteria($this->getTableSchema(),$pk,$condition,$params); + $this->applyScopes($criteria); + $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 query condition or criteria. + * @param array parameters to be bound to an SQL statement. + * @return integer the number of rows deleted + */ + public function deleteAll($condition='',$params=array()) + { + Prado::trace(get_class($this).'.deleteAll()','System.Testing.Data.ActiveRecord.TActiveRecord'); + $builder=$this->getCommandBuilder(); + $criteria=$builder->createCriteria($condition,$params); + $this->applyScopes($criteria); + $command=$builder->createDeleteCommand($this->getTableSchema(),$criteria); + return $command->execute(); + } + + /** + * Creates an active record with the given attributes. + * This method is internally used by the find methods. + * @param array attribute values (column name=>column value) + * @param boolean whether to call {@link afterFind} after the record is populated. + * This parameter is added in version 1.0.3. + * @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->_md=$this->getMetaData(); + foreach($attributes as $name=>$value) + { + if(property_exists($record,$name)) + $record->$name=$value; + else if(isset($record->_md->columns[$name])) + $record->_attributes[$name]=$value; + } + $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 list of attribute values for the active records. + * @param boolean whether to call {@link afterFind} after each record is populated. + * This parameter is added in version 1.0.3. + * @return array list of active records. + */ + public function populateRecords($data,$callAfterFind=true) + { + $records=array(); + $md=$this->getMetaData(); + $table=$md->tableSchema; + foreach($data as $attributes) + { + $record=$this->instantiate($attributes); + $record->_md=$md; + foreach($attributes as $name=>$value) + { + if(property_exists($record,$name)) + $record->$name=$value; + else if(isset($record->_md->columns[$name])) + $record->_attributes[$name]=$value; + } + $record->attachBehaviors($record->behaviors()); + if($callAfterFind) + $record->afterFind(); + $records[]=$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 list of attribute values for the active records. + * @return CActiveRecord the active record + * @since 1.0.2 + */ + protected function instantiate($attributes) + { + $class=get_class($this); + $model=new $class(null); + $model->setScenario('update'); + return $model; + } + + /** + * Returns whether there is an element at the specified offset. + * This method is required by the interface ArrayAccess. + * @param mixed the offset to check on + * @return boolean + * @since 1.0.2 + */ + public function offsetExists($offset) + { + return isset($this->getMetaData()->columns[$offset]); + } +} + + +/** + * CBaseActiveRelation is the base class for all active relations. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0.4 + */ +class TBaseActiveRelation extends TComponent +{ + /** + * @var string name of the related object + */ + public $name; + /** + * @var string name of the related active record class + */ + public $className; + /** + * @var string 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 '??.'. + */ + public $select='*'; + /** + * @var string WHERE clause. For {@link CActiveRelation} descendant classes, column names + * referenced in the condition should be disambiguated with prefix '??.'. + */ + 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 '??.'. + */ + public $group=''; + /** + * @var string HAVING clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix '??.'. + */ + public $having=''; + /** + * @var string ORDER BY clause. For {@link CActiveRelation} descendant classes, column names + * referenced in this property should be disambiguated with prefix '??.'. + */ + public $order=''; + + /** + * Constructor. + * @param string name of the relation + * @param string name of the related active record class + * @param string foreign key for this relation + * @param array 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 the dynamically specified criteria + * @since 1.0.5 + */ + public function mergeWith($criteria) + { + 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']; + } + + 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['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 + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0.4 + */ +class TStatRelation extends TBaseActiveRelation +{ + /** + * @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 the dynamically specified criteria + * @since 1.0.5 + */ + public function mergeWith($criteria) + { + parent::mergeWith($criteria); + + 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 + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class TActiveRelation extends TBaseActiveRelation +{ + /** + * @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. + * @since 1.0.2 + */ + 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. + * @since 1.0.1 + */ + 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(); + + /** + * Merges this relation with a criteria specified dynamically. + * @param array the dynamically specified criteria + * @since 1.0.5 + */ + public function mergeWith($criteria) + { + 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 + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class TBelongsToRelation extends TActiveRelation +{ +} + + +/** + * CHasOneRelation represents the parameters specifying a HAS_ONE relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class THasOneRelation extends TActiveRelation +{ +} + + +/** + * CHasManyRelation represents the parameters specifying a HAS_MANY relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class THasManyRelation extends TActiveRelation +{ + /** + * @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 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. Defaults to true, meaning the table will be joined + * together with the primary table. Note that in version 1.0.x, the default value of this property was false. + */ + public $together=true; + + /** + * Merges this relation with a criteria specified dynamically. + * @param array the dynamically specified criteria + * @since 1.0.5 + */ + public function mergeWith($criteria) + { + parent::mergeWith($criteria); + if(isset($criteria['limit']) && $criteria['limit']>0) + $this->limit=$criteria['limit']; + + if(isset($criteria['offset']) && $criteria['offset']>=0) + $this->offset=$criteria['offset']; + } +} + + +/** + * CManyManyRelation represents the parameters specifying a MANY_MANY relation. + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class TManyManyRelation extends THasManyRelation +{ +} + + +/** + * CActiveRecordMetaData represents the meta-data for an Active Record class. + * + * @author Qiang Xue + * @version $Id: CActiveRecord.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0 + */ +class TActiveRecordMetaData +{ + /** + * @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 the model instance + */ + public function __construct($model) + { + $this->_model=$model; + + $tableName=$model->tableName(); + if(($table=$model->getDbConnection()->getSchema()->getTable($tableName))===null) + throw new TDbException('The table "'.$tableName.'" for active record class "'.get_class($model).'" cannot be found in the database.'); + + if($table->primaryKey===null) + $table->primaryKey=$model->primaryKey(); + $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) + { + 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 TDbException('Active record "'.get_class($model).'" has an invalid configuration for relation "'.$name.'". It must specify the relation type, the related active record class and the foreign key.'); + } + } +} + +/** + * TActiveRecordInvalidFinderResult class. + * TActiveRecordInvalidFinderResult defines the enumerable type for possible results + * if an invalid {@link TActiveRecord::__call magic-finder} invoked. + * + * The following enumerable values are defined: + * - Null: return null (default) + * - Exception: throws a TActiveRecordException + * + * @author Yves Berkholz + * @version $Id: TActiveRecord.php 2649 2009-05-11 07:44:55Z godzilla80@gmx.net $ + * @package System.Data.ActiveRecord + * @see TActiveRecordManager::setInvalidFinderResult + * @see TActiveRecordConfig::setInvalidFinderResult + * @see TActiveRecord::setInvalidFinderResult + * @since 3.1.5 + */ +class TActiveRecordInvalidFinderResult extends TEnumerable +{ + const Null = 'Null'; + const Exception = 'Exception'; +} diff --git a/framework/Testing/Data/ActiveRecord/TActiveRecordBehavior.php b/framework/Testing/Data/ActiveRecord/TActiveRecordBehavior.php new file mode 100644 index 00000000..cd4d8386 --- /dev/null +++ b/framework/Testing/Data/ActiveRecord/TActiveRecordBehavior.php @@ -0,0 +1,94 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 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}. + * + * @author Qiang Xue + * @version $Id: CActiveRecordBehavior.php 564 2009-01-21 22:07:10Z qiang.xue $ + * @package System.Testing.Data.ActiveRecord + * @since 1.0.2 + */ +class TActiveRecordBehavior extends TModelBehavior +{ + /** + * 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', + 'onAfterConstruct'=>'afterConstruct', + '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}. + * @param CEvent 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 CEvent 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}. + * @param CEvent 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 parameter + */ + public function afterDelete($event) + { + } + + /** + * Responds to {@link CActiveRecord::onAfterConstruct} event. + * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}. + * @param CEvent event parameter + */ + public function afterConstruct($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 parameter + */ + public function afterFind($event) + { + } +} diff --git a/framework/Testing/Data/ActiveRecord/TActiveRecordCriteria.php b/framework/Testing/Data/ActiveRecord/TActiveRecordCriteria.php new file mode 100644 index 00000000..46eddaf9 --- /dev/null +++ b/framework/Testing/Data/ActiveRecord/TActiveRecordCriteria.php @@ -0,0 +1,254 @@ + + */ + +Prado::using('System.Testing.Data.Schema.TDbCriteria'); + +class TSqlCriteria extends TDbCriteria +{ + + private $_TDbCriteria = null; + + public function __construct($condition=null, $parameters=array()) + { + if(!is_array($parameters) && func_num_args() > 1) + $parameters = array_slice(func_get_args(), 1); + + $this->_TDbCriteria = $this; + + $this->_TDbCriteria->params = new ArrayObject(); + + $param = array( + 'condition' => ($condition !== null) ? $condition : '', + 'params' => $parameters + ); + } + + public function getCondition() + { + return $this->_TDbCriteria->condition; + } + + public function setCondition($value) + { + if(empty($value)) + return; + + // supporting the following SELECT-syntax: + // [ORDER BY {col_name | expr | position} + // [ASC | DESC], ...] + // [LIMIT {[offset,] row_count | row_count OFFSET offset}] + // See: http://dev.mysql.com/doc/refman/5.0/en/select.html + + + if(preg_match('/ORDER\s+BY\s+(.*?)(?=LIMIT)|ORDER\s+BY\s+(.*?)$/i', $value, $matches) > 0) { + // condition contains ORDER BY + $value = str_replace($matches[0], '', $value); + if(strlen($matches[1]) > 0) { + $this->_TDbCriteria->setOrdersBy($matches[1]); + } else if(strlen($matches[2]) > 0) { + $this->setOrdersBy($matches[2]); + } + } + + if(preg_match('/LIMIT\s+([\d\s,]+)/i', $value, $matches) > 0) { + // condition contains limit + $value = str_replace($matches[0], '', $value); + // remove limit from query + if(strpos($matches[1], ',')) { // both offset and limit given + list($offset, $limit) = explode(',', $matches[1]); + $this->_TDbCriteria->limit = (int)$limit; + $this->_TDbCriteria->offset = (int)$offset; + } else { // only limit given + $this->_TDbCriteria->limit = (int)$matches[1]; + } + } + + if(preg_match('/OFFSET\s+(\d+)/i', $value, $matches) > 0) { + // condition contains offset + $value = str_replace($matches[0], '', $value); + // remove offset from query + $this->_TDbCriteria->offset = (int)$matches[1]; // set offset in criteria + } + + $this->_TDbCriteria->condition = trim($value); + } + + public function getParameters() + { + return $this->_TDbCriteria->params; + } + + public function setParameters($value) + { + if(is_array($value)) + { + $this->_TDbCriteria->params = $value; + } + elseif ($value instanceof ArrayAccess) + { + $this->_TDbCriteria->params = (array)$value; + } + else + throw new TException('Value must be an array or of type ArrayAccess.'); + } + + public function getIsNamedParameters() + { + foreach($this->_TDbCriteria->params as $key=>$val) + return is_string($key); + } + + public function getLimit() + { + return ($this->_TDbCriteria->limit != -1) ? $this->_TDbCriteria->limit : null; + } + + public function setLimit($value) + { + $this->_TDbCriteria->limit = (int)$value; + } + + public function getOffset() + { + return ($this->_TDbCriteria->offset != -1) ? $this->_TDbCriteria->offset : null; + } + + public function setOffset($value) + { + $this->_TDbCriteria->offset = (int)$value; + } + + public function getOrdersBy() + { + if(empty($this->_TDbCriteria->order)) + return array(); + + $value=trim(preg_replace('/\s+/',' ', $this->_TDbCriteria->order)); + $orderBys=array(); + foreach(explode(',',$value) as $orderBy) + { + $vs=explode(' ',trim($orderBy)); + + $orderBys[$vs[0]]=isset($vs[1])?$vs[1]:'asc'; + } + return $orderBys; + } + + public function setOrdersBy($value) + { + if(is_string($value)) + $this->TDbCriteria->order = $value; + elseif (is_array($value) || $value instanceof Traversable) + { + $str = ''; + foreach($value as $key => $val) + $str .= (($str != '') ? ', ': '').$key.' '.$val; + + $this->_TDbCriteria->order = $str; + } + } + + public function __toString() + { + $str = ''; + if(strlen((string)$this->getCondition()) > 0) + $str .= '"'.(string)$this->getCondition().'"'; + $params = array(); + foreach($this->getParameters() as $k=>$v) + $params[] = "{$k} => ${v}"; + if(count($params) > 0) + $str .= ', "'.implode(', ',$params).'"'; + $orders = array(); + foreach($this->getOrdersBy() as $k=>$v) + $orders[] = "{$k} => ${v}"; + if(count($orders) > 0) + $str .= ', "'.implode(', ',$orders).'"'; + if($this->getLimit() !==null) + $str .= ', '.$this->_limit; + if($this->getOffset() !== null) + $str .= ', '.$this->_offset; + return $str; + } + + /** + * Returns a property value or an event handler list by property or event name. + * Do not call this method. This is a PHP magic method that we override + * to allow using the following syntax to read a property: + * + * $value=$component->PropertyName; + * + * and to obtain the event handler list for an event, + * + * $eventHandlerList=$component->EventName; + * + * @param string the property name or the event name + * @return mixed the property value or the event handler list + * @throws TInvalidOperationException if the property/event is not defined. + */ + public function __get($name) + { + $getter='get'.$name; + if(method_exists($this,$getter)) + { + // getting a property + return $this->$getter(); + } + else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) + { + // getting an event (handler list) + $name=strtolower($name); + if(!isset($this->_e[$name])) + $this->_e[$name]=new TList; + return $this->_e[$name]; + } + else + { + throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); + } + } + + /** + * Sets value of a component property. + * Do not call this method. This is a PHP magic method that we override + * to allow using the following syntax to set a property or attach an event handler. + * + * $this->PropertyName=$value; + * $this->EventName=$handler; + * + * @param string the property name or event name + * @param mixed the property value or event handler + * @throws TInvalidOperationException If the property is not defined or read-only. + */ + public function __set($name,$value) + { + $setter='set'.$name; + if(method_exists($this,$setter)) + { + $this->$setter($value); + } + else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) + { + $this->attachEventHandler($name,$value); + } + else if(method_exists($this,'get'.$name)) + { + throw new TInvalidOperationException('component_property_readonly',get_class($this),$name); + } + else + { + throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); + } + } + +} + +class TActiveRecordCriteria extends TSqlCriteria +{ + +} diff --git a/framework/Testing/Data/Schema/TDbColumnSchema.php b/framework/Testing/Data/Schema/TDbColumnSchema.php new file mode 100755 index 00000000..92b7c0bd --- /dev/null +++ b/framework/Testing/Data/Schema/TDbColumnSchema.php @@ -0,0 +1,145 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbExpression'); + +/** + * TDbColumnSchema class describes the column meta data of a database table. + * + * @author Qiang Xue + * @version $Id: TDbColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema + * @since 1.0 + */ +class TDbColumnSchema extends TComponent +{ + /** + * @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; + + + /** + * 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 the column's DB type + * @param mixed 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 DB type + */ + protected function extractType($dbType) + { + if(stripos($dbType,'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 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 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 input value + * @return mixed converted value + */ + public function typecast($value) + { + if(gettype($value)===$this->type || $value===null || $value instanceof TDbExpression) + return $value; + if($value==='') + return $this->type==='string' ? '' : null; + switch($this->type) + { + case 'integer': return (integer)$value; + case 'boolean': return (boolean)$value; + case 'double': return (double)$value; + case 'string': return (string)$value; + default: return $value; + } + } +} diff --git a/framework/Testing/Data/Schema/TDbCommandBuilder.php b/framework/Testing/Data/Schema/TDbCommandBuilder.php new file mode 100755 index 00000000..7a196439 --- /dev/null +++ b/framework/Testing/Data/Schema/TDbCommandBuilder.php @@ -0,0 +1,656 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbSchema'); +prado::using('System.Testing.Data.Schema.TDbCriteria'); + +/** + * TDbCommandBuilder provides basic methods to create query commands for tables. + * + * @author Qiang Xue + * @version $Id: TDbCommandBuilder.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema + * @since 1.0 + */ +class TDbCommandBuilder extends TComponent +{ + const PARAM_PREFIX=':yp'; + + private $_schema; + private $_connection; + + /** + * @param TDbSchema the schema for this command builder + */ + public function __construct($schema) + { + $this->_schema=$schema; + $this->_connection=$schema->getDbConnection(); + } + + /** + * @return TDbConnection database connection. + */ + public function getDbConnection() + { + return $this->_connection; + } + + /** + * @return TDbSchema the schema for this command builder. + */ + public function getSchema() + { + return $this->_schema; + } + + /** + * Returns the last insertion ID for the specified table. + * @param mixed the table schema ({@link TDbTableSchema}) 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 the table schema ({@link TDbTableSchema}) or the table name (string). + * @param TDbCriteria the query criteria + * @return TDbCommand query command. + */ + public function createFindCommand($table,$criteria) + { + $this->ensureTable($table); + $select=is_array($criteria->select) ? implode(', ',$criteria->select) : $criteria->select; + $sql="SELECT {$select} 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 a COUNT(*) command for a single table. + * @param mixed the table schema ({@link TDbTableSchema}) or the table name (string). + * @param TDbCriteria the query criteria + * @return TDbCommand query command. + */ + public function createCountCommand($table,$criteria) + { + $this->ensureTable($table); + $criteria->select='COUNT(*)'; + return $this->createFindCommand($table,$criteria); + } + + /** + * Creates a DELETE command. + * @param mixed the table schema ({@link TDbTableSchema}) or the table name (string). + * @param TDbCriteria the query criteria + * @return TDbCommand 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 the table schema ({@link TDbTableSchema}) or the table name (string). + * @param array data to be inserted (column name=>column value). If a key is not a valid column name, the corresponding value will be ignored. + * @return TDbCommand 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 TDbExpression) + $placeholders[]=(string)$value; + 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).')'; + $command=$this->_connection->createCommand($sql); + + foreach($values as $name=>$value) + $command->bindValue($name,$value); + + return $command; + } + + /** + * Creates an UPDATE command. + * @param mixed the table schema ({@link TDbTableSchema}) or the table name (string). + * @param array list of columns to be updated (name=>value) + * @param TDbCriteria the query criteria + * @return TDbCommand 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 TDbExpression) + $fields[]=$column->rawName.'='.(string)$value; + 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 TDbException('No columns are being updated for table "{0}".', + $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 the table schema ({@link TDbTableSchema}) or the table name (string). + * @param TDbCriteria the query criteria + * @param array counters to be updated (counter increments/decrements indexed by column names.) + * @return TDbCommand 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 TDbException('No counter columns are being updated for table "{0}".', + $table->name); + } + + /** + * Creates a command based on a given SQL statement. + * @param string the explicitly specified SQL statement + * @param array parameters that will be bound to the SQL statement + * @return TDbCommand 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 the SQL statement to be altered + * @param string 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 the SQL statement without WHERE clause + * @param string 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 statement without ORDER BY. + * @param string 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 query string without LIMIT and OFFSET. + * @param integer maximum number of rows, -1 to ignore limit. + * @param integer 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 query string without GROUP BY. + * @param string 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 query string without HAVING + * @param string HAVING + * @return string SQL with HAVING + * @since 1.0.1 + */ + public function applyHaving($sql,$having) + { + if($having!=='') + return $sql.' HAVING '.$having; + else + return $sql; + } + + /** + * Binds parameter values for an SQL command. + * @param TDbCommand database command + * @param array 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 the table schema ({@link TDbTableSchema}) or the table name (string). + * @param mixed 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 TDbCriteria} object; + * Otherwise, it should be an instance of {@link TDbCriteria}. + * @param array 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 TDbCriteria::params} to set parameters. + * @return TDbCriteria the created query criteria + * @throws CException if the condition is not string, array and TDbCriteria + */ + public function createCriteria($condition='',$params=array()) + { + if(is_array($condition)) + $criteria=new TDbCriteria($condition); + else if($condition instanceof TDbCriteria) + $criteria=clone $condition; + else + { + $criteria=new TDbCriteria; + $criteria->condition=$condition; + $criteria->params=$params; + } + return $criteria; + } + + /** + * Creates a query criteria with the specified primary key. + * @param mixed the table schema ({@link TDbTableSchema}) or the table name (string). + * @param mixed 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 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 TDbCriteria}; + * Otherwise, it should be an instance of {@link TDbCriteria}. + * @param array 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 TDbCriteria::params} to set parameters. + * @return TDbCriteria the created query criteria + */ + public function createPkCriteria($table,$pk,$condition='',$params=array()) + { + $this->ensureTable($table); + $criteria=$this->createCriteria($condition,$params); + 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); + 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 the table schema ({@link TDbTableSchema}) or the table name (string). + * @param array list of primary key values to be selected within + * @param string 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 the table schema ({@link TDbTableSchema}) or the table name (string). + * @param array column values that should be matched in the query (name=>value) + * @param mixed 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 TDbCriteria}; + * Otherwise, it should be an instance of {@link TDbCriteria}. + * @param array 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 TDbCriteria::params} to set parameters. + * @return TDbCriteria the created query criteria + */ + public function createColumnCriteria($table,$columns,$condition='',$params=array()) + { + $this->ensureTable($table); + $criteria=$this->createCriteria($condition,$params); + $bindByPosition=isset($criteria->params[0]); + $conditions=array(); + $values=array(); + $i=0; + foreach($columns as $name=>$value) + { + if(($column=$table->getColumn($name))!==null) + { + if($value!==null) + { + if($bindByPosition) + { + $conditions[]=$table->rawName.'.'.$column->rawName.'=?'; + $values[]=$value; + } + else + { + $conditions[]=$table->rawName.'.'.$column->rawName.'='.self::PARAM_PREFIX.$i; + $values[self::PARAM_PREFIX.$i]=$value; + $i++; + } + } + else + $conditions[]=$table->rawName.'.'.$column->rawName.' IS NULL'; + } + else + throw new TDbException('Table "{0}" does not have a column named "{1}".', + $table->name,$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 the table schema ({@link TDbTableSchema}) or the table name (string). + * @param array list of column names for potential search condition. + * @param mixed search keywords. This can be either a string with space-separated keywords or an array of keywords. + * @param string optional column prefix (with dot at the end). If null, the table name will be used as the prefix. + * @param boolean whether the search is case-sensitive. Defaults to true. This parameter + * has been available since version 1.0.4. + * @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 TDbException('Table "{0}" does not have a column named "{0}".', + $table->name,$name); + $condition=array(); + foreach($keywords as $keyword) + { + 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 the table schema ({@link TDbTableSchema}) or the table name (string). + * @param mixed 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 list of key values to be selected within + * @param string column prefix (ended with dot). If null, it will be the table name + * @return string the expression for selection + * @since 1.0.4 + */ + 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 TDbException('Table "{0}" does not have a column named "{1}".', + $table->name, $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 TDbException('Table "{0}" does not have a column named "{1}".', + $table->name, $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 TDbException('The value for the column "{1}" is not supplied when querying the table "{0}".', + $table->name,$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 TDbException('Column name must be either a string or an array.'); + } + + /** + * Generates the expression for selecting rows with specified composite key values. + * @param TDbTableSchema the table schema + * @param array list of primary key values to be selected within + * @param string column prefix (ended with dot) + * @return string the expression for selection + * @since 1.0.4 + */ + 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 schema ({@link TDbTableSchema}) or table name (string). + * If this refers to a valid table name, this parameter will be returned with the corresponding table schema. + * @throws TDbException if the table name is not valid + * @since 1.0.4 + */ + protected function ensureTable(&$table) + { + if(is_string($table) && ($table=$this->_schema->getTable($tableName=$table))===null) + throw new TDbException('Table "{0}" does not exist.', + $tableName); + } +} diff --git a/framework/Testing/Data/Schema/TDbCriteria.php b/framework/Testing/Data/Schema/TDbCriteria.php new file mode 100755 index 00000000..585765e9 --- /dev/null +++ b/framework/Testing/Data/Schema/TDbCriteria.php @@ -0,0 +1,166 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * TDbCriteria represents a query criteria, such as conditions, ordering by, limit/offset. + * + * @author Qiang Xue + * @version $Id: TDbCriteria.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.schema + * @since 1.0 + */ +class TDbCriteria +{ + /** + * @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 string query condition. This refers to the WHERE clause in an SQL statement. + * For example, age>31 AND team=1. + */ + public $condition=''; + /** + * @var array list of query parameter values indexed by parameter placeholders. + * For example, array(':name'=>'Dan', ':age'=>31). + */ + 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, 'projectID, teamID'. + */ + public $group=''; + /** + * @var string how to join with other tables. This refers to the JOIN clause in an SQL statement. + * For example, 'LEFT JOIN users ON users.id=authorID'. + */ + public $join=''; + /** + * @var string the condition to be applied with GROUP-BY clause. + * For example, 'SUM(revenue)<50000'. + * @since 1.0.1 + */ + public $having=''; + + /** + * Constructor. + * @param array criteria initial property values (indexed by property name) + */ + public function __construct($data=array()) + { + foreach($data as $name=>$value) + $this->$name=$value; + } + + /** + * 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 TDbCriteria the criteria to be merged with. + * @param boolean whether to use 'AND' to merge condition and having options. + * If false, 'OR' will be used instead. Defaults to 'AND'. This parameter has been + * available since version 1.0.6. + * @since 1.0.5 + */ + 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($this->order!==$criteria->order) + { + if($this->order==='') + $this->order=$criteria->order; + else if($criteria->order!=='') + $this->order.=', '.$criteria->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})"; + } + } + + /** + * @return array the array representation of the criteria + * @since 1.0.6 + */ + public function toArray() + { + $result=array(); + foreach(array('select', 'condition', 'params', 'limit', 'offset', 'order', 'group', 'join', 'having') as $name) + $result[$name]=$this->$name; + return $result; + } +} diff --git a/framework/Testing/Data/Schema/TDbExpression.php b/framework/Testing/Data/Schema/TDbExpression.php new file mode 100755 index 00000000..a1819000 --- /dev/null +++ b/framework/Testing/Data/Schema/TDbExpression.php @@ -0,0 +1,49 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * TDbExpression represents a DB expression that does not need escaping. + * TDbExpression is mainly used in {@link CActiveRecord} as attribute values. + * When inserting or updating a {@link CActiveRecord}, attribute values of + * type TDbExpression 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. + * + * @author Qiang Xue + * @version $Id: TDbExpression.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema + * @since 1.0.2 + */ +class TDbExpression extends TComponent +{ + /** + * @var string the DB expression + */ + public $expression; + + /** + * Constructor. + * @param string the DB expression + */ + public function __construct($expression) + { + $this->expression=$expression; + } + + /** + * String magic method + * @return string the DB expression + */ + public function __toString() + { + return $this->expression; + } +} diff --git a/framework/Testing/Data/Schema/TDbSchema.php b/framework/Testing/Data/Schema/TDbSchema.php new file mode 100755 index 00000000..2c568e7e --- /dev/null +++ b/framework/Testing/Data/Schema/TDbSchema.php @@ -0,0 +1,193 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbCommandBuilder'); + +/** + * TDbSchema is the base class for retrieving metadata information. + * + * @author Qiang Xue + * @version $Id: TDbSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema + * @since 1.0 + */ +abstract class TDbSchema extends TComponent +{ + private $_tableNames=array(); + private $_tables=array(); + private $_connection; + private $_builder; + private $_cacheExclude=array(); + + /** + * Creates a table instance representing the metadata for the named table. + * @return TDbTableSchema driver dependent table metadata, null if the table does not exist. + */ + abstract protected function createTable($name); + + /** + * Constructor. + * @param TDbConnection database connection. + */ + public function __construct($conn) + { + $conn->setActive(true); + $this->_connection=$conn; + foreach($conn->schemaCachingExclude as $name) + $this->_cacheExclude[$name]=true; + } + + /** + * @return TDbConnection database connection. The connection is active. + */ + public function getDbConnection() + { + return $this->_connection; + } + + /** + * Obtains the metadata for the named table. + * @param string table name + * @return TDbTableSchema table metadata. Null if the named table does not exist. + */ + public function getTable($name) + { + if(isset($this->_tables[$name])) + return $this->_tables[$name]; + else if(!isset($this->_cacheExclude[$name]) && ($duration=$this->_connection->schemaCachingDuration)>0 && ($cache=prado::getApplication()->getCache())!==null) + { + $key='prado:dbschema'.$this->_connection->connectionString.':'.$this->_connection->username.':'.$name; + if(($table=$cache->get($key))===false) + { + $table=$this->createTable($name); + $cache->set($key,$table,$duration); + } + return $this->_tables[$name]=$table; + } + else + return $this->_tables[$name]=$this->createTable($name); + } + + /** + * Returns the metadata for all tables in the database. + * @param string 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 TDbTableSchema} (or its child class). + * The array keys are table names. + * @since 1.0.2 + */ + public function getTables($schema='') + { + $tables=array(); + foreach($this->getTableNames($schema) as $name) + $tables[$name]=$this->getTable($name); + return $tables; + } + + /** + * Returns all table names in the database. + * @param string 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. + * @since 1.0.2 + */ + public function getTableNames($schema='') + { + if(!isset($this->_tableNames[$schema])) + $this->_tableNames[$schema]=$this->findTableNames($schema); + return $this->_tableNames[$schema]; + } + + /** + * @return TDbCommandBuilder 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() + { + $this->_tables=array(); + $this->_builder=null; + } + + /** + * Quotes a table name for use in a query. + * @param string table name + * @return string the properly quoted table name + */ + public function quoteTableName($name) + { + return "'".$name."'"; + } + + /** + * Quotes a column name for use in a query. + * @param string column name + * @return string the properly quoted column name + */ + public function quoteColumnName($name) + { + return '"'.$name.'"'; + } + + /** + * Compares two table names. + * The table names can be either quoted or unquoted. This method + * will consider both cases. + * @param string table name 1 + * @param string 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); + return $name1===$name2; + } + + /** + * Creates a command builder for the database. + * This method may be overridden by child classes to create a DBMS-specific command builder. + * @return TDbCommandBuilder command builder instance + */ + protected function createCommandBuilder() + { + return new TDbCommandBuilder($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 implemenation simply throws an exception. + * @param string 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. + * @since 1.0.2 + */ + protected function findTableNames($schema='') + { + throw new TDbException('{0} does not support fetching all table names.', + get_class($this)); + } +} diff --git a/framework/Testing/Data/Schema/TDbTableSchema.php b/framework/Testing/Data/Schema/TDbTableSchema.php new file mode 100755 index 00000000..8472059b --- /dev/null +++ b/framework/Testing/Data/Schema/TDbTableSchema.php @@ -0,0 +1,76 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * TDbTableSchema 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. + * + * TDbTableSchema provides the following information about a table: + *
      + *
    • {@link name}
    • + *
    • {@link rawName}
    • + *
    • {@link columns}
    • + *
    • {@link primaryKey}
    • + *
    • {@link foreignKeys}
    • + *
    • {@link sequenceName}
    • + *
    + * + * @author Qiang Xue + * @version $Id: TDbTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema + * @since 1.0 + */ +class TDbTableSchema extends TComponent +{ + /** + * @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 TDbColumnSchema 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 column name + * @return TDbColumnSchema 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/Testing/Data/Schema/mssql/TMssqlColumnSchema.php b/framework/Testing/Data/Schema/mssql/TMssqlColumnSchema.php new file mode 100755 index 00000000..0123f183 --- /dev/null +++ b/framework/Testing/Data/Schema/mssql/TMssqlColumnSchema.php @@ -0,0 +1,57 @@ + + * @author Christophe Boulain + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbColumnSchema'); + +/** + * TMssqlColumnSchema class describes the column meta data of a MSSQL table. + * + * @author Qiang Xue + * @author Christophe Boulain + * @version $Id: TMssqlColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.mssql + * @since 1.0.4 + */ +class TMssqlColumnSchema extends TDbColumnSchema +{ + /** + * Extracts the PHP type from DB type. + * @param string DB type + */ + protected function extractType($dbType) + { + if(strpos($dbType,'bigint')!==false || strpos($dbType,'float')!==false || strpos($dbType,'real')!==false) + $this->type='double'; + else if(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'; + } + + 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 the column's DB type + */ + protected function extractLimit($dbType) + { + } +} diff --git a/framework/Testing/Data/Schema/mssql/TMssqlCommandBuilder.php b/framework/Testing/Data/Schema/mssql/TMssqlCommandBuilder.php new file mode 100755 index 00000000..17a6f6ab --- /dev/null +++ b/framework/Testing/Data/Schema/mssql/TMssqlCommandBuilder.php @@ -0,0 +1,303 @@ + + * @author Christophe Boulain + * @author Wei Zhuo + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using ('System.Testing.Data.schame.TDbCommandBuilder'); + +/** + * TMssqlCommandBuilder provides basic methods to create query commands for tables for Mssql Servers. + * + * @author Qiang Xue + * @author Christophe Boulain + * @author Wei Zhuo + * @version $Id: TMssqlCommandBuilder.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.schema.mssql + * @since 1.0.4 + */ +class TMssqlCommandBuilder extends TDbCommandBuilder +{ + /** + * Returns the last insertion ID for the specified table. + * Override parent implemantation since PDO mssql driver does not provide this method + * @param TDbTableSchema the table metadata + * @return mixed last insertion id. Null is returned if no sequence name. + */ + public function getLastInsertID($table) + { + if($table->sequenceName!==null) + return $this->getDbConnection()->createCommand('SELECT SCOPE_IDENTITY()')->queryScalar(); + else + return null; + } + + /** + * Creates a COUNT(*) command for a single table. + * Override parent implementation to remove the order clause of criteria if it exists + * @param TDbTableSchema the table metadata + * @param TDbCriteria the query criteria + * @return TDbCommand query command. + */ + public function createCountCommand($table,$criteria) + { + $criteria->order=''; + return parent::createCountCommand($table, $criteria); + } + + /** + * 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 TDbTableSchema the table metadata + * @param TDbCriteria the query criteria + * @return TDbCommand query command. + */ + public function createFindCommand($table,$criteria) + { + $criteria=$this->checkCriteria($table,$criteria); + return parent::createFindCommand($table,$criteria); + + } + + /** + * Creates an UPDATE command. + * Override parent implementation because mssql don't want to update an identity column + * @param TDbTableSchema the table metadata + * @param array list of columns to be updated (name=>value) + * @param TDbCriteria the query criteria + * @return TDbCommand update command. + */ + public function createUpdateCommand($table,$data,$criteria) + { + $criteria=$this->checkCriteria($table,$criteria); + $fields=array(); + $values=array(); + $bindByPosition=isset($criteria->params[0]); + foreach($data as $name=>$value) + { + if(($column=$table->getColumn($name))!==null) + { + if ($table->sequenceName !== null && $column->isPrimaryKey === true) continue; + if($value instanceof TDbExpression) + $fields[]=$column->rawName.'='.(string)$value; + else if($bindByPosition) + { + $fields[]=$column->rawName.'=?'; + $values[]=$column->typecast($value); + } + else + { + $fields[]=$column->rawName.'=:'.$name; + $values[':'.$name]=$column->typecast($value); + } + } + } + if($fields===array()) + throw new TDbException('No columns are being updated for table "{0}".', + $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 TDbTableSchema the table metadata + * @param TDbCriteria the query criteria + * @return TDbCommand 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 TDbTableSchema the table metadata + * @param TDbCriteria the query criteria + * @param array counters to be updated (counter increments/decrements indexed by column names.) + * @return TDbCommand 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 + * + * + * 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) + * + * + * Regular expressions are used to alter the SQL query. The resulting SQL query + * may be malformed for complex queries. The following restrictions apply + * + *
      + *
    • + * In particular, commas should NOT + * be used as part of the ordering expression or identifier. Commas must only be + * used for separating the ordering clauses. + *
    • + *
    • + * 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. + *
    • + *
    • + * No clauses should follow the ORDER BY clause, e.g. no COMPUTE or FOR clauses. + *
    • + * + * @param string SQL query string. + * @param integer maximum number of rows, -1 to ignore limit. + * @param integer row offset, -1 to ignore offset. + * @return string SQL with limit and offset. + * + * @author Wei Zhuo + */ + 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 query + * @param integer $limit > 0 + * @param integer $offset > 0 + * @return sql modified sql query applied with limit and offset. + * + * @author Wei Zhuo + */ + 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); + $reverseOrdering = $this->joinOrdering($this->reverseDirection($ordering)); + $sql = "SELECT * FROM (SELECT TOP {$limit} * FROM ($sql) as [__inner top table__] {$reverseOrdering}) as [__outer top table__] {$orginalOrdering}"; + return $sql; + } + + /** + * Base on simplified syntax http://msdn2.microsoft.com/en-us/library/aa259187(SQL.80).aspx + * + * @param string $sql + * @return array ordering expression as key and ordering direction as value + * + * @author Wei Zhuo + */ + 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) + { + $ordering[$subs[1][0]] = $subs[2][0]; + } + //else what? + } + else + $ordering[trim($part)] = 'ASC'; + } + } + return $ordering; + } + + /** + * @param array ordering obtained from findOrdering() + * @return string concat the orderings + * + * @author Wei Zhuo + */ + protected function joinOrdering($orders) + { + if(count($orders)>0) + { + $str=array(); + foreach($orders as $column => $direction) + $str[] = $column.' '.$direction; + return 'ORDER BY '.implode(', ', $str); + } + } + + /** + * @param array original ordering + * @return array ordering with reversed direction. + * + * @author Wei Zhuo + */ + 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 TMssqlTableSchema table schema + * @param TDbCriteria criteria + * @return TDbCrireria 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; + } +} diff --git a/framework/Testing/Data/Schema/mssql/TMssqlPdoAdapter.php b/framework/Testing/Data/Schema/mssql/TMssqlPdoAdapter.php new file mode 100755 index 00000000..6f8777dd --- /dev/null +++ b/framework/Testing/Data/Schema/mssql/TMssqlPdoAdapter.php @@ -0,0 +1,74 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 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 + * @version $Id: TMssqlPdoAdapter.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.schema.mssql + * @since 1.0.4 + */ +class TMssqlPdoAdapter extends PDO +{ + /** + * Get the last inserted id value + * MSSQL doesn't support sequence, so, argument is ignored + * + * @param string sequence name. Defaults to null + * @return int last inserted id + */ + public function lastInsertId ($sequence=NULL) + { + return $this->query('SELECT SCOPE_IDENTITY()')->fetchColumn(); + } + + /** + * 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/Testing/Data/Schema/mssql/TMssqlSchema.php b/framework/Testing/Data/Schema/mssql/TMssqlSchema.php new file mode 100755 index 00000000..1b3d815c --- /dev/null +++ b/framework/Testing/Data/Schema/mssql/TMssqlSchema.php @@ -0,0 +1,312 @@ + + * @author Christophe Boulain + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbSchema'); + +/** + * TMssqlSchema is the class for retrieving metadata information from a MS SQL Server database. + * + * @author Qiang Xue + * @author Christophe Boulain + * @version $Id: TMssqlSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.mssql + * @since 1.0.4 + */ +class TMssqlSchema extends TDbSchema +{ + const DEFAULT_SCHEMA='dbo'; + + + /** + * Quotes a table name for use in a query. + * @param string table name + * @return string the properly quoted table name + */ + public function quoteTableName($name) + { + if (strpos($name,'.')===false) + return '['.$name.']'; + $names=explode('.',$name); + foreach ($names as &$n) + $n = '['.$n.']'; + return implode('.',$names); + } + + /** + * Quotes a column name for use in a query. + * @param string column name + * @return string the properly quoted column name + */ + public function quoteColumnName($name) + { + return '['.$name.']'; + } + + /** + * Compares two table names. + * The table names can be either quoted or unquoted. This method + * will consider both cases. + * @param string table name 1 + * @param string 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); + $name1=str_replace(array('[',']'),'',$name2); + return parent::compareTableNames(strtolower($name1),strtolower($name2)); + } + + /** + * Creates a table instance representing the metadata for the named table. + * @return CMysqlTableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function createTable($name) + { + $table=new TMssqlTableSchema; + $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 CMysqlTableSchema the table instance + * @param string 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 TMssqlTableSchema 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 = <<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 TMssqlTableSchema 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 = <<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 CMysqlTableSchema the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $where=array(); + $where[]="TABLE_NAME='".$table->name."'"; + if (isset($table->catalogName)) + $where[]="TABLE_CATALOG='".$table->catalogName."'"; + if (isset($table->schemaName)) + $where[]="TABLE_SCHEMA='".$table->schemaName."'"; + $sql="SELECT *, columnproperty(object_id(table_schema+'.'+table_name), column_name, 'IsIdentity') as IsIdentity ". + "FROM INFORMATION_SCHEMA.COLUMNS 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 ($column['IsIdentity']==1 && $table->sequenceName===null) + $table->sequenceName=''; + + } + return true; + } + + /** + * Creates a table column. + * @param array column metadata + * @return TDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new TMssqlColumnSchema; + $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->init($column['DATA_TYPE'],$column['COLUMN_DEFAULT']); + return $c; + } + + /** + * Returns all table names in the database. + * @return array all table names in the database. + * @since 1.0.4 + */ + protected function findTableNames($schema='') + { + if($schema==='') + $schema=self::DEFAULT_SCHEMA; + $sql=<<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 TDbCommandBuilder command builder instance + */ + protected function createCommandBuilder() + { + return new TMssqlCommandBuilder($this); + } +} diff --git a/framework/Testing/Data/Schema/mssql/TMssqlTableSchema.php b/framework/Testing/Data/Schema/mssql/TMssqlTableSchema.php new file mode 100755 index 00000000..b6584a32 --- /dev/null +++ b/framework/Testing/Data/Schema/mssql/TMssqlTableSchema.php @@ -0,0 +1,35 @@ + + * @author Christophe Boulain + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.TDbTableSchema'); + +/** + * TMssqlTableSchema represents the metadata for a MSSQL table. + * + * @author Qiang Xue + * @author Christophe Boulain + * @version $Id: TMssqlTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.mssql + * @since 1.0.4 + */ +class TMssqlTableSchema extends TDbTableSchema +{ + /** + * @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/Testing/Data/Schema/mysql/TMysqlColumnSchema.php b/framework/Testing/Data/Schema/mysql/TMysqlColumnSchema.php new file mode 100755 index 00000000..9e82ada5 --- /dev/null +++ b/framework/Testing/Data/Schema/mysql/TMysqlColumnSchema.php @@ -0,0 +1,46 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbColumnSchema'); + +/** + * TMysqlColumnSchema class describes the column meta data of a MySQL table. + * + * @author Qiang Xue + * @version $Id: TMysqlColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.mysql + * @since 1.0 + */ +class TMysqlColumnSchema extends TDbColumnSchema +{ + /** + * Extracts the PHP type from DB type. + * @param string DB type + */ + protected function extractType($dbType) + { + if(strpos($dbType,'bigint')!==false || strpos($dbType,'float')!==false || strpos($dbType,'double')!==false) + $this->type='double'; + else if(strpos($dbType,'bool')!==false || $dbType==='tinyint(1)') + $this->type='boolean'; + else if(strpos($dbType,'int')!==false || strpos($dbType,'bit')!==false) + $this->type='integer'; + else + $this->type='string'; + } + + protected function extractDefault($defaultValue) + { + if($this->dbType==='timestamp' && $defaultValue==='CURRENT_TIMESTAMP') + $this->defaultValue=null; + else + parent::extractDefault($defaultValue); + } +} diff --git a/framework/Testing/Data/Schema/mysql/TMysqlSchema.php b/framework/Testing/Data/Schema/mysql/TMysqlSchema.php new file mode 100755 index 00000000..910bf915 --- /dev/null +++ b/framework/Testing/Data/Schema/mysql/TMysqlSchema.php @@ -0,0 +1,205 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbSchema'); +prado::using('System.Testing.Data.Schema.mysql.TMysqlTableSchema'); +prado::using('System.Testing.Data.Schema.mysql.TMysqlColumnSchema'); + +/** + * TMysqlSchema is the class for retrieving metadata information from a MySQL database (version 4.1.x and 5.x). + * + * @author Qiang Xue + * @version $Id: TMysqlSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.mysql + * @since 1.0 + */ +class TMysqlSchema extends TDbSchema +{ + private $_tableNames; + private $_schemaNames; + + /** + * Quotes a table name for use in a query. + * @param string table name + * @return string the properly quoted table name + */ + public function quoteTableName($name) + { + return '`'.$name.'`'; + } + + /** + * Quotes a column name for use in a query. + * @param string column name + * @return string the properly quoted column name + */ + public function quoteColumnName($name) + { + return '`'.$name.'`'; + } + + /** + * Compares two table names. + * The table names can be either quoted or unquoted. This method + * will consider both cases. + * @param string table name 1 + * @param string 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)); + } + + /** + * Creates a table instance representing the metadata for the named table. + * @return TMysqlTableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function createTable($name) + { + $table=new TMysqlTableSchema; + $this->resolveTableNames($table,$name); + + if($this->findColumns($table)) + { + $this->findConstraints($table); + return $table; + } + else + return null; + } + + /** + * Generates various kinds of table names. + * @param TMysqlTableSchema the table instance + * @param string 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 TMysqlTableSchema 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(strpos(strtolower($column['Extra']),'auto_increment')!==false) + $table->sequenceName=''; + } + } + return true; + } + + /** + * Creates a table column. + * @param array column metadata + * @return TDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new TMysqlColumnSchema; + $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']); + 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 TMysqlTableSchema 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; + } + $foreign = array(); + 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. + * @return array all table names in the database. + * @since 1.0.2 + */ + 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; + } +} diff --git a/framework/Testing/Data/Schema/mysql/TMysqlTableSchema.php b/framework/Testing/Data/Schema/mysql/TMysqlTableSchema.php new file mode 100755 index 00000000..87f5eefd --- /dev/null +++ b/framework/Testing/Data/Schema/mysql/TMysqlTableSchema.php @@ -0,0 +1,26 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ +prado::using('System.Testing.Data.Schema.TDbTableSchema'); +/** + * TMysqlTableSchema represents the metadata for a MySQL table. + * + * @author Qiang Xue + * @version $Id: TMysqlTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.mysql + * @since 1.0 + */ +class TMysqlTableSchema extends TDbTableSchema +{ + /** + * @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/Testing/Data/Schema/oci/TOciColumnSchema.php b/framework/Testing/Data/Schema/oci/TOciColumnSchema.php new file mode 100755 index 00000000..d8c77e16 --- /dev/null +++ b/framework/Testing/Data/Schema/oci/TOciColumnSchema.php @@ -0,0 +1,56 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbColumnSchema'); + +/** + * TOciColumnSchema class describes the column meta data of a Oracle table. + * + * @author Ricardo Grana + * @version $Id: TOciColumnSchema.php + * @package System.Testing.Data.Schema.oci + * @since 1.0.5 + */ +class TOciColumnSchema extends TDbColumnSchema +{ + /** + * Extracts the PHP type from DB type. + * @param string DB type + */ + protected function extractOraType($dbType){ + if(strpos($dbType,'FLOAT')!==false) return 'double'; + + if ((strpos($dbType,'NUMBER')!==false) or + (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 'string'; + } + } + protected function extractType($dbType) + { + $this->type=$this->extractOraType($dbType); + } + + protected function extractDefault($defaultValue) + { + if(strpos($dbType,'timestamp')!==false) + $this->defaultValue=null; + else + parent::extractDefault($defaultValue); + } +} diff --git a/framework/Testing/Data/Schema/oci/TOciCommandBuilder.php b/framework/Testing/Data/Schema/oci/TOciCommandBuilder.php new file mode 100755 index 00000000..ed3e8c3b --- /dev/null +++ b/framework/Testing/Data/Schema/oci/TOciCommandBuilder.php @@ -0,0 +1,122 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbCommandBuilder'); + +/** + * TOciCommandBuilder provides basic methods to create query commands for tables. + * + * @author Ricardo Grana + * @version $Id: TOciCommandBuilder.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.oci + * @since 1.0.5 + */ +class TOciCommandBuilder extends TDbCommandBuilder +{ + /** + * @var integer the last insertion ID + */ + public $returnID; + + /** + * Returns the last insertion ID for the specified table. + * @param mixed the table schema ({@link TDbTableSchema}) 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 query string without LIMIT and OFFSET. + * @param integer maximum number of rows, -1 to ignore limit. + * @param integer 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 = <<column value). If a key is not a valid column name, the corresponding value will be ignored. + * @return TDbCommand insert command + */ + public function createInsertCommand($table,$data) + { + $this->ensureTable($table); + $fields=array(); + $values=array(); + $placeholders=array(); + foreach($data as $name=>$value) + { + if(($column=$table->getColumn($name))!==null && ($value!==null || $column->allowNull)) + { + $fields[]=$column->rawName; + if($value instanceof TDbExpression) + $placeholders[]=(string)$value; + else + { + $placeholders[]=':'.$name; + $values[':'.$name]=$column->typecast($value); + } + } + } + + $sql="INSERT INTO {$table->rawName} (".implode(', ',$fields).') VALUES ('.implode(', ',$placeholders).')'; + + if(is_string($table->primaryKey)) + { + $sql.=" RETURNING ".$table->primaryKey." 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; + } +} diff --git a/framework/Testing/Data/Schema/oci/TOciSchema.php b/framework/Testing/Data/Schema/oci/TOciSchema.php new file mode 100755 index 00000000..1253dac6 --- /dev/null +++ b/framework/Testing/Data/Schema/oci/TOciSchema.php @@ -0,0 +1,278 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbSchema'); + +/** + * TOciSchema is the class for retrieving metadata information from a PostgreSQL database. + * + * @author Ricardo Grana + * @version $Id: TOciSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.oci + * @since 1.0.5 + */ +class TOciSchema extends TDbSchema +{ + private $_defaultSchema = ''; + private $_sequences=array(); + + /** + * Quotes a table name for use in a query. + * @param string table name + * @return string the properly quoted table name + */ + public function quoteTableName($name) + { + return $name; + } + + /** + * Quotes a column name for use in a query. + * @param string column name + * @return string the properly quoted column name + */ + public function quoteColumnName($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 TDbCommandBuilder command builder instance + */ + protected function createCommandBuilder() + { + return new TOciCommandBuilder($this); + } + + /** + * @param string 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 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]); + } + + /** + * Creates a table instance representing the metadata for the named table. + * @return TDbTableSchema driver dependent table metadata. + */ + protected function createTable($name) + { + $table=new TOciTableSchema; + $this->resolveTableNames($table,$name); + + if(!$this->findColumns($table)) + return null; + $this->findConstraints($table); + + return $table; + } + + /** + * Generates various kinds of table names. + * @param TOciTableSchema the table instance + * @param string 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 TOciTableSchema the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + list($schemaName,$tableName) = $this->getSchemaTableName($table->name); + + $sql=<< 0 then ',' || a.data_scale else '' end + || ')' + when data_type = 'DATE' 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' + 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; + } + } + return true; + } + + /** + * Creates a table column. + * @param array column metadata + * @return TDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new TOciColumnSchema; + $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 TOciTableSchema the table metadata + */ + protected function findConstraints($table) + { + $sql=<< '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"], array($row["COLUMN_REF"])); + if(isset($table->columns[$name])) + $table->columns[$name]->isForeignKey=true; + } + + } + } + + + /** + * Returns all table names in the database. + * @return array all table names in the database. + */ + protected function findTableNames($schema='') + { + if($schema==='') + { + $sql=<<getDbConnection()->createCommand($sql); + } + else + { + $sql=<<getDbConnection()->createCommand($sql); + $command->bindParam(':schema',$schema); + } + + $rows=$command->queryAll(); + $names=array(); + foreach($rows as $row) + { + if($schema===$this->getDefaultSchema()) + $names[]=$row['table_name']; + else + $names[]=$row['schema_name'].'.'.$row['table_name']; + } + return $names; + } +} diff --git a/framework/Testing/Data/Schema/oci/TOciTableSchema.php b/framework/Testing/Data/Schema/oci/TOciTableSchema.php new file mode 100755 index 00000000..4376c528 --- /dev/null +++ b/framework/Testing/Data/Schema/oci/TOciTableSchema.php @@ -0,0 +1,28 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbTableSchame'); + +/** + * TOciTableSchema represents the metadata for a Ora table. + * + * @author Ricardo Grana + * @version $Id: TOciTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.oci + * @since 1.0.5 + */ +class TOciTableSchema extends TDbTableSchema +{ + /** + * @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/Testing/Data/Schema/pgsql/TPgsqlColumnSchema.php b/framework/Testing/Data/Schema/pgsql/TPgsqlColumnSchema.php new file mode 100755 index 00000000..7872dd4a --- /dev/null +++ b/framework/Testing/Data/Schema/pgsql/TPgsqlColumnSchema.php @@ -0,0 +1,58 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbColumnSchema'); + +/** + * TPgsqlColumnSchema class describes the column meta data of a PostgreSQL table. + * + * @author Qiang Xue + * @version $Id: TPgsqlColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.pgsql + * @since 1.0 + */ +class TPgsqlColumnSchema extends TDbColumnSchema +{ + /** + * Extracts the PHP type from DB type. + * @param string DB type + */ + protected function extractType($dbType) + { + if(strpos($dbType,'integer')!==false || strpos($dbType,'oid')===0) + $this->type='integer'; + else if(strpos($dbType,'bool')!==false) + $this->type='boolean'; + else if(preg_match('/(real|float|double)/',$dbType)) + $this->type='double'; + else + $this->type='string'; + } + + /** + * Extracts the default value for the column. + * The value is typecasted to correct PHP type. + * @param mixed 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/Testing/Data/Schema/pgsql/TPgsqlSchema.php b/framework/Testing/Data/Schema/pgsql/TPgsqlSchema.php new file mode 100755 index 00000000..05e0fbfa --- /dev/null +++ b/framework/Testing/Data/Schema/pgsql/TPgsqlSchema.php @@ -0,0 +1,284 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbSchema'); + +/** + * TPgsqlSchema is the class for retrieving metadata information from a PostgreSQL database. + * + * @author Qiang Xue + * @version $Id: TPgsqlSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.pgsql + * @since 1.0 + */ +class TPgsqlSchema extends TDbSchema +{ + const DEFAULT_SCHEMA='public'; + private $_sequences=array(); + + /** + * Quotes a table name for use in a query. + * @param string table name + * @return string the properly quoted table name + */ + public function quoteTableName($name) + { + return '"'.$name.'"'; + } + + /** + * Creates a table instance representing the metadata for the named table. + * @return TDbTableSchema driver dependent table metadata. + */ + protected function createTable($name) + { + $table=new TPgsqlTableSchema; + $this->resolveTableNames($table,$name); + if(!$this->findColumns($table)) + return null; + $this->findConstraints($table); + + if(is_string($table->primaryKey) && isset($this->_sequences[$table->primaryKey])) + $table->sequenceName=$this->_sequences[$table->primaryKey]; + + return $table; + } + + /** + * Generates various kinds of table names. + * @param TPgsqlTableSchema the table instance + * @param string 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 TPgsqlTableSchema the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $sql=<< 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[$c->name]=$matches[1]; + else + $this->_sequences[$c->name]=$table->schemaName.'.'.$matches[1]; + } + } + return true; + } + + /** + * Creates a table column. + * @param array column metadata + * @return TDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new TPgsqlColumnSchema; + $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 TPgsqlTableSchema the table metadata + */ + protected function findConstraints($table) + { + $sql=<<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 TPgsqlTableSchema the table metadata + * @param string pgsql primary key index list + */ + protected function findPrimaryKey($table,$indices) + { + $indices=implode(', ',preg_split('/\s+/',$indices)); + $sql=<<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 TPgsqlTableSchema the table metadata + * @param string 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. + * @return array all table names in the database. + * @since 1.0.2 + */ + protected function findTableNames($schema='') + { + if($schema==='') + $schema=self::DEFAULT_SCHEMA; + $sql=<<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['schema_name'].'.'.$row['table_name']; + } + return $names; + } +} diff --git a/framework/Testing/Data/Schema/pgsql/TPgsqlTableSchema.php b/framework/Testing/Data/Schema/pgsql/TPgsqlTableSchema.php new file mode 100755 index 00000000..5e79b6f0 --- /dev/null +++ b/framework/Testing/Data/Schema/pgsql/TPgsqlTableSchema.php @@ -0,0 +1,27 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbTableSchema'); + +/** + * TPgsqlTable represents the metadata for a PostgreSQL table. + * + * @author Qiang Xue + * @version $Id: TPgsqlTableSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.pgsql + * @since 1.0 + */ +class TPgsqlTableSchema extends TDbTableSchema +{ + /** + * @var string name of the schema that this table belongs to. + */ + public $schemaName; +} diff --git a/framework/Testing/Data/Schema/sqlite/TSqliteColumnSchema.php b/framework/Testing/Data/Schema/sqlite/TSqliteColumnSchema.php new file mode 100755 index 00000000..9926e852 --- /dev/null +++ b/framework/Testing/Data/Schema/sqlite/TSqliteColumnSchema.php @@ -0,0 +1,35 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbColumnSchema'); + +/** + * TSqliteColumnSchema class describes the column meta data of a SQLite table. + * + * @author Qiang Xue + * @version $Id: TSqliteColumnSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.sqlite + * @since 1.0 + */ +class TSqliteColumnSchema extends TDbColumnSchema +{ + /** + * Extracts the default value for the column. + * The value is typecasted to correct PHP type. + * @param mixed 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($defaultValue); + } +} diff --git a/framework/Testing/Data/Schema/sqlite/TSqliteCommandBuilder.php b/framework/Testing/Data/Schema/sqlite/TSqliteCommandBuilder.php new file mode 100755 index 00000000..fa79b890 --- /dev/null +++ b/framework/Testing/Data/Schema/sqlite/TSqliteCommandBuilder.php @@ -0,0 +1,43 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbCommandBuilder'); + +/** + * TSqliteCommandBuilder provides basic methods to create query commands for SQLite tables. + * + * @author Qiang Xue + * @version $Id: TSqliteCommandBuilder.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.sqlite + * @since 1.0 + */ +class TSqliteCommandBuilder extends TDbCommandBuilder +{ + /** + * 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 TDbTableSchema the table schema + * @param array list of primary key values to be selected within + * @param string column prefix (ended with dot) + * @return string the expression for selection + * @since 1.0.4 + */ + 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/Testing/Data/Schema/sqlite/TSqliteSchema.php b/framework/Testing/Data/Schema/sqlite/TSqliteSchema.php new file mode 100755 index 00000000..dd3e73af --- /dev/null +++ b/framework/Testing/Data/Schema/sqlite/TSqliteSchema.php @@ -0,0 +1,134 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using('System.Testing.Data.Schema.TDbSchema'); +prado::using('System.Testing.Data.Schema.TDbTableSchema'); +prado::using('System.Testing.Data.Schema.sqlite.TSqliteColumnSchema'); +prado::using('System.Testing.Data.Schema.sqlite.TSqliteCommandBuilder'); + +/** + * TSqliteSchema is the class for retrieving metadata information from a SQLite (2/3) database. + * + * @author Qiang Xue + * @version $Id: TSqliteSchema.php 2679 2009-06-15 07:49:42Z Christophe.Boulain $ + * @package System.Testing.Data.Schema.sqlite + * @since 1.0 + */ +class TSqliteSchema extends TDbSchema +{ + /** + * Returns all table names in the database. + * @param string the schema of the tables. This is not used for sqlite database. + * @return array all table names in the database. + * @since 1.0.2 + */ + 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 TSqliteCommandBuilder command builder instance + */ + protected function createCommandBuilder() + { + return new TSqliteCommandBuilder($this); + } + + /** + * Creates a table instance representing the metadata for the named table. + * @return TDbTableSchema driver dependent table metadata. Null if the table does not exist. + */ + protected function createTable($name) + { + $db=$this->getDbConnection(); + + $table=new TDbTableSchema; + $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 TDbTableSchema 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=''; + + return true; + } + + /** + * Collects the foreign key column details for the given table. + * @param TDbTableSchema 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 metadata + * @return TDbColumnSchema normalized column metadata + */ + protected function createColumn($column) + { + $c=new TSqliteColumnSchema; + $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; + } +} diff --git a/framework/Testing/Data/TDataSourceConfig.php b/framework/Testing/Data/TDataSourceConfig.php new file mode 100755 index 00000000..356e7f17 --- /dev/null +++ b/framework/Testing/Data/TDataSourceConfig.php @@ -0,0 +1,168 @@ + + * @author Christophe Boulain configuration for database connections. + * + * Example usage: mysql connection + * + * + * + * + * + * + * + * + * Usage in php: + * + * class Home extends TPage + * { + * function onLoad($param) + * { + * $db = $this->Application->Modules['db1']->DbConnection; + * $db->createCommand('...'); //... + * } + * } + * + * + * The properties of are those of the class TDbConnection. + * Set {@link setConnectionClass} attribute for a custom database connection class + * that extends the TDbConnection class. + * + * @author Wei Zhuo + * @version $Id$ + * @package System.Testing.Data + * @since 3.1 + */ +class TDataSourceConfig extends TModule +{ + private $_connID=''; + private $_conn; + private $_connClass='System.Testing.Data.TDbConnection'; + + /** + * Initalize the database connection properties from attributes in tag. + * @param TXmlDocument xml configuration. + */ + public function init($xml) + { + if($this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) + { + if(isset($xml['database']) && is_array($xml['database'])) + { + $db=$this->getDbConnection(); + foreach($xml['database'] as $name=>$value) + $db->setSubProperty($name,$value); + } + } + else + { + if($prop=$xml->getElementByTagName('database')) + { + $db=$this->getDbConnection(); + foreach($prop->getAttributes() as $name=>$value) + $db->setSubproperty($name,$value); + } + } + } + + /** + * The module ID of another TDataSourceConfig. The {@link getDbConnection DbConnection} + * property of this configuration will equal to {@link getDbConnection DbConnection} + * of the given TDataSourceConfig module. + * @param string module ID. + */ + public function setConnectionID($value) + { + $this->_connID=$value; + } + + /** + * @return string connection module ID. + */ + public function getConnectionID() + { + return $this->_connID; + } + + /** + * Gets the TDbConnection from another module if {@link setConnectionID ConnectionID} + * is supplied and valid. Otherwise, a connection of type given by + * {@link setConnectionClass ConnectionClass} is created. + * @return TDbConnection database connection. + */ + public function getDbConnection() + { + if($this->_conn===null) + { + if($this->_connID!=='') + $this->_conn = $this->findConnectionByID($this->getConnectionID()); + else + $this->_conn = Prado::createComponent($this->getConnectionClass()); + } + return $this->_conn; + } + + /** + * Alias for getDbConnection(). + * @return TDbConnection database connection. + */ + public function getDatabase() + { + return $this->getDbConnection(); + } + + /** + * @param string Database connection class name to be created. + */ + public function getConnectionClass() + { + return $this->_connClass; + } + + /** + * The database connection class name to be created when {@link getDbConnection} + * method is called and {@link setConnectionID ConnectionID} is null. The + * {@link setConnectionClass ConnectionClass} property must be set before + * calling {@link getDbConnection} if you wish to create the connection using the + * given class name. + * @param string Database connection class name. + * @throws TConfigurationException when database connection is already established. + */ + public function setConnectionClass($value) + { + if($this->_conn!==null) + throw new TConfigurationException('datasource_dbconnection_exists', $value); + $this->_connClass=$value; + } + + /** + * Finds the database connection instance from the Application modules. + * @param string Database connection module ID. + * @return TDbConnection database connection. + * @throws TConfigurationException when module is not of TDbConnection or TDataSourceConfig. + */ + protected function findConnectionByID($id) + { + $conn = $this->getApplication()->getModule($id); + if($conn instanceof TDbConnection) + return $conn; + else if($conn instanceof TDataSourceConfig) + return $conn->getDbConnection(); + else + throw new TConfigurationException('datasource_dbconnection_invalid',$id); + } +} diff --git a/framework/Testing/Data/TDbCommand.php b/framework/Testing/Data/TDbCommand.php new file mode 100755 index 00000000..dd3a9fe6 --- /dev/null +++ b/framework/Testing/Data/TDbCommand.php @@ -0,0 +1,344 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * TDbCommand represents an SQL statement to execute against a database. + * + * It is usually created by calling {@link TDbConnection::createCommand}. + * The SQL statement to be executed may be set via {@link setText Text}. + * + * To execute a non-query SQL (such as insert, delete, update), call + * {@link execute}. To execute an SQL statement that returns result data set + * (such as SELECT), use {@link query} or its convenient versions {@link queryRow}, + * {@link queryColumn}, or {@link queryScalar}. + * + * If an SQL statement returns results (such as a SELECT SQL), the results + * can be accessed via the returned {@link TDbDataReader}. + * + * TDbCommand supports SQL statment preparation and parameter binding. + * Call {@link bindParam} to bind a PHP variable to a parameter in SQL. + * Call {@link bindValue} to bind a value to an SQL parameter. + * When binding a parameter, the SQL statement is automatically prepared. + * You may also call {@link prepare} to explicitly prepare an SQL statement. + * + * @author Qiang Xue + * @version $Id$ + * @package System.Testing.Data + * @since 1.0 + */ +class TDbCommand extends TComponent +{ + private $_connection; + private $_text=''; + private $_statement=null; + private $_params; + + /** + * Constructor. + * @param TDbConnection the database connection + * @param string the SQL statement to be executed + */ + public function __construct(TDbConnection $connection,$text) + { + $this->_connection=$connection; + $this->setText($text); + } + + /** + * Set the statement to null when serializing. + */ + public function __sleep() + { + $this->_statement=null; + return array_keys(get_object_vars($this)); + } + + /** + * @return string the SQL statement to be executed + */ + public function getText() + { + return $this->_text; + } + + /** + * Specifies the SQL statement to be executed. + * Any previous execution will be terminated or cancel. + * @param string the SQL statement to be executed + */ + public function setText($value) + { + $this->_text=$value; + $this->cancel(); + } + + /** + * @return TDbConnection the connection associated with this command + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @return PDOStatement the underlying PDOStatement for this command + * It could be null if the statement is not prepared yet. + */ + public function getPdoStatement() + { + return $this->_statement; + } + + /** + * Prepares the SQL statement to be executed. + * For complex SQL statement that is to be executed multiple times, + * this may improve performance. + * For SQL statement with binding parameters, this method is invoked + * automatically. + */ + public function prepare() + { + if($this->_statement==null) + { + try + { + $this->_statement=$this->getConnection()->getPdoInstance()->prepare($this->getText()); + $this->_params=array(); + } + catch(Exception $e) + { + Prado::log('Error in preparing SQL: '.$this->getText(),TLogger::ERROR,'System.Testing.Data.TDbCommand'); + throw new TDbException('TDbCommand failed to prepare the SQL statement: {0}', + $e->getMessage()); + } + } + } + + /** + * Cancels the execution of the SQL statement. + */ + public function cancel() + { + $this->_statement=null; + } + + /** + * Binds a parameter to the SQL statement to be executed. + * @param mixed Parameter identifier. For a prepared statement + * using named placeholders, this will be a parameter name of + * the form :name. For a prepared statement using question mark + * placeholders, this will be the 1-indexed position of the parameter. + * @param mixed Name of the PHP variable to bind to the SQL statement parameter + * @param int SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @param int length of the data type + * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php + */ + public function bindParameter($name, &$value, $dataType=null, $length=null) + { + $this->prepare(); + if($dataType===null) + $this->_statement->bindParam($name,$value,$this->_connection->getPdoType(gettype($value))); + else if($length===null) + $this->_statement->bindParam($name,$value,$dataType); + else + $this->_statement->bindParam($name,$value,$dataType,$length); + if($this->_connection->enableParamLogging) + $this->_params[]=$name.'=['.gettype($value).']'; + } + + + /** + * Binds a value to a parameter. + * @param mixed Parameter identifier. For a prepared statement + * using named placeholders, this will be a parameter name of + * the form :name. For a prepared statement using question mark + * placeholders, this will be the 1-indexed position of the parameter. + * @param mixed The value to bind to the parameter + * @param int SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @see http://www.php.net/manual/en/function.PDOStatement-bindValue.php + */ + public function bindValue($name, $value, $dataType=null) + { + $this->prepare(); + if($dataType===null) + $this->_statement->bindValue($name,$value,$this->_connection->getPdoType(gettype($value))); + else + $this->_statement->bindValue($name,$value,$dataType); + if($this->_connection->enableParamLogging) + $this->_params[]=$name.'='.var_export($value,true); + } + + /** + * Executes the SQL statement. + * This method is meant only for executing non-query SQL statement. + * No result set will be returned. + * @return integer number of rows affected by the execution. + * @throws CException execution failed + */ + public function execute() + { + $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; + Prado::trace('Executing SQL: '.$this->getText().$params,'System.Testing.Data.TDbCommand'); + try + { + /* + if($this->_connection->enableProfiling) + Prado::beginProfile('System.Testing.Data.TDbCommand.execute('.$this->getText().')','System.Testing.Data.TDbCommand.execute'); + */ + if($this->_statement instanceof PDOStatement) + { + $this->_statement->execute(); + $n=$this->_statement->rowCount(); + } + else + $n=$this->getConnection()->getPdoInstance()->exec($this->getText()); + /* + if($this->_connection->enableProfiling) + Prado::endProfile('System.Testing.Data.TDbCommand.execute('.$this->getText().')','System.Testing.Data.TDbCommand.execute'); + */ + return $n; + } + catch(Exception $e) + { + /* + if($this->_connection->enableProfiling) + Prado::endProfile('System.Testing.Data.TDbCommand.execute('.$this->getText().')','System.Testing.Data.TDbCommand.execute'); + */ + Prado::log('Error in executing SQL: '.$this->getText().$params,TLogger::ERROR,'System.Testing.Data.TDbCommand'); + throw new TDbException('TDbCommand failed to execute the SQL statement: {0}', + $e->getMessage()); + } + } + + /** + * @return String prepared SQL text for debugging purposes. + */ + public function getDebugStatementText() + { + if(Prado::getApplication()->getMode() === TApplicationMode::Debug) + { + $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; + return $this->_statement instanceof PDOStatement ? + $this->_statement->queryString.$params + : $this->getText().$params; + + } + } + + /** + * Executes the SQL statement and returns query result. + * This method is for executing an SQL query that returns result set. + * @return TDbDataReader the reader object for fetching the query result + * @throws CException execution failed + */ + public function query() + { + return $this->queryInternal('',0); + } + + /** + * Executes the SQL statement and returns all rows. + * @param boolean whether each row should be returned as an associated array with + * column names as the keys or the array keys are column indexes (0-based). + * @return array all rows of the query result. Each array element is an array representing a row. + * An empty array is returned if the query results in nothing. + * @throws CException execution failed + */ + public function queryAll($fetchAssociative=true) + { + return $this->queryInternal('fetchAll',$fetchAssociative ? PDO::FETCH_ASSOC : PDO::FETCH_NUM); + } + + /** + * Executes the SQL statement and returns the first row of the result. + * This is a convenient method of {@link query} when only the first row of data is needed. + * @param boolean whether the row should be returned as an associated array with + * column names as the keys or the array keys are column indexes (0-based). + * @return array the first row of the query result, false if no result. + * @throws CException execution failed + */ + public function queryRow($fetchAssociative=true) + { + return $this->queryInternal('fetch',$fetchAssociative ? PDO::FETCH_ASSOC : PDO::FETCH_NUM); + } + + /** + * Executes the SQL statement and returns the value of the first column in the first row of data. + * This is a convenient method of {@link query} when only a single scalar + * value is needed (e.g. obtaining the count of the records). + * @return mixed the value of the first column in the first row of the query result. False is returned if there is no value. + * @throws CException execution failed + */ + public function queryScalar() + { + $result=$this->queryInternal('fetchColumn',0); + if(is_resource($result) && get_resource_type($result)==='stream') + return stream_get_contents($result); + else + return $result; + } + + /** + * Executes the SQL statement and returns the first column of the result. + * This is a convenient method of {@link query} when only the first column of data is needed. + * Note, the column returned will contain the first element in each row of result. + * @return array the first column of the query result. Empty array if no result. + * @throws CException execution failed + */ + public function queryColumn() + { + return $this->queryInternal('fetchAll',PDO::FETCH_COLUMN); + } + + /** + * @param string method of PDOStatement to be called + * @param mixed the first parameter to be passed to the method + * @return mixed the method execution result + */ + private function queryInternal($method,$mode) + { + $params=$this->_connection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : ''; + Prado::trace('Querying SQL: '.$this->getText().$params,'System.Testing.Data.TDbCommand'); + try + { + /* + if($this->_connection->enableProfiling) + Prado::beginProfile('System.Testing.Data.TDbCommand.query('.$this->getText().')','System.Testing.Data.TDbCommand.query'); + */ + if($this->_statement instanceof PDOStatement) + $this->_statement->execute(); + else + $this->_statement=$this->getConnection()->getPdoInstance()->query($this->getText()); + + if($method==='') + $result=new TDbDataReader($this); + else + { + $result=$this->_statement->{$method}($mode); + $this->_statement->closeCursor(); + } + /* + if($this->_connection->enableProfiling) + Prado::endProfile('System.Testing.Data.TDbCommand.query('.$this->getText().')','System.Testing.Data.TDbCommand.query'); + */ + return $result; + } + catch(Exception $e) + { + /* + if($this->_connection->enableProfiling) + prado::endProfile('System.Testing.Data.TDbCommand.query('.$this->getText().')','System.Testing.Data.TDbCommand.query'); + */ + Prado::log('Error in querying SQL: '.$this->getText().$params,TLogger::ERROR,'System.Testing.Data..TDbCommand'); + throw new TDbException('TDbCommand failed to execute the SQL statement: {0}', + $e->getMessage()); + } + } +} diff --git a/framework/Testing/Data/TDbConnection.php b/framework/Testing/Data/TDbConnection.php new file mode 100755 index 00000000..92708907 --- /dev/null +++ b/framework/Testing/Data/TDbConnection.php @@ -0,0 +1,759 @@ + + * @author Christophe Boulain + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +prado::using ('System.Testing.Data.TDbCommand'); +prado::using ('System.Testing.Data.TDbTransaction'); + +/** + * TDbConnection represents a connection to a database. + * + * This is a port of {@link http://www.yiiframework.com Yii} {@link http://www.yiiframework.com/ CDbConnection} + * + * TDbConnection works together with {@link TDbCommand}, {@link TDbDataReader} + * and {@link TDbTransaction} 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 TDbConnection instance and establish + * the actual connection: + *
      + * $connection=new TDbConnection($dsn,$username,$password);
      + * $connection->active=true;
      + * 
      + * + * After the DB connection is established, one can execute an SQL statement like the following: + *
      + * $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) ...
      + * 
      + * + * One can do prepared SQL execution and bind parameters to the prepared SQL: + *
      + * $command=$connection->createCommand($sqlStatement);
      + * $command->bindParam($name1,$value1);
      + * $command->bindParam($name2,$value2);
      + * $command->execute();
      + * 
      + * + * To use transaction, do like the following: + *
      + * $transaction=$connection->beginTransaction();
      + * try
      + * {
      + *    $connection->createCommand($sql1)->execute();
      + *    $connection->createCommand($sql2)->execute();
      + *    //.... other SQL executions
      + *    $transaction->commit();
      + * }
      + * catch(Exception $e)
      + * {
      + *    $transaction->rollBack();
      + * }
      + * 
      + * + * TDbConnection also provides a set of methods to support setting and querying + * of certain DBMS attributes, such as {@link getNullConversion nullConversion}. + * + * @author Qiang Xue + * @author Christophe.Boulain + * @version $Id$ + * @package System.Testing.Data + * @since 3.2 + */ +class TDbConnection extends TComponent +{ + /** + * @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 + */ + 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 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 TDbConnection 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. + */ + 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. + */ + public $emulatePrepare=false; + /** + * @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; + + 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 The Data Source Name, or DSN, contains the information required to connect to the database. + * @param string The user name for the DSN string. + * @param string 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. + */ + public function __sleep() + { + $this->close(); + return array_keys(get_object_vars($this)); + } + + /** + * @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 TDbConnection 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() + { + if($this->autoConnect) + $this->setActive(true); + } + + /** + * @return boolean whether the DB connection is established + */ + public function getActive() + { + return $this->_active; + } + + /** + * Open or close the DB connection. + * @param boolean 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(); + } + } + + /** + * 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 TDbException('TDbConnection.connectionString cannot be empty.'); + try + { + Prado::trace('Opening DB connection','System.Testing.Data.TDbConnection'); + $this->_pdo=$this->createPdoInstance(); + $this->initConnection($this->_pdo); + $this->_active=true; + } + catch(PDOException $e) + { + throw new TDbException('TDbConnection failed to open the DB connection: {0}',$e->getMessage()); + } + } + } + + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + protected function close() + { + Prado::trace('Closing DB connection','System.Testing.Data.TDbConnection'); + $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 + * @since 1.0.4 + */ + protected function createPdoInstance() + { + $pdoClass='PDO'; + if(($pos=strpos($this->connectionString,':'))!==false) + { + $driver=strtolower(substr($this->connectionString,0,$pos)); + if($driver==='mssql' || $driver==='dblib') + { + prado::using('System.Testing.Data.Schema.mssql.TMssqlPdoAdapter'); + $pdoClass='TMssqlPdoAdapter'; + } + } + 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 the PDO instance + */ + protected function initConnection($pdo) + { + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + if($this->emulatePrepare && constant('PDO::ATTR_EMULATE_PREPARES')) + $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES,true); + if($this->charset!==null) + { + switch ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)) + { + case 'mysql': + $stmt = $pdo->prepare('SET CHARACTER SET ?'); + break; + case 'pgsql': + $stmt = $pdo->prepare('SET client_encoding TO ?'); + break; + case 'sqlite': + $stmt = $pdo->prepare ('SET NAMES ?'); + break; + } + $stmt->execute(array($this->charset)); + } + } + + /** + * @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 string SQL statement associated with the new command. + * @return TDbCommand the DB command + * @throws CException if the connection is not active + */ + public function createCommand($sql) + { + if($this->getActive()) + return new TDbCommand($this,$sql); + else + throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); + } + + /** + * @return TDbTransaction 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 TDbTransaction the transaction initiated + * @throws CException if the connection is not active + */ + public function beginTransaction() + { + if($this->getActive()) + { + $this->_pdo->beginTransaction(); + return $this->_transaction=new TDbTransaction($this); + } + else + throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); + } + + /** + * @return TDbSchema the database schema for the current connection + * @throws CException if the connection is not active yet + */ + public function getSchema() + { + if($this->_schema!==null) + return $this->_schema; + else + { + if(!$this->getActive()) + throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); + $driver=$this->getDriverName(); + switch(strtolower($driver)) + { + case 'pgsql': // PostgreSQL + prado::using('System.Testing.Data.Schema.pgsql.TPgsqlSchema'); + return $this->_schema=new TPgsqlSchema($this); + case 'mysqli': // MySQL + case 'mysql': + prado::using('System.Testing.Data.Schema.mysql.TMysqlSchema'); + return $this->_schema=new TMysqlSchema($this); + case 'sqlite': // sqlite 3 + case 'sqlite2': // sqlite 2 + prado::using('System.Testing.Data.Schema.sqlite.TSqliteSchema'); + return $this->_schema=new TSqliteSchema($this); + case 'mssql': // Mssql driver on windows hosts + case 'dblib': // dblib drivers on linux (and maybe others os) hosts + prado::using('System.Testing.Data.Schema.mssql.TMssqlSchema'); + return $this->_schema=new TMssqlSchema($this); + case 'oci': // Oracle driver + prado::using('System.Testing.Data.Schema.oci.TOciSchema'); + return $this->_schema=new TOciSchema($this); + case 'ibm': + default: + throw new TDbException('TDbConnection does not support reading schema for {0} database.', + $driver); + } + } + } + + /** + * Returns the SQL command builder for the current DB connection. + * @return TDbCommandBuilder the command builder + */ + public function getCommandBuilder() + { + return $this->getSchema()->getCommandBuilder(); + } + + /** + * Returns the ID of the last inserted row or sequence value. + * @param string 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='') + { + if($this->getActive()) + return $this->_pdo->lastInsertId($sequenceName); + else + throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); + } + + /** + * Quotes a string value for use in a query. + * @param string string to be quoted + * @return string the properly quoted string + * @see http://www.php.net/manual/en/function.PDO-quote.php + */ + public function quoteString($str) + { + if($this->getActive()) + return $this->_pdo->quote($str); + else + throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); + } + + /** + * Quotes a table name for use in a query. + * @param string 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. + * @param string 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 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; + } + + /** + * @return TDbColumnCaseMode the case of the column names + */ + public function getColumnCase() + { + switch($this->getAttribute(PDO::ATTR_CASE)) + { + case PDO::CASE_NATURAL: + return TDbColumnCaseMode::Preserved; + case PDO::CASE_LOWER: + return TDbColumnCaseMode::LowerCase; + case PDO::CASE_UPPER: + return TDbColumnCaseMode::UpperCase; + } + } + + /** + * @param TDbColumnCaseMode the case of the column names + */ + public function setColumnCase($value) + { + switch(TPropertyValue::ensureEnum($value,'TDbColumnCaseMode')) + { + case TDbColumnCaseMode::Preserved: + $value=PDO::CASE_NATURAL; + break; + case TDbColumnCaseMode::LowerCase: + $value=PDO::CASE_LOWER; + break; + case TDbColumnCaseMode::UpperCase: + $value=PDO::CASE_UPPER; + break; + } + $this->setAttribute(PDO::ATTR_CASE,$value); + } + /** + * @return TDbNullConversionMode how the null and empty strings are converted + */ + public function getNullConversion() + { + switch($this->getAttribute(PDO::ATTR_ORACLE_NULLS)) + { + case PDO::NULL_NATURAL: + return TDbNullConversionMode::Preserved; + case PDO::NULL_EMPTY_STRING: + return TDbNullConversionMode::EmptyStringToNull; + case PDO::NULL_TO_STRING: + return TDbNullConversionMode::NullToEmptyString; + } + } + + /** + * @param TDbNullConversionMode how the null and empty strings are converted + */ + public function setNullConversion($value) + { + switch(TPropertyValue::ensureEnum($value,'TDbNullConversionMode')) + { + case TDbNullConversionMode::Preserved: + $value=PDO::NULL_NATURAL; + break; + case TDbNullConversionMode::EmptyStringToNull: + $value=PDO::NULL_EMPTY_STRING; + break; + case TDbNullConversionMode::NullToEmptyString: + $value=PDO::NULL_TO_STRING; + break; + } + $this->setAttribute(PDO::ATTR_ORACLE_NULLS,$value); + } + + /** + * @return boolean whether creating or updating a DB record will be automatically committed. + * Some DBMS (such as sqlite) may not support this feature. + */ + public function getAutoCommit() + { + return $this->getAttribute(PDO::ATTR_AUTOCOMMIT); + } + + /** + * @param boolean whether creating or updating a DB record will be automatically committed. + * Some DBMS (such as sqlite) may not support this feature. + */ + public function setAutoCommit($value) + { + $this->setAttribute(PDO::ATTR_AUTOCOMMIT,$value); + } + + /** + * @return boolean whether the connection is persistent or not + * Some DBMS (such as sqlite) may not support this feature. + */ + public function getPersistent() + { + return $this->getAttribute(PDO::ATTR_PERSISTENT); + } + + /** + * @param boolean whether the connection is persistent or not + * Some DBMS (such as sqlite) may not support this feature. + */ + public function setPersistent($value) + { + return $this->setAttribute(PDO::ATTR_PERSISTENT,$value); + } + + /** + * @return string name of the DB driver + */ + public function getDriverName() + { + return $this->getAttribute(PDO::ATTR_DRIVER_NAME); + } + + /** + * @return string the version information of the DB driver + */ + public function getClientVersion() + { + return $this->getAttribute(PDO::ATTR_CLIENT_VERSION); + } + + /** + * @return string the status of the connection + * Some DBMS (such as sqlite) may not support this feature. + */ + public function getConnectionStatus() + { + return $this->getAttribute(PDO::ATTR_CONNECTION_STATUS); + } + + /** + * @return boolean whether the connection performs data prefetching + */ + public function getPrefetch() + { + return $this->getAttribute(PDO::ATTR_PREFETCH); + } + + /** + * @return string the information of DBMS server + */ + public function getServerInfo() + { + return $this->getAttribute(PDO::ATTR_SERVER_INFO); + } + + /** + * @return string the version information of DBMS server + */ + public function getServerVersion() + { + return $this->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * @return int timeout settings for the connection + */ + public function getTimeout() + { + return $this->getAttribute(PDO::ATTR_TIMEOUT); + } + + /** + * Obtains a specific DB connection attribute information. + * @param int 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) + { + if($this->getActive()) + return $this->_pdo->getAttribute($name); + else + throw new TDbException('TDbConnection is inactive and cannot perform any DB operations.'); + } + + /** + * Sets an attribute on the database connection. + * @param int the attribute to be set + * @param mixed 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 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. + * @since 1.0.6 + */ + public function getStats() + { + /*$logger=Yii::getLogger(); + $timings=$logger->getProfilingResults(null,'System.Testing.Data.TDbCommand.query'); + $count=count($timings); + $time=array_sum($timings); + $timings=$logger->getProfilingResults(null,'System.Testing.Data.TDbCommand.execute'); + $count+=count($timings); + $time+=array_sum($timings); + return array($count,$time);*/ + } + + /** + * Getters & Setters to provide BC with prado-3.1 + */ + public function getConnectionString() { return $this->connectionString; } + public function getUsername () { return $this->username; } + public function getPassword () { return $this->password; } + public function getCharset () { return $this->charset; } + public function getSchemaCachingDuration() { return $this->schemaCachingDuration; } + public function getSchemaCachingExclude () { return $this->schemaCachingExclude; } + public function getAutoConnect () { return $this->autoConnect; } + public function getEmulatePrepare () { return $this->emulatePrepare; } + public function getEnableParamLogging () { return $this->enableParamLogging; } + public function getEnableProfiling () { return $this->enableProfiling; } + + public function setConnectionString($value) { $this->connectionString=TPropertyValue::ensureString($value); } + public function setUsername ($value) { $this->username=TPropertyValue::ensureString($value); } + public function setPassword ($value) { $this->password=TPropertyValue::ensureString($value); } + public function setCharset ($value) { $this->charset=TPropertyValue::ensureString($value); } + public function setSchemaCachingDuration ($value) { $this->schemaCachingDuration=TPropertyValue::ensureInteger($value); } + public function setSchemaCachingExclude ($value) { $this->username=TPropertyValue::ensureArray($value); } + public function setAutoConnect ($value) { $this->autoConnect = TPropertyValue::ensureBoolean($value); } + public function setEnablePrepare ($value) { $this->emulatePrepare = TPropertyValue::ensureBoolean($value); } + public function setEnableParamLogging ($value) { $this->enableParamLogging = TPropertyValue::ensureBoolean($value); } + public function setEnableProfiling ($value) { $this->enableProfiling = TPropertyValue::ensureBoolean ($value) ; } +} + + +/** + * TDbColumnCaseMode + * + * @author Qiang Xue + * @version $Id$ + * @package System.Testing.Data + * @since 3.0 + */ +class TDbColumnCaseMode extends TEnumerable +{ + /** + * Column name cases are kept as is from the database + */ + const Preserved='Preserved'; + /** + * Column names are converted to lower case + */ + const LowerCase='LowerCase'; + /** + * Column names are converted to upper case + */ + const UpperCase='UpperCase'; +} + +/** + * TDbNullConversionMode + * + * @author Qiang Xue + * @version $Id$ + * @package System.Testing.Data + * @since 3.0 + */ +class TDbNullConversionMode extends TEnumerable +{ + /** + * No conversion is performed for null and empty values. + */ + const Preserved='Preserved'; + /** + * NULL is converted to empty string + */ + const NullToEmptyString='NullToEmptyString'; + /** + * Empty string is converted to NULL + */ + const EmptyStringToNull='EmptyStringToNull'; +} diff --git a/framework/Testing/Data/TDbDataReader.php b/framework/Testing/Data/TDbDataReader.php new file mode 100755 index 00000000..6014ff04 --- /dev/null +++ b/framework/Testing/Data/TDbDataReader.php @@ -0,0 +1,221 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * TDbDataReader 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 TDbDataReader by using foreach: + *
      + * foreach($reader as $row)
      + *     // $row represents a row of data
      + * 
      + * Since TDbDataReader 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. + * + * @author Qiang Xue + * @version $Id$ + * @package System.Testing.Data + * @since 1.0 + */ +class TDbDataReader extends TComponent implements Iterator +{ + private $_statement; + private $_closed=false; + private $_row; + private $_index=-1; + + /** + * Constructor. + * @param TDbCommand the command generating the query result + */ + public function __construct(TDbCommand $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 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 Name of the PHP variable to which the column will be bound. + * @param int 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); + } + + /** + * @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 int 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 class name of the object to be created and populated + * @param array 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. + */ + public function nextResult() + { + return $this->_statement->nextRowset(); + } + + /** + * 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; + } + + /** + * @return boolean whether the reader is closed or not. + */ + public function getIsClosed() + { + return $this->_closed; + } + + /** + * @return int number of rows contained in the result. + * Note, most DBMS may not give a meaningful count. + * In this case, use "SELECT COUNT(*) FROM tableName" to obtain the number of rows. + */ + public function getRowCount() + { + return $this->_statement->rowCount(); + } + + /** + * @return int the number of columns in the result set. + * Note, even there's no row in the reader, this still gives correct column number. + */ + 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 TDbException('dbdatareader_rewind_invalid'); + } + + /** + * 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/Testing/Data/TDbTransaction.php b/framework/Testing/Data/TDbTransaction.php new file mode 100755 index 00000000..41a2c1f9 --- /dev/null +++ b/framework/Testing/Data/TDbTransaction.php @@ -0,0 +1,111 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +Prado::using('System.Testing.Data.TDbDataReader'); + + +/** + * TDbTransaction represents a DB transaction. + * + * It is usually created by calling {@link TDbConnection::beginTransaction}. + * + * The following code is a common scenario of using transactions: + *
      + * $transaction=$connection->beginTransaction();
      + * try
      + * {
      + *    $connection->createCommand($sql1)->execute();
      + *    $connection->createCommand($sql2)->execute();
      + *    //.... other SQL executions
      + *    $transaction->commit();
      + * }
      + * catch(Exception $e)
      + * {
      + *    $transaction->rollBack();
      + * }
      + * 
      + * + * @author Qiang Xue + * @version $Id$ + * @package System.Testing.Data + * @since 1.0 + */ +class TDbTransaction extends TComponent +{ + private $_connection=null; + private $_active; + + /** + * Constructor. + * @param TDbConnection the connection associated with this transaction + * @see TDbConnection::beginTransaction + */ + public function __construct(TDbConnection $connection) + { + $this->_connection=$connection; + $this->setActive(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()) + { + Prado::trace('Committing transaction','System.Testing.Data.TDbTransaction'); + $this->_connection->getPdoInstance()->commit(); + $this->_active=false; + } + else + throw new TDbException('dbtransaction_transaction_inactive'); + } + + /** + * 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()) + { + Prado::trace('Rolling back transaction','System.Testing.Data.TDbTransaction'); + $this->_connection->getPdoInstance()->rollBack(); + $this->_active=false; + } + else + throw new TDbException('dbtransaction_transaction_inactive'); + } + + /** + * @return TDbConnection 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 whether this transaction is active + */ + protected function setActive($value) + { + $this->_active=TPropertyValue::ensureBoolean($value); + } +} -- cgit v1.2.3