diff options
31 files changed, 7141 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes index 1ba435cd..5fdca142 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2202,6 +2202,36 @@ framework/Data/TDbCommand.php -text  framework/Data/TDbConnection.php -text  framework/Data/TDbDataReader.php -text  framework/Data/TDbTransaction.php -text +framework/Db/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY -text +framework/Db/ActiveRecord/Relations/TActiveRecordRelationContext.php -text +framework/Db/ActiveRecord/TActiveFinder.php -text +framework/Db/ActiveRecord/TActiveRecord.php -text +framework/Db/ActiveRecord/TActiveRecordBehavior.php -text +framework/Db/ActiveRecord/TActiveRecordCriteria.php -text +framework/Db/Schema/TDbColumnSchema.php -text +framework/Db/Schema/TDbCommandBuilder.php -text +framework/Db/Schema/TDbCriteria.php -text +framework/Db/Schema/TDbExpression.php -text +framework/Db/Schema/TDbSchema.php -text +framework/Db/Schema/TDbTableSchema.php -text +framework/Db/Schema/mssql/TMssqlColumnSchema.php -text +framework/Db/Schema/mssql/TMssqlCommandBuilder.php -text +framework/Db/Schema/mssql/TMssqlPdoAdapter.php -text +framework/Db/Schema/mssql/TMssqlSchema.php -text +framework/Db/Schema/mssql/TMssqlTableSchema.php -text +framework/Db/Schema/mysql/TMysqlColumnSchema.php -text +framework/Db/Schema/mysql/TMysqlSchema.php -text +framework/Db/Schema/mysql/TMysqlTableSchema.php -text +framework/Db/Schema/oci/TOciColumnSchema.php -text +framework/Db/Schema/oci/TOciCommandBuilder.php -text +framework/Db/Schema/oci/TOciSchema.php -text +framework/Db/Schema/oci/TOciTableSchema.php -text +framework/Db/Schema/pgsql/TPgsqlColumnSchema.php -text +framework/Db/Schema/pgsql/TPgsqlSchema.php -text +framework/Db/Schema/pgsql/TPgsqlTableSchema.php -text +framework/Db/Schema/sqlite/TSqliteColumnSchema.php -text +framework/Db/Schema/sqlite/TSqliteCommandBuilder.php -text +framework/Db/Schema/sqlite/TSqliteSchema.php -text  framework/Exceptions/TErrorHandler.php -text  framework/Exceptions/TException.php -text  framework/Exceptions/messages/messages-id.txt -text diff --git a/framework/Db/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY b/framework/Db/ActiveRecord/Relations/DEPRECATED_COMPATIBILITY_REASONS_ONLY new file mode 100644 index 00000000..8c7a4073 --- /dev/null +++ b/framework/Db/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/Db/ActiveRecord/Relations/TActiveRecordRelationContext.php b/framework/Db/ActiveRecord/Relations/TActiveRecordRelationContext.php new file mode 100644 index 00000000..696bb5b1 --- /dev/null +++ b/framework/Db/ActiveRecord/Relations/TActiveRecordRelationContext.php @@ -0,0 +1,230 @@ +<?php
 +/**
 + * TActiveRecordRelationContext class.
 + *
 + * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
 + * @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 <weizho[at]gmail[dot]com>
 + * @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 new file mode 100644 index 00000000..d55074c0 --- /dev/null +++ b/framework/Db/ActiveRecord/TActiveFinder.php @@ -0,0 +1,1279 @@ +<?php +/** + * CActiveRecord class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2009 Yii Software LLC + * @license http://www.yiiframework.com/license/CBaseActiveRelation*/ + +/** + * CActiveFinder implements eager loading and lazy loading of related active records. + * + * When used in eager loading, this class provides the same set of find methods as + * {@link CActiveRecord}. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveFinder.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.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 <qiang.xue@gmail.com> + * @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<count($parent->_table->primaryKey)) +				{ +					$pk=is_array($parent->_table->primaryKey) ? $parent->_table->primaryKey[$i] : $parent->_table->primaryKey; +					$parentCondition[$pk]=$parent->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk); +				} +				else +				{ +					$j=$i-count($parent->_table->primaryKey); +					$pk=is_array($this->_table->primaryKey) ? $this->_table->primaryKey[$j] : $this->_table->primaryKey; +					$childCondition[$pk]=$this->getColumnPrefix().$schema->quoteColumnName($pk).'='.$joinAlias.'.'.$schema->quoteColumnName($fk); +				} +			} +		} +		if($parentCondition!==array() && $childCondition!==array()) +		{ +			$join=$this->relation->joinType.' '.$joinTable->rawName.' '.$joinAlias; +			$join.=' ON ('.implode(') AND (',$parentCondition).')'; +			$join.=' '.$this->relation->joinType.' '.$this->getTableNameWithAlias(); +			$join.=' ON ('.implode(') AND (',$childCondition).')'; +			if(!empty($this->relation->on)) +				$join.=' AND ('.$this->relation->on.')'; +			return $join; +		} +		else +			throw new TDbException('The relation "'.($this->relation->name).'" in active record class "'.get_class($parent->model).'" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.'); +	} +} + + +/** + * CJoinQuery represents a JOIN SQL statement. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @version $Id: CActiveFinder.php 1127 2009-06-13 20:26:35Z qiang.xue $ + * @package System.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 <qiang.xue@gmail.com> + * @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($i<count($pkTable->primaryKey)) +				{ +					$pk=is_array($pkTable->primaryKey) ? $pkTable->primaryKey[$i] : $pkTable->primaryKey; +					$map[$pk]=$fk; +				} +				else +				{ +					$j=$i-count($pkTable->primaryKey); +					$pk=is_array($table->primaryKey) ? $table->primaryKey[$j] : $table->primaryKey; +					$joinCondition[$pk]=$table->rawName.'.'.$schema->quoteColumnName($pk).'='.$joinTable->rawName.'.'.$schema->quoteColumnName($fk); +				} +			} +		} + +		if($joinCondition===array() || $map===array()) +			throw new TDbException('The relation "'.($relation->name).'" in active record class "'.get_class($this->_parent->model).'" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.'); + +		$records=$this->_parent->records; + +		$cols=array(); +		foreach(is_string($pkTable->primaryKey)?array($pkTable->primaryKey):$pkTable->primaryKey as $n=>$pk) +		{ +			$name=$joinTable->rawName.'.'.$schema->quoteColumnName($map[$pk]); +			$cols[$name]=$name.' AS '.$schema->quoteColumnName('c'.$n); +		} + +		$keys=array_keys($records); +		if(is_array($pkTable->primaryKey)) +		{ +			foreach($keys as &$key) +			{ +				$key2=unserialize($key); +				$key=array(); +				foreach($pkTable->primaryKey as $pk) +					$key[$map[$pk]]=$key2[$pk]; +			} +		} + +		$where=empty($relation->condition)?'' : ' WHERE ('.$relation->condition.')'; +		$group=empty($relation->group)?'' : ', '.$relation->group; +		$having=empty($relation->having)?'' : ' AND ('.$relation->having.')'; +		$order=empty($relation->order)?'' : ' ORDER BY '.$relation->order; + +		$sql='SELECT '.$this->relation->select.' AS '.$schema->quoteColumnName('s').', '.implode(', ',$cols) +			.' FROM '.$table->rawName.' INNER JOIN '.$joinTable->rawName +			.' ON ('.implode(') AND (',$joinCondition).')' +			.$where +			.' GROUP BY '.implode(', ',array_keys($cols)).$group +			.' HAVING ('.$builder->createInCondition($joinTable,$map,$keys).')' +			.$having.$order; + +		$command=$builder->getDbConnection()->createCommand($sql); +		if(is_array($relation->params)) +			$builder->bindValues($command,$relation->params); + +		$stats=array(); +		foreach($command->queryAll() as $row) +		{ +			if(is_array($pkTable->primaryKey)) +			{ +				$key=array(); +				foreach($pkTable->primaryKey as $n=>$k) +					$key[$k]=$row['c'.$n]; +				$stats[serialize($key)]=$row['s']; +			} +			else +				$stats[$row['c0']]=$row['s']; +		} + +		foreach($records as $pk=>$record) +			$record->addRelatedRecord($relation->name,isset($stats[$pk])?$stats[$pk]:$this->relation->defaultValue,false); +	} +} diff --git a/framework/Db/ActiveRecord/TActiveRecord.php b/framework/Db/ActiveRecord/TActiveRecord.php new file mode 100644 index 00000000..c0ffc32e --- /dev/null +++ b/framework/Db/ActiveRecord/TActiveRecord.php @@ -0,0 +1,1843 @@ +<?php +/** + * CActiveRecord class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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, +	 * <pre> +	 * public static function model($className=__CLASS__) +	 * { +	 *     return parent::model($className); +	 * } +	 * </pre> +	 * +	 * @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: +	 * <ul> +	 * <li>BELONGS_TO: e.g. a member belongs to a team;</li> +	 * <li>HAS_ONE: e.g. a member has at most one profile;</li> +	 * <li>HAS_MANY: e.g. a team has many members;</li> +	 * <li>MANY_MANY: e.g. a member has many skills and a skill belongs to a member.</li> +	 * </ul> +	 * +	 * 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: +	 * <pre> +	 * 'varName'=>array('relationType', 'className', 'foreignKey', ...additional options) +	 * </pre> +	 * where 'varName' refers to the name of the variable/property that the related object(s) can +	 * be accessed through; 'relationType' refers to the type of the relation, which can be one of the +	 * following four constants: self::BELONGS_TO, self::HAS_ONE, self::HAS_MANY and self::MANY_MANY; +	 * 'className' refers to the name of the active record class that the related object(s) is of; +	 * and '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: +	 * <ul> +	 * <li>'select': string|array, a list of columns to be selected. Defaults to '*', meaning all columns. +	 *   Column names should be disambiguated if they appear in an expression (e.g. COUNT(??.name) AS name_count).</li> +	 * <li>'condition': string, the WHERE clause. Defaults to empty. Note, column references need to +	 *   be disambiguated with prefix '??.' (e.g. ??.age>20)</li> +	 * <li>'order': string, the ORDER BY clause. Defaults to empty. Note, column references need to +	 *   be disambiguated with prefix '??.' (e.g. ??.age DESC)</li> +	 * <li>'with': string|array, a list of child related objects that should be loaded together with this object. +	 *   Note, this is only honored by lazy loading, not eager loading.</li> +	 * <li>'joinType': type of join. Defaults to 'LEFT OUTER JOIN'.</li> +	 * <li>'alias': the alias for the table associated with this relationship. +	 *   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.</li> +     * <li>'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.</li> +	 * <li>'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.</li> +	 * </ul> +	 * +	 * The following options are available for certain relations when lazy loading: +	 * <ul> +	 * <li>'group': string, the GROUP BY clause. Defaults to empty. Note, column references need to +	 *   be disambiguated with prefix '??.' (e.g. ??.age). This option only applies to HAS_MANY and MANY_MANY relations.</li> +	 * <li>'having': string, the HAVING clause. Defaults to empty. Note, column references need to +	 *   be disambiguated with prefix '??.' (e.g. ??.age). This option only applies to HAS_MANY and MANY_MANY relations.</li> +	 * <li>'limit': limit of the rows to be selected. This option does not apply to BELONGS_TO relation.</li> +	 * <li>'offset': offset of the rows to be selected. This option does not apply to BELONGS_TO relation.</li> +	 * </ul> +	 * +	 * Below is an example declaring related objects for 'Post' active record class: +	 * <pre> +	 * 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'), +	 * ); +	 * </pre> +	 * +	 * @return array list of related object declarations. Defaults to empty array. +	 */ +	public function relations() +	{ +		return array(); +	} +	 +	/** +	 * Returns the declaration of named scopes. +	 * A named scope represents a query criteria that can be chained together with +	 * other named scopes and applied to a query. This method should be overridden +	 * by child classes to declare named scopes for the particular AR classes. +	 * For example, the following code declares two named scopes: 'recently' and +	 * 'published'. +	 * <pre> +	 * return array( +	 *     'published'=>array( +	 *           'condition'=>'status=1', +	 *     ), +	 *     'recently'=>array( +	 *           'order'=>'createTime DESC', +	 *           'limit'=>5, +	 *     ), +	 * ); +	 * </pre> +	 * If the above scopes are declared in a 'Post' model, we can perform the following +	 * queries: +	 * <pre> +	 * $posts=Post::model()->published()->findAll(); +	 * $posts=Post::model()->published()->recently()->findAll(); +	 * $posts=Post::model()->published()->with('comments')->findAll(); +	 * </pre> +	 * Note that the last query is a relational query. +	 * +	 * @return array the scope definition. The array keys are scope names; the array +	 * values are the corresponding scope definitions. Each scope definition is represented +	 * as an array whose keys must be properties of {@link CDbCriteria}. +	 * @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: +	 * <pre> +	 * $postRecord=Post::model()->findByPk($postID); +	 * $postRecord->attributes=$_POST['post']; +	 * $postRecord->save(); +	 * </pre> +	 * @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, +	 * <pre> +	 * // find all posts together with their author and comments +	 * Post::model()->with('author','comments')->findAll(); +	 * // find all posts together with their author and the author's profile +	 * Post::model()->with('author','author.profile')->findAll(); +	 * </pre> +	 * The relations should be declared in {@link relations()}. +	 * +	 * By default, the options specified in {@link relations()} will be used +	 * to do relational query. In order to customize the options on the fly, +	 * we should pass an array parameter to the with() method. The array keys +	 * are relation names, and the array values are the corresponding query options. +	 * For example, +	 * <pre> +	 * Post::model()->with(array( +	 *     'author'=>array('select'=>'id, name'), +	 *     'comments'=>array('condition'=>'approved=1', 'order'=>'createTime'), +	 * ))->findAll(); +	 * </pre> +	 * +	 * 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 <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 <godzilla80@gmx.net> + * @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 new file mode 100644 index 00000000..1e4ccbae --- /dev/null +++ b/framework/Db/ActiveRecord/TActiveRecordBehavior.php @@ -0,0 +1,94 @@ +<?php +/** + * CActiveRecordBehavior class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100644 index 00000000..9dded69a --- /dev/null +++ b/framework/Db/ActiveRecord/TActiveRecordCriteria.php @@ -0,0 +1,254 @@ +<?php +/** + * TActiveRecordCriteria class file + * Adapts *deprecated* Prado TActiveRecordCriteria to TDbCriteria (Yii) + * @class TActiveRecordCriteria + * @deprecated + * @author Daniel Mueller <tux@penguinfriends.org> + */ + +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: +	 * <code> +	 * $value=$component->PropertyName; +	 * </code> +	 * and to obtain the event handler list for an event, +	 * <code> +	 * $eventHandlerList=$component->EventName; +	 * </code> +	 * @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. +	 * <code> +	 * $this->PropertyName=$value; +	 * $this->EventName=$handler; +	 * </code> +	 * @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 new file mode 100755 index 00000000..f2c4faaa --- /dev/null +++ b/framework/Db/Schema/TDbColumnSchema.php @@ -0,0 +1,145 @@ +<?php +/** + * TDbColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..7c25888d --- /dev/null +++ b/framework/Db/Schema/TDbCommandBuilder.php @@ -0,0 +1,656 @@ +<?php +/** + * TDbCommandBuilder class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..3dab036a --- /dev/null +++ b/framework/Db/Schema/TDbCriteria.php @@ -0,0 +1,166 @@ +<?php +/** + * TDbCriteria class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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, <code>age>31 AND team=1</code>. +	 */ +	public $condition=''; +	/** +	 * @var array list of query parameter values indexed by parameter placeholders. +	 * For example, <code>array(':name'=>'Dan', ':age'=>31)</code>. +	 */ +	public $params=array(); +	/** +	 * @var integer maximum number of records to be returned. If less than 0, it means no limit. +	 */ +	public $limit=-1; +	/** +	 * @var integer zero-based offset from where the records are to be returned. If less than 0, it means starting from the beginning. +	 */ +	public $offset=-1; +	/** +	 * @var string how to sort the query results. This refers to the ORDER BY clause in an SQL statement. +	 */ +	public $order=''; +	/** +	 * @var string how to group the query results. This refers to the GROUP BY clause in an SQL statement. +	 * For example, <code>'projectID, teamID'</code>. +	 */ +	public $group=''; +	/** +	 * @var string how to join with other tables. This refers to the JOIN clause in an SQL statement. +	 * For example, <code>'LEFT JOIN users ON users.id=authorID'</code>. +	 */ +	public $join=''; +	/** +	 * @var string the condition to be applied with GROUP-BY clause. +	 * For example, <code>'SUM(revenue)<50000'</code>. +	 * @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 new file mode 100755 index 00000000..86b9a1ef --- /dev/null +++ b/framework/Db/Schema/TDbExpression.php @@ -0,0 +1,49 @@ +<?php +/** + * TDbExpression class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..6837b7e5 --- /dev/null +++ b/framework/Db/Schema/TDbSchema.php @@ -0,0 +1,193 @@ +<?php +/** + * TDbSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..eaeda1b4 --- /dev/null +++ b/framework/Db/Schema/TDbTableSchema.php @@ -0,0 +1,76 @@ +<?php +/** + * TDbTableSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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: + * <ul> + * <li>{@link name}</li> + * <li>{@link rawName}</li> + * <li>{@link columns}</li> + * <li>{@link primaryKey}</li> + * <li>{@link foreignKeys}</li> + * <li>{@link sequenceName}</li> + * </ul> + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..0c5c6ac4 --- /dev/null +++ b/framework/Db/Schema/mssql/TMssqlColumnSchema.php @@ -0,0 +1,57 @@ +<?php +/** + * TMssqlColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @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 <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @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 new file mode 100755 index 00000000..f0b5c7f4 --- /dev/null +++ b/framework/Db/Schema/mssql/TMssqlCommandBuilder.php @@ -0,0 +1,303 @@ +<?php +/** + * CMsCommandBuilder class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-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 <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + * @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 +	 * +	 * <code> +	 * SELECT * FROM ( +	 *  SELECT TOP n * FROM ( +	 *    SELECT TOP z columns      -- (z=n+skip) +	 *    FROM tablename +	 *    ORDER BY key ASC +	 *  ) AS FOO ORDER BY key DESC -- ('FOO' may be anything) +	 * ) AS BAR ORDER BY key ASC    -- ('BAR' may be anything) +	 * </code> +	 * +	 * <b>Regular expressions are used to alter the SQL query. The resulting SQL query +	 * may be malformed for complex queries.</b> The following restrictions apply +	 * +	 * <ul> +	 *   <li> +	 * In particular, <b>commas</b> should <b>NOT</b> +	 * be used as part of the ordering expression or identifier. Commas must only be +	 * used for separating the ordering clauses. +	 *  </li> +	 *  <li> +	 * In the ORDER BY clause, the column name should NOT be be qualified +	 * with a table name or view name. Alias the column names or use column index. +	 * </li> +	 * <li> +	 * No clauses should follow the ORDER BY clause, e.g. no COMPUTE or FOR clauses. +	 * </li> +	 * +	 * @param string SQL 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 <weizhuo[at]gmail[dot]com> +	 */ +	public function applyLimit($sql, $limit, $offset) +	{ +		$limit = $limit!==null ? intval($limit) : -1; +		$offset = $offset!==null ? intval($offset) : -1; +		if ($limit > 0 && $offset <= 0) //just limit +			$sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $limit", $sql); +		else if($limit > 0 && $offset > 0) +			$sql = $this->rewriteLimitOffsetSql($sql, $limit,$offset); +		return $sql; +	} + +	/** +	 * Rewrite sql to apply $limit > and $offset > 0 for MSSQL database. +	 * See http://troels.arvin.dk/db/rdbms/#select-limit-offset +	 * @param string sql query +	 * @param integer $limit > 0 +	 * @param integer $offset > 0 +	 * @return sql modified sql query applied with limit and offset. +	 * +	 * @author Wei Zhuo <weizhuo[at]gmail[dot]com> +	 */ +	protected function rewriteLimitOffsetSql($sql, $limit, $offset) +	{ +		$fetch = $limit+$offset; +		$sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $fetch", $sql); +		$ordering = $this->findOrdering($sql); + +		$orginalOrdering = $this->joinOrdering($ordering); +		$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 <weizhuo[at]gmail[dot]com> +	 */ +	protected function findOrdering($sql) +	{ +		if(!preg_match('/ORDER BY/i', $sql)) +			return array(); +		$matches=array(); +		$ordering=array(); +		preg_match_all('/(ORDER BY)[\s"\[](.*)(ASC|DESC)?(?:[\s"\[]|$|COMPUTE|FOR)/i', $sql, $matches); +		if(count($matches)>1 && count($matches[2]) > 0) +		{ +			$parts = explode(',', $matches[2][0]); +			foreach($parts as $part) +			{ +				$subs=array(); +				if(preg_match_all('/(.*)[\s"\]](ASC|DESC)$/i', trim($part), $subs)) +				{ +					if(count($subs) > 1 && count($subs[2]) > 0) +					{ +						$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 <weizhuo[at]gmail[dot]com> +	 */ +	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 <weizhuo[at]gmail[dot]com> +	 */ +	protected function reverseDirection($orders) +	{ +		foreach($orders as $column => $direction) +			$orders[$column] = strtolower(trim($direction))==='desc' ? 'ASC' : 'DESC'; +		return $orders; +	} + + +	/** +	 * Checks if the criteria has an order by clause when using offset/limit. +	 * Override parent implementation to check if an orderby clause if specified when querying with an offset +	 * If not, order it by pk. +	 * @param 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 new file mode 100755 index 00000000..c9d4597f --- /dev/null +++ b/framework/Db/Schema/mssql/TMssqlPdoAdapter.php @@ -0,0 +1,74 @@ +<?php +/** + * TMssqlPdo class file + * + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @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 <Christophe.Boulain@gmail.com> + * @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 new file mode 100755 index 00000000..063e8994 --- /dev/null +++ b/framework/Db/Schema/mssql/TMssqlSchema.php @@ -0,0 +1,312 @@ +<?php +/** + * TMssqlSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @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 <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @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 = <<<EOD +		SELECT k.column_name field_name +			FROM {$this->quoteTableName($kcu)} k +		    LEFT JOIN {$this->quoteTableName($tc)} c +		      ON k.table_name = c.table_name +		     AND k.constraint_name = c.constraint_name +		   WHERE c.constraint_type ='PRIMARY KEY' +		   	    AND k.table_name = :table +				AND k.table_schema = :schema +EOD; +		$command = $this->getDbConnection()->createCommand($sql); +		$command->bindValue(':table', $table->name); +		$command->bindValue(':schema', $table->schemaName); +		$primary=$command->queryColumn(); +		switch (count($primary)) +		{ +			case 0: // No primary key on table +				$primary=null; +				break; +			case 1: // Only 1 primary key +				$primary=$primary[0]; +				break; +		} +		return $primary; +	} + +	/** +	 * Gets foreign relationship constraint keys and table name +	 * @param 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 = <<<EOD +		SELECT +		     KCU1.CONSTRAINT_NAME AS 'FK_CONSTRAINT_NAME' +		   , KCU1.TABLE_NAME AS 'FK_TABLE_NAME' +		   , KCU1.COLUMN_NAME AS 'FK_COLUMN_NAME' +		   , KCU1.ORDINAL_POSITION AS 'FK_ORDINAL_POSITION' +		   , KCU2.CONSTRAINT_NAME AS 'UQ_CONSTRAINT_NAME' +		   , KCU2.TABLE_NAME AS 'UQ_TABLE_NAME' +		   , KCU2.COLUMN_NAME AS 'UQ_COLUMN_NAME' +		   , KCU2.ORDINAL_POSITION AS 'UQ_ORDINAL_POSITION' +		FROM {$this->quoteTableName($rc)} RC +		JOIN {$this->quoteTableName($kcu)} KCU1 +		ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG +		   AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA +		   AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME +		JOIN {$this->quoteTableName($kcu)} KCU2 +		ON KCU2.CONSTRAINT_CATALOG = +		RC.UNIQUE_CONSTRAINT_CATALOG +		   AND KCU2.CONSTRAINT_SCHEMA = +		RC.UNIQUE_CONSTRAINT_SCHEMA +		   AND KCU2.CONSTRAINT_NAME = +		RC.UNIQUE_CONSTRAINT_NAME +		   AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION +		WHERE KCU1.TABLE_NAME = :table +EOD; +		$command = $this->getDbConnection()->createCommand($sql); +		$command->bindValue(':table', $table->name); +		$fkeys=array(); +		foreach($command->queryAll() as $info) +		{ +			$fkeys[$info['FK_COLUMN_NAME']]=array($info['UQ_TABLE_NAME'],$info['UQ_COLUMN_NAME'],); + +		} +		return $fkeys; +	} + + +	/** +	 * Collects the table column metadata. +	 * @param 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=<<<EOD +SELECT TABLE_NAME, TABLE_SCHEMA FROM [INFORMATION_SCHEMA].[TABLES] +WHERE TABLE_TYPE='BASE TABLE' AND TABLE_SCHEMA=:schema +EOD; +		$command=$this->getDbConnection()->createCommand($sql); +		$command->bindParam(":schema", $schema); +		$rows=$command->queryAll(); +		$names=array(); +		foreach ($rows as $row) +		{ +			if ($schema == self::DEFAULT_SCHEMA) +				$names[]=$row['TABLE_NAME']; +			else +				$names[]=$schema.'.'.$row['TABLE_SCHEMA'].'.'.$row['TABLE_NAME']; +		} + +		return $names; +	} + +	/** +	 * Creates a command builder for the database. +	 * This method overrides parent implementation in order to create a MSSQL specific command builder +	 * @return 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 new file mode 100755 index 00000000..ed1b7c8a --- /dev/null +++ b/framework/Db/Schema/mssql/TMssqlTableSchema.php @@ -0,0 +1,35 @@ +<?php +/** + * TMssqlTableSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @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 <qiang.xue@gmail.com> + * @author Christophe Boulain <Christophe.Boulain@gmail.com> + * @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 new file mode 100755 index 00000000..d3bb230c --- /dev/null +++ b/framework/Db/Schema/mysql/TMysqlColumnSchema.php @@ -0,0 +1,46 @@ +<?php +/** + * TMysqlColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..8061e7a8 --- /dev/null +++ b/framework/Db/Schema/mysql/TMysqlSchema.php @@ -0,0 +1,205 @@ +<?php +/** + * TMysqlSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..8bed3314 --- /dev/null +++ b/framework/Db/Schema/mysql/TMysqlTableSchema.php @@ -0,0 +1,26 @@ +<?php +/** + * TMysqlTableSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..d609d14d --- /dev/null +++ b/framework/Db/Schema/oci/TOciColumnSchema.php @@ -0,0 +1,56 @@ +<?php +/** + * TOciColumnSchema class file. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @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 <rickgrana@yahoo.com.br> + * @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 new file mode 100755 index 00000000..807b7271 --- /dev/null +++ b/framework/Db/Schema/oci/TOciCommandBuilder.php @@ -0,0 +1,122 @@ +<?php +/** + * TOciCommandBuilder class file. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @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 <rickgrana@yahoo.com.br> + * @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 = <<<EOD +				WITH USER_SQL AS ({$sql}), +				   PAGINATION AS (SELECT USER_SQL.*, rownum as rowNumId FROM USER_SQL) +				SELECT * +				FROM PAGINATION +				{$filter} +EOD; + +		return $sql; +	} + +	/** +	 * Creates an INSERT command. +	 * @param mixed 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(); +		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 new file mode 100755 index 00000000..50de0626 --- /dev/null +++ b/framework/Db/Schema/oci/TOciSchema.php @@ -0,0 +1,278 @@ +<?php +/** + * TOciSchema class file. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @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 <qiang.xue@gmail.com> + * @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=<<<EOD +SELECT a.column_name, a.data_type || +    case +        when data_precision is not null +            then '(' || a.data_precision || +                    case when a.data_scale > 0 then ',' || a.data_scale else '' end +                || ')' +        when data_type = 'DATE' then '' +        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=<<<EOD +		SELECT D.constraint_type, C.COLUMN_NAME, C.position, D.r_constraint_name, +                E.table_name as table_ref, f.column_name as column_ref +        FROM ALL_CONS_COLUMNS C +        inner join ALL_constraints D on D.OWNER = C.OWNER and D.constraint_name = C.constraint_name +        left join ALL_constraints E on E.OWNER = D.r_OWNER and E.constraint_name = D.r_constraint_name +        left join ALL_cons_columns F on F.OWNER = E.OWNER and F.constraint_name = E.constraint_name and F.position = c.position +        WHERE C.OWNER = '{$table->schemaName}' +           and C.table_name = '{$table->name}' +           and D.constraint_type <> 'P' +        order by d.constraint_name, c.position +EOD; +		$command=$this->getDbConnection()->createCommand($sql); +		foreach($command->queryAll() as $row) +		{ +			if($row['constraint_type']==='R')   // foreign key +			{ +				$name = $row["COLUMN_NAME"]; +				$table->foreignKeys[$name]=array($row["TABLE_REF"], 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=<<<EOD +SELECT table_name, '{$schema}' as table_schema FROM user_tables +EOD; +			$command=$this->getDbConnection()->createCommand($sql); +		} +		else +		{ +			$sql=<<<EOD +SELECT object_name as table_name, owner as table_schema FROM all_objects +WHERE object_type = 'TABLE' AND owner=:schema +EOD; +			$command=$this->getDbConnection()->createCommand($sql); +			$command->bindParam(':schema',$schema); +		} + +		$rows=$command->queryAll(); +		$names=array(); +		foreach($rows as $row) +		{ +			if($schema===$this->getDefaultSchema()) +				$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 new file mode 100755 index 00000000..9fe73624 --- /dev/null +++ b/framework/Db/Schema/oci/TOciTableSchema.php @@ -0,0 +1,28 @@ +<?php +/** + * TOciTableSchema class file. + * + * @author Ricardo Grana <rickgrana@yahoo.com.br> + * @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 <rickgrana@yahoo.com.br> + * @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 new file mode 100755 index 00000000..3677cd44 --- /dev/null +++ b/framework/Db/Schema/pgsql/TPgsqlColumnSchema.php @@ -0,0 +1,58 @@ +<?php +/** + * TPgsqlColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..5f009208 --- /dev/null +++ b/framework/Db/Schema/pgsql/TPgsqlSchema.php @@ -0,0 +1,284 @@ +<?php +/** + * TPgsqlSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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=<<<EOD +SELECT a.attname, LOWER(format_type(a.atttypid, a.atttypmod)) AS type, d.adsrc, a.attnotnull, a.atthasdef +FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum +WHERE a.attnum > 0 AND NOT a.attisdropped +	AND a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table +		AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = :schema)) +ORDER BY a.attnum +EOD; +		$command=$this->getDbConnection()->createCommand($sql); +		$command->bindValue(':table',$table->name); +		$command->bindValue(':schema',$table->schemaName); + +		if(($columns=$command->queryAll())===array()) +			return false; + +		foreach($columns as $column) +		{ +			$c=$this->createColumn($column); +			$table->columns[$c->name]=$c; + +			if(stripos($column['adsrc'],'nextval')===0 && preg_match('/nextval\([^\']*\'([^\']+)\'[^\)]*\)/i',$column['adsrc'],$matches)) +			{ +				if(strpos($matches[1],'.')!==false || $table->schemaName===self::DEFAULT_SCHEMA) +					$this->_sequences[$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=<<<EOD +SELECT conname, consrc, contype, indkey FROM ( +	SELECT +		conname, +		CASE WHEN contype='f' THEN +			pg_catalog.pg_get_constraintdef(oid) +		ELSE +			'CHECK (' || consrc || ')' +		END AS consrc, +		contype, +		conrelid AS relid, +		NULL AS indkey +	FROM +		pg_catalog.pg_constraint +	WHERE +		contype IN ('f', 'c') +	UNION ALL +	SELECT +		pc.relname, +		NULL, +		CASE WHEN indisprimary THEN +				'p' +		ELSE +				'u' +		END, +		pi.indrelid, +		indkey +	FROM +		pg_catalog.pg_class pc, +		pg_catalog.pg_index pi +	WHERE +		pc.oid=pi.indexrelid +		AND EXISTS ( +			SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c +			ON (d.refclassid = c.tableoid AND d.refobjid = c.oid) +			WHERE d.classid = pc.tableoid AND d.objid = pc.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p') +	) +) AS sub +WHERE relid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table +	AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace +	WHERE nspname=:schema)) +EOD; +		$command=$this->getDbConnection()->createCommand($sql); +		$command->bindValue(':table',$table->name); +		$command->bindValue(':schema',$table->schemaName); +		foreach($command->queryAll() as $row) +		{ +			if($row['contype']==='p') // primary key +				$this->findPrimaryKey($table,$row['indkey']); +			else if($row['contype']==='f') // foreign key +				$this->findForeignKey($table,$row['consrc']); +		} +	} + +	/** +	 * Collects primary key information. +	 * @param TPgsqlTableSchema the table metadata +	 * @param string pgsql primary key index list +	 */ +	protected function findPrimaryKey($table,$indices) +	{ +		$indices=implode(', ',preg_split('/\s+/',$indices)); +		$sql=<<<EOD +SELECT attnum, attname FROM pg_catalog.pg_attribute WHERE +	attrelid=( +		SELECT oid FROM pg_catalog.pg_class WHERE relname=:table AND relnamespace=( +			SELECT oid FROM pg_catalog.pg_namespace WHERE nspname=:schema +		) +	) +    AND attnum IN ({$indices}) +EOD; +		$command=$this->getDbConnection()->createCommand($sql); +		$command->bindValue(':table',$table->name); +		$command->bindValue(':schema',$table->schemaName); +		foreach($command->queryAll() as $row) +		{ +			$name=$row['attname']; +			if(isset($table->columns[$name])) +			{ +				$table->columns[$name]->isPrimaryKey=true; +				if($table->primaryKey===null) +					$table->primaryKey=$name; +				else if(is_string($table->primaryKey)) +					$table->primaryKey=array($table->primaryKey,$name); +				else +					$table->primaryKey[]=$name; +			} +		} +	} + +	/** +	 * Collects foreign key information. +	 * @param 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=<<<EOD +SELECT table_name, table_schema FROM information_schema.tables +WHERE table_schema=:schema +EOD; +		$command=$this->getDbConnection()->createCommand($sql); +		$command->bindParam(':schema',$schema); +		$rows=$command->queryAll(); +		$names=array(); +		foreach($rows as $row) +		{ +			if($schema===self::DEFAULT_SCHEMA) +				$names[]=$row['table_name']; +			else +				$names[]=$row['schema_name'].'.'.$row['table_name']; +		} +		return $names; +	} +} diff --git a/framework/Db/Schema/pgsql/TPgsqlTableSchema.php b/framework/Db/Schema/pgsql/TPgsqlTableSchema.php new file mode 100755 index 00000000..9c14da28 --- /dev/null +++ b/framework/Db/Schema/pgsql/TPgsqlTableSchema.php @@ -0,0 +1,27 @@ +<?php +/** + * TPgsqlTable class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..ca6858eb --- /dev/null +++ b/framework/Db/Schema/sqlite/TSqliteColumnSchema.php @@ -0,0 +1,35 @@ +<?php +/** + * TSqliteColumnSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..69ffd8a8 --- /dev/null +++ b/framework/Db/Schema/sqlite/TSqliteCommandBuilder.php @@ -0,0 +1,43 @@ +<?php +/** + * TSqliteCommandBuilder class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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 new file mode 100755 index 00000000..0e3d4f9a --- /dev/null +++ b/framework/Db/Schema/sqlite/TSqliteSchema.php @@ -0,0 +1,134 @@ +<?php +/** + * TSqliteSchema class file. + * + * @author Qiang Xue <qiang.xue@gmail.com> + * @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 <qiang.xue@gmail.com> + * @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; +	} +}  | 
