CMssqlCommandBuilder.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. <?php
  2. /**
  3. * CMsCommandBuilder class file.
  4. *
  5. * @author Qiang Xue <qiang.xue@gmail.com>
  6. * @author Christophe Boulain <Christophe.Boulain@gmail.com>
  7. * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
  8. * @link http://www.yiiframework.com/
  9. * @copyright 2008-2013 Yii Software LLC
  10. * @license http://www.yiiframework.com/license/
  11. */
  12. /**
  13. * CMssqlCommandBuilder provides basic methods to create query commands for tables for Mssql Servers.
  14. *
  15. * @author Qiang Xue <qiang.xue@gmail.com>
  16. * @author Christophe Boulain <Christophe.Boulain@gmail.com>
  17. * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
  18. * @package system.db.schema.mssql
  19. */
  20. class CMssqlCommandBuilder extends CDbCommandBuilder
  21. {
  22. /**
  23. * Creates a COUNT(*) command for a single table.
  24. * Override parent implementation to remove the order clause of criteria if it exists
  25. * @param CDbTableSchema $table the table metadata
  26. * @param CDbCriteria $criteria the query criteria
  27. * @param string $alias the alias name of the primary table. Defaults to 't'.
  28. * @return CDbCommand query command.
  29. */
  30. public function createCountCommand($table,$criteria,$alias='t')
  31. {
  32. $criteria->order='';
  33. return parent::createCountCommand($table, $criteria,$alias);
  34. }
  35. /**
  36. * Creates a SELECT command for a single table.
  37. * Override parent implementation to check if an orderby clause if specified when querying with an offset
  38. * @param CDbTableSchema $table the table metadata
  39. * @param CDbCriteria $criteria the query criteria
  40. * @param string $alias the alias name of the primary table. Defaults to 't'.
  41. * @return CDbCommand query command.
  42. */
  43. public function createFindCommand($table,$criteria,$alias='t')
  44. {
  45. $criteria=$this->checkCriteria($table,$criteria);
  46. return parent::createFindCommand($table,$criteria,$alias);
  47. }
  48. /**
  49. * Creates an UPDATE command.
  50. * Override parent implementation because mssql don't want to update an identity column
  51. * @param CDbTableSchema $table the table metadata
  52. * @param array $data list of columns to be updated (name=>value)
  53. * @param CDbCriteria $criteria the query criteria
  54. * @throws CDbException if no columns are being updated
  55. * @return CDbCommand update command.
  56. */
  57. public function createUpdateCommand($table,$data,$criteria)
  58. {
  59. $criteria=$this->checkCriteria($table,$criteria);
  60. $fields=array();
  61. $values=array();
  62. $bindByPosition=isset($criteria->params[0]);
  63. $i=0;
  64. foreach($data as $name=>$value)
  65. {
  66. if(($column=$table->getColumn($name))!==null)
  67. {
  68. if ($table->sequenceName !== null && $column->isPrimaryKey === true) continue;
  69. if ($column->dbType === 'timestamp') continue;
  70. if($value instanceof CDbExpression)
  71. {
  72. $fields[]=$column->rawName.'='.$value->expression;
  73. foreach($value->params as $n=>$v)
  74. $values[$n]=$v;
  75. }
  76. elseif($bindByPosition)
  77. {
  78. $fields[]=$column->rawName.'=?';
  79. $values[]=$column->typecast($value);
  80. }
  81. else
  82. {
  83. $fields[]=$column->rawName.'='.self::PARAM_PREFIX.$i;
  84. $values[self::PARAM_PREFIX.$i]=$column->typecast($value);
  85. $i++;
  86. }
  87. }
  88. }
  89. if($fields===array())
  90. throw new CDbException(Yii::t('yii','No columns are being updated for table "{table}".',
  91. array('{table}'=>$table->name)));
  92. $sql="UPDATE {$table->rawName} SET ".implode(', ',$fields);
  93. $sql=$this->applyJoin($sql,$criteria->join);
  94. $sql=$this->applyCondition($sql,$criteria->condition);
  95. $sql=$this->applyOrder($sql,$criteria->order);
  96. $sql=$this->applyLimit($sql,$criteria->limit,$criteria->offset);
  97. $command=$this->getDbConnection()->createCommand($sql);
  98. $this->bindValues($command,array_merge($values,$criteria->params));
  99. return $command;
  100. }
  101. /**
  102. * Creates a DELETE command.
  103. * Override parent implementation to check if an orderby clause if specified when querying with an offset
  104. * @param CDbTableSchema $table the table metadata
  105. * @param CDbCriteria $criteria the query criteria
  106. * @return CDbCommand delete command.
  107. */
  108. public function createDeleteCommand($table,$criteria)
  109. {
  110. $criteria=$this->checkCriteria($table, $criteria);
  111. return parent::createDeleteCommand($table, $criteria);
  112. }
  113. /**
  114. * Creates an UPDATE command that increments/decrements certain columns.
  115. * Override parent implementation to check if an orderby clause if specified when querying with an offset
  116. * @param CDbTableSchema $table the table metadata
  117. * @param CDbCriteria $counters the query criteria
  118. * @param array $criteria counters to be updated (counter increments/decrements indexed by column names.)
  119. * @return CDbCommand the created command
  120. * @throws CException if no counter is specified
  121. */
  122. public function createUpdateCounterCommand($table,$counters,$criteria)
  123. {
  124. $criteria=$this->checkCriteria($table, $criteria);
  125. return parent::createUpdateCounterCommand($table, $counters, $criteria);
  126. }
  127. /**
  128. * This is a port from Prado Framework.
  129. *
  130. * Overrides parent implementation. Alters the sql to apply $limit and $offset.
  131. * The idea for limit with offset is done by modifying the sql on the fly
  132. * with numerous assumptions on the structure of the sql string.
  133. * The modification is done with reference to the notes from
  134. * http://troels.arvin.dk/db/rdbms/#select-limit-offset
  135. *
  136. * <code>
  137. * SELECT * FROM (
  138. * SELECT TOP n * FROM (
  139. * SELECT TOP z columns -- (z=n+skip)
  140. * FROM tablename
  141. * ORDER BY key ASC
  142. * ) AS FOO ORDER BY key DESC -- ('FOO' may be anything)
  143. * ) AS BAR ORDER BY key ASC -- ('BAR' may be anything)
  144. * </code>
  145. *
  146. * <b>Regular expressions are used to alter the SQL query. The resulting SQL query
  147. * may be malformed for complex queries.</b> The following restrictions apply
  148. *
  149. * <ul>
  150. * <li>
  151. * In particular, <b>commas</b> should <b>NOT</b>
  152. * be used as part of the ordering expression or identifier. Commas must only be
  153. * used for separating the ordering clauses.
  154. * </li>
  155. * <li>
  156. * In the ORDER BY clause, the column name should NOT be be qualified
  157. * with a table name or view name. Alias the column names or use column index.
  158. * </li>
  159. * <li>
  160. * No clauses should follow the ORDER BY clause, e.g. no COMPUTE or FOR clauses.
  161. * </li>
  162. * </ul>
  163. *
  164. * @param string $sql SQL query string.
  165. * @param integer $limit maximum number of rows, -1 to ignore limit.
  166. * @param integer $offset row offset, -1 to ignore offset.
  167. * @return string SQL with limit and offset.
  168. *
  169. * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
  170. */
  171. public function applyLimit($sql, $limit, $offset)
  172. {
  173. $limit = $limit!==null ? (int)$limit : -1;
  174. $offset = $offset!==null ? (int)$offset : -1;
  175. if ($limit > 0 && $offset <= 0) //just limit
  176. $sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $limit", $sql);
  177. elseif($limit > 0 && $offset > 0)
  178. $sql = $this->rewriteLimitOffsetSql($sql, $limit,$offset);
  179. return $sql;
  180. }
  181. /**
  182. * Rewrite sql to apply $limit > and $offset > 0 for MSSQL database.
  183. * See http://troels.arvin.dk/db/rdbms/#select-limit-offset
  184. * @param string $sql sql query
  185. * @param integer $limit $limit > 0
  186. * @param integer $offset $offset > 0
  187. * @return string modified sql query applied with limit and offset.
  188. *
  189. * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
  190. */
  191. protected function rewriteLimitOffsetSql($sql, $limit, $offset)
  192. {
  193. $fetch = $limit+$offset;
  194. $sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $fetch", $sql);
  195. $ordering = $this->findOrdering($sql);
  196. $orginalOrdering = $this->joinOrdering($ordering, '[__outer__]');
  197. $reverseOrdering = $this->joinOrdering($this->reverseDirection($ordering), '[__inner__]');
  198. $sql = "SELECT * FROM (SELECT TOP {$limit} * FROM ($sql) as [__inner__] {$reverseOrdering}) as [__outer__] {$orginalOrdering}";
  199. return $sql;
  200. }
  201. /**
  202. * Base on simplified syntax http://msdn2.microsoft.com/en-us/library/aa259187(SQL.80).aspx
  203. *
  204. * @param string $sql $sql
  205. * @return array ordering expression as key and ordering direction as value
  206. *
  207. * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
  208. */
  209. protected function findOrdering($sql)
  210. {
  211. if(!preg_match('/ORDER BY/i', $sql))
  212. return array();
  213. $matches=array();
  214. $ordering=array();
  215. preg_match_all('/(ORDER BY)[\s"\[](.*)(ASC|DESC)?(?:[\s"\[]|$|COMPUTE|FOR)/i', $sql, $matches);
  216. if(count($matches)>1 && count($matches[2]) > 0)
  217. {
  218. $parts = explode(',', $matches[2][0]);
  219. foreach($parts as $part)
  220. {
  221. $subs=array();
  222. if(preg_match_all('/(.*)[\s"\]](ASC|DESC)$/i', trim($part), $subs))
  223. {
  224. if(count($subs) > 1 && count($subs[2]) > 0)
  225. {
  226. $name='';
  227. foreach(explode('.', $subs[1][0]) as $p)
  228. {
  229. if($name!=='')
  230. $name.='.';
  231. $name.='[' . trim($p, '[]') . ']';
  232. }
  233. $ordering[$name] = $subs[2][0];
  234. }
  235. //else what?
  236. }
  237. else
  238. $ordering[trim($part)] = 'ASC';
  239. }
  240. }
  241. // replacing column names with their alias names
  242. foreach($ordering as $name => $direction)
  243. {
  244. $matches = array();
  245. $pattern = '/\s+'.str_replace(array('[',']'), array('\[','\]'), $name).'\s+AS\s+(\[[^\]]+\])/i';
  246. preg_match($pattern, $sql, $matches);
  247. if(isset($matches[1]))
  248. {
  249. $ordering[$matches[1]] = $ordering[$name];
  250. unset($ordering[$name]);
  251. }
  252. }
  253. return $ordering;
  254. }
  255. /**
  256. * @param array $orders ordering obtained from findOrdering()
  257. * @param string $newPrefix new table prefix to the ordering columns
  258. * @return string concat the orderings
  259. *
  260. * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
  261. */
  262. protected function joinOrdering($orders, $newPrefix)
  263. {
  264. if(count($orders)>0)
  265. {
  266. $str=array();
  267. foreach($orders as $column => $direction)
  268. $str[] = $column.' '.$direction;
  269. $orderBy = 'ORDER BY '.implode(', ', $str);
  270. return preg_replace('/\s+\[[^\]]+\]\.(\[[^\]]+\])/i', ' '.$newPrefix.'.\1', $orderBy);
  271. }
  272. }
  273. /**
  274. * @param array $orders original ordering
  275. * @return array ordering with reversed direction.
  276. *
  277. * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
  278. */
  279. protected function reverseDirection($orders)
  280. {
  281. foreach($orders as $column => $direction)
  282. $orders[$column] = strtolower(trim($direction))==='desc' ? 'ASC' : 'DESC';
  283. return $orders;
  284. }
  285. /**
  286. * Checks if the criteria has an order by clause when using offset/limit.
  287. * Override parent implementation to check if an orderby clause if specified when querying with an offset
  288. * If not, order it by pk.
  289. * @param CMssqlTableSchema $table table schema
  290. * @param CDbCriteria $criteria criteria
  291. * @return CDbCriteria the modified criteria
  292. */
  293. protected function checkCriteria($table, $criteria)
  294. {
  295. if ($criteria->offset > 0 && $criteria->order==='')
  296. {
  297. $criteria->order=is_array($table->primaryKey)?implode(',',$table->primaryKey):$table->primaryKey;
  298. }
  299. return $criteria;
  300. }
  301. /**
  302. * Generates the expression for selecting rows with specified composite key values.
  303. * @param CDbTableSchema $table the table schema
  304. * @param array $values list of primary key values to be selected within
  305. * @param string $prefix column prefix (ended with dot)
  306. * @return string the expression for selection
  307. */
  308. protected function createCompositeInCondition($table,$values,$prefix)
  309. {
  310. $vs=array();
  311. foreach($values as $value)
  312. {
  313. $c=array();
  314. foreach($value as $k=>$v)
  315. $c[]=$prefix.$table->columns[$k]->rawName.'='.$v;
  316. $vs[]='('.implode(' AND ',$c).')';
  317. }
  318. return '('.implode(' OR ',$vs).')';
  319. }
  320. }