Skip to content
This repository
Fetching contributors…

Cannot retrieve contributors at this time

file 338 lines (320 sloc) 11.388 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
<?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 &copy; 2008-2011 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/

/**
* CMssqlCommandBuilder provides basic methods to create query commands for tables for Mssql Servers.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @author Christophe Boulain <Christophe.Boulain@gmail.com>
* @author Wei Zhuo <weizhuo[at]gmail[dot]com>
* @version $Id$
* @package system.db.schema.mssql
*/
class CMssqlCommandBuilder extends CDbCommandBuilder
{
/**
* Creates a COUNT(*) command for a single table.
* Override parent implementation to remove the order clause of criteria if it exists
* @param CDbTableSchema $table the table metadata
* @param CDbCriteria $criteria the query criteria
* @param string $alias the alias name of the primary table. Defaults to 't'.
* @return CDbCommand query command.
*/
public function createCountCommand($table,$criteria,$alias='t')
{
$criteria->order='';
return parent::createCountCommand($table, $criteria,$alias);
}

/**
* Creates a SELECT command for a single table.
* Override parent implementation to check if an orderby clause if specified when querying with an offset
* @param CDbTableSchema $table the table metadata
* @param CDbCriteria $criteria the query criteria
* @param string $alias the alias name of the primary table. Defaults to 't'.
* @return CDbCommand query command.
*/
public function createFindCommand($table,$criteria,$alias='t')
{
$criteria=$this->checkCriteria($table,$criteria);
return parent::createFindCommand($table,$criteria,$alias);

}

/**
* Creates an UPDATE command.
* Override parent implementation because mssql don't want to update an identity column
* @param CDbTableSchema $table the table metadata
* @param array $data list of columns to be updated (name=>value)
* @param CDbCriteria $criteria the query criteria
* @return CDbCommand update command.
*/
public function createUpdateCommand($table,$data,$criteria)
{
$criteria=$this->checkCriteria($table,$criteria);
$fields=array();
$values=array();
$bindByPosition=isset($criteria->params[0]);
$i=0;
foreach($data as $name=>$value)
{
if(($column=$table->getColumn($name))!==null)
{
if ($table->sequenceName !== null && $column->isPrimaryKey === true) continue;
if ($column->dbType === 'timestamp') continue;
if($value instanceof CDbExpression)
{
$fields[]=$column->rawName.'='.$value->expression;
foreach($value->params as $n=>$v)
$values[$n]=$v;
}
else if($bindByPosition)
{
$fields[]=$column->rawName.'=?';
$values[]=$column->typecast($value);
}
else
{
$fields[]=$column->rawName.'='.self::PARAM_PREFIX.$i;
$values[self::PARAM_PREFIX.$i]=$column->typecast($value);
$i++;
}
}
}
if($fields===array())
throw new CDbException(Yii::t('yii','No columns are being updated for table "{table}".',
array('{table}'=>$table->name)));
$sql="UPDATE {$table->rawName} SET ".implode(', ',$fields);
$sql=$this->applyJoin($sql,$criteria->join);
$sql=$this->applyCondition($sql,$criteria->condition);
$sql=$this->applyOrder($sql,$criteria->order);
$sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset);

$command=$this->getDbConnection()->createCommand($sql);
$this->bindValues($command,array_merge($values,$criteria->params));

return $command;
}

/**
* Creates a DELETE command.
* Override parent implementation to check if an orderby clause if specified when querying with an offset
* @param CDbTableSchema $table the table metadata
* @param CDbCriteria $criteria the query criteria
* @return CDbCommand delete command.
*/
public function createDeleteCommand($table,$criteria)
{
$criteria=$this->checkCriteria($table, $criteria);
return parent::createDeleteCommand($table, $criteria);
}

/**
* Creates an UPDATE command that increments/decrements certain columns.
* Override parent implementation to check if an orderby clause if specified when querying with an offset
* @param CDbTableSchema $table the table metadata
* @param CDbCriteria $counters the query criteria
* @param array $criteria counters to be updated (counter increments/decrements indexed by column names.)
* @return CDbCommand the created command
* @throws CException if no counter is specified
*/
public function createUpdateCounterCommand($table,$counters,$criteria)
{
$criteria=$this->checkCriteria($table, $criteria);
return parent::createUpdateCounterCommand($table, $counters, $criteria);
}

/**
* This is a port from Prado Framework.
*
* Overrides parent implementation. Alters the sql to apply $limit and $offset.
* The idea for limit with offset is done by modifying the sql on the fly
* with numerous assumptions on the structure of the sql string.
* The modification is done with reference to the notes from
* http://troels.arvin.dk/db/rdbms/#select-limit-offset
*
* <code>
* SELECT * FROM (
* SELECT TOP n * FROM (
* SELECT TOP z columns -- (z=n+skip)
* FROM tablename
* ORDER BY key ASC
* ) AS FOO ORDER BY key DESC -- ('FOO' may be anything)
* ) AS BAR ORDER BY key ASC -- ('BAR' may be anything)
* </code>
*
* <b>Regular expressions are used to alter the SQL query. The resulting SQL query
* may be malformed for complex queries.</b> The following restrictions apply
*
* <ul>
* <li>
* In particular, <b>commas</b> should <b>NOT</b>
* be used as part of the ordering expression or identifier. Commas must only be
* used for separating the ordering clauses.
* </li>
* <li>
* In the ORDER BY clause, the column name should NOT be be qualified
* with a table name or view name. Alias the column names or use column index.
* </li>
* <li>
* No clauses should follow the ORDER BY clause, e.g. no COMPUTE or FOR clauses.
* </li>
* </ul>
*
* @param string $sql SQL query string.
* @param integer $limit maximum number of rows, -1 to ignore limit.
* @param integer $offset row offset, -1 to ignore offset.
* @return string SQL with limit and offset.
*
* @author Wei Zhuo <weizhuo[at]gmail[dot]com>
*/
public function applyLimit($sql, $limit, $offset)
{
$limit = $limit!==null ? intval($limit) : -1;
$offset = $offset!==null ? intval($offset) : -1;
if ($limit > 0 && $offset <= 0) //just limit
$sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $limit", $sql);
else if($limit > 0 && $offset > 0)
$sql = $this->rewriteLimitOffsetSql($sql, $limit,$offset);
return $sql;
}

/**
* Rewrite sql to apply $limit > and $offset > 0 for MSSQL database.
* See http://troels.arvin.dk/db/rdbms/#select-limit-offset
* @param string $sql sql query
* @param integer $limit $limit > 0
* @param integer $offset $offset > 0
* @return string modified sql query applied with limit and offset.
*
* @author Wei Zhuo <weizhuo[at]gmail[dot]com>
*/
protected function rewriteLimitOffsetSql($sql, $limit, $offset)
{
$fetch = $limit+$offset;
$sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $fetch", $sql);
$ordering = $this->findOrdering($sql);
$orginalOrdering = $this->joinOrdering($ordering, '[__outer__]');
$reverseOrdering = $this->joinOrdering($this->reverseDirection($ordering), '[__inner__]');
$sql = "SELECT * FROM (SELECT TOP {$limit} * FROM ($sql) as [__inner__] {$reverseOrdering}) as [__outer__] {$orginalOrdering}";
return $sql;
}

/**
* Base on simplified syntax http://msdn2.microsoft.com/en-us/library/aa259187(SQL.80).aspx
*
* @param string $sql $sql
* @return array ordering expression as key and ordering direction as value
*
* @author Wei Zhuo <weizhuo[at]gmail[dot]com>
*/
protected function findOrdering($sql)
{
if(!preg_match('/ORDER BY/i', $sql))
return array();
$matches=array();
$ordering=array();
preg_match_all('/(ORDER BY)[\s"\[](.*)(ASC|DESC)?(?:[\s"\[]|$|COMPUTE|FOR)/i', $sql, $matches);
if(count($matches)>1 && count($matches[2]) > 0)
{
$parts = explode(',', $matches[2][0]);
foreach($parts as $part)
{
$subs=array();
if(preg_match_all('/(.*)[\s"\]](ASC|DESC)$/i', trim($part), $subs))
{
if(count($subs) > 1 && count($subs[2]) > 0)
{
$name='';
foreach(explode('.', $subs[1][0]) as $p)
{
if($name!=='')
$name.='.';
$name.='[' . trim($p, '[]') . ']';
}
$ordering[$name] = $subs[2][0];
}
//else what?
}
else
$ordering[trim($part)] = 'ASC';
}
}

// replacing column names with their alias names
foreach($ordering as $name => $direction)
{
$matches = array();
$pattern = '/\s+'.str_replace(array('[',']'), array('\[','\]'), $name).'\s+AS\s+(\[[^\]]+\])/i';
preg_match($pattern, $sql, $matches);
if(isset($matches[1]))
{
$ordering[$matches[1]] = $ordering[$name];
unset($ordering[$name]);
}
}

return $ordering;
}

/**
* @param array $orders ordering obtained from findOrdering()
* @param string $newPrefix new table prefix to the ordering columns
* @return string concat the orderings
*
* @author Wei Zhuo <weizhuo[at]gmail[dot]com>
*/
protected function joinOrdering($orders, $newPrefix)
{
if(count($orders)>0)
{
$str=array();
foreach($orders as $column => $direction)
$str[] = $column.' '.$direction;
$orderBy = 'ORDER BY '.implode(', ', $str);
return preg_replace('/\s+\[[^\]]+\]\.(\[[^\]]+\])/i', ' '.$newPrefix.'.\1', $orderBy);
}
}

/**
* @param array $orders original ordering
* @return array ordering with reversed direction.
*
* @author Wei Zhuo <weizhuo[at]gmail[dot]com>
*/
protected function reverseDirection($orders)
{
foreach($orders as $column => $direction)
$orders[$column] = strtolower(trim($direction))==='desc' ? 'ASC' : 'DESC';
return $orders;
}


/**
* Checks if the criteria has an order by clause when using offset/limit.
* Override parent implementation to check if an orderby clause if specified when querying with an offset
* If not, order it by pk.
* @param CMssqlTableSchema $table table schema
* @param CDbCriteria $criteria criteria
* @return CDbCriteria the modified criteria
*/
protected function checkCriteria($table, $criteria)
{
if ($criteria->offset > 0 && $criteria->order==='')
{
$criteria->order=is_array($table->primaryKey)?implode(',',$table->primaryKey):$table->primaryKey;
}
return $criteria;
}

/**
* Generates the expression for selecting rows with specified composite key values.
* @param CDbTableSchema $table the table schema
* @param array $values list of primary key values to be selected within
* @param string $prefix column prefix (ended with dot)
* @return string the expression for selection
*/
protected function createCompositeInCondition($table,$values,$prefix)
{
$vs=array();
foreach($values as $value)
{
$c=array();
foreach($value as $k=>$v)
$c[]=$prefix.$table->columns[$k]->rawName.'='.$v;
$vs[]='('.implode(' AND ',$c).')';
}
return '('.implode(' OR ',$vs).')';
}
}
Something went wrong with that request. Please try again.