DBMSSQL.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. <?php
  2. /**
  3. * This file is part of the Propel package.
  4. * For the full copyright and license information, please view the LICENSE
  5. * file that was distributed with this source code.
  6. *
  7. * @license MIT License
  8. */
  9. /**
  10. * This is used to connect to a MSSQL database.
  11. *
  12. * @author Hans Lellelid <hans@xmpl.org> (Propel)
  13. * @version $Revision: 1700 $
  14. * @package propel.runtime.adapter
  15. */
  16. class DBMSSQL extends DBAdapter
  17. {
  18. /**
  19. * This method is used to ignore case.
  20. *
  21. * @param in The string to transform to upper case.
  22. * @return The upper case string.
  23. */
  24. public function toUpperCase($in)
  25. {
  26. return $this->ignoreCase($in);
  27. }
  28. /**
  29. * This method is used to ignore case.
  30. *
  31. * @param in The string whose case to ignore.
  32. * @return The string in a case that can be ignored.
  33. */
  34. public function ignoreCase($in)
  35. {
  36. return 'UPPER(' . $in . ')';
  37. }
  38. /**
  39. * Returns SQL which concatenates the second string to the first.
  40. *
  41. * @param string String to concatenate.
  42. * @param string String to append.
  43. * @return string
  44. */
  45. public function concatString($s1, $s2)
  46. {
  47. return '(' . $s1 . ' + ' . $s2 . ')';
  48. }
  49. /**
  50. * Returns SQL which extracts a substring.
  51. *
  52. * @param string String to extract from.
  53. * @param int Offset to start from.
  54. * @param int Number of characters to extract.
  55. * @return string
  56. */
  57. public function subString($s, $pos, $len)
  58. {
  59. return 'SUBSTRING(' . $s . ', ' . $pos . ', ' . $len . ')';
  60. }
  61. /**
  62. * Returns SQL which calculates the length (in chars) of a string.
  63. *
  64. * @param string String to calculate length of.
  65. * @return string
  66. */
  67. public function strLength($s)
  68. {
  69. return 'LEN(' . $s . ')';
  70. }
  71. /**
  72. * @see DBAdapter::quoteIdentifier()
  73. */
  74. public function quoteIdentifier($text)
  75. {
  76. return '[' . $text . ']';
  77. }
  78. /**
  79. * @see DBAdapter::random()
  80. */
  81. public function random($seed = null)
  82. {
  83. return 'RAND(' . ((int)$seed) . ')';
  84. }
  85. /**
  86. * Simulated Limit/Offset
  87. * This rewrites the $sql query to apply the offset and limit.
  88. * some of the ORDER BY logic borrowed from Doctrine MsSqlPlatform
  89. * @see DBAdapter::applyLimit()
  90. * @author Benjamin Runnels <kraven@kraven.org>
  91. */
  92. public function applyLimit(&$sql, $offset, $limit)
  93. {
  94. // make sure offset and limit are numeric
  95. if(! is_numeric($offset) || ! is_numeric($limit))
  96. {
  97. throw new PropelException('DBMSSQL::applyLimit() expects a number for argument 2 and 3');
  98. }
  99. //split the select and from clauses out of the original query
  100. $selectSegment = array();
  101. $selectText = 'SELECT ';
  102. if (preg_match('/\Aselect(\s+)distinct/i', $sql)) {
  103. $selectText .= 'DISTINCT ';
  104. }
  105. preg_match('/\Aselect(.*)from(.*)/si', $sql, $selectSegment);
  106. if(count($selectSegment) == 3) {
  107. $selectStatement = trim($selectSegment[1]);
  108. $fromStatement = trim($selectSegment[2]);
  109. } else {
  110. throw new Exception('DBMSSQL::applyLimit() could not locate the select statement at the start of the query.');
  111. }
  112. // if we're starting at offset 0 then theres no need to simulate limit,
  113. // just grab the top $limit number of rows
  114. if($offset == 0) {
  115. $sql = $selectText . 'TOP ' . $limit . ' ' . $selectStatement . ' FROM ' . $fromStatement;
  116. return;
  117. }
  118. //get the ORDER BY clause if present
  119. $orderStatement = stristr($fromStatement, 'ORDER BY');
  120. $orders = '';
  121. if($orderStatement !== false) {
  122. //remove order statement from the from statement
  123. $fromStatement = trim(str_replace($orderStatement, '', $fromStatement));
  124. $order = str_ireplace('ORDER BY', '', $orderStatement);
  125. $orders = explode(',', $order);
  126. for($i = 0; $i < count($orders); $i ++) {
  127. $orderArr[trim(preg_replace('/\s+(ASC|DESC)$/i', '', $orders[$i]))] = array(
  128. 'sort' => (stripos($orders[$i], ' DESC') !== false) ? 'DESC' : 'ASC',
  129. 'key' => $i
  130. );
  131. }
  132. }
  133. //setup inner and outer select selects
  134. $innerSelect = '';
  135. $outerSelect = '';
  136. foreach(explode(', ', $selectStatement) as $selCol) {
  137. $selColArr = explode(' ', $selCol);
  138. $selColCount = count($selColArr) - 1;
  139. //make sure the current column isn't * or an aggregate
  140. if($selColArr[0] != '*' && ! strstr($selColArr[0], '(')) {
  141. if(isset($orderArr[$selColArr[0]])) {
  142. $orders[$orderArr[$selColArr[0]]['key']] = $selColArr[0] . ' ' . $orderArr[$selColArr[0]]['sort'];
  143. }
  144. //use the alias if one was present otherwise use the column name
  145. $alias = (! stristr($selCol, ' AS ')) ? $this->quoteIdentifier($selColArr[0]) : $this->quoteIdentifier($selColArr[$selColCount]);
  146. //save the first non-aggregate column for use in ROW_NUMBER() if required
  147. if(! isset($firstColumnOrderStatement)) {
  148. $firstColumnOrderStatement = 'ORDER BY ' . $selColArr[0];
  149. }
  150. //add an alias to the inner select so all columns will be unique
  151. $innerSelect .= $selColArr[0] . ' AS ' . $alias . ', ';
  152. $outerSelect .= $alias . ', ';
  153. } else {
  154. //agregate columns must always have an alias clause
  155. if(! stristr($selCol, ' AS ')) {
  156. throw new Exception('DBMSSQL::applyLimit() requires aggregate columns to have an Alias clause');
  157. }
  158. //aggregate column alias can't be used as the count column you must use the entire aggregate statement
  159. if(isset($orderArr[$selColArr[$selColCount]])) {
  160. $orders[$orderArr[$selColArr[$selColCount]]['key']] = str_replace($selColArr[$selColCount - 1] . ' ' . $selColArr[$selColCount], '', $selCol) . $orderArr[$selColArr[$selColCount]]['sort'];
  161. }
  162. //quote the alias
  163. $alias = $this->quoteIdentifier($selColArr[$selColCount]);
  164. $innerSelect .= str_replace($selColArr[$selColCount], $alias, $selCol) . ', ';
  165. $outerSelect .= $alias . ', ';
  166. }
  167. }
  168. if(is_array($orders)) {
  169. $orderStatement = 'ORDER BY ' . implode(', ', $orders);
  170. } else {
  171. //use the first non aggregate column in our select statement if no ORDER BY clause present
  172. if(isset($firstColumnOrderStatement)) {
  173. $orderStatement = $firstColumnOrderStatement;
  174. } else {
  175. throw new Exception('DBMSSQL::applyLimit() unable to find column to use with ROW_NUMBER()');
  176. }
  177. }
  178. //substring the select strings to get rid of the last comma and add our FROM and SELECT clauses
  179. $innerSelect = $selectText . 'ROW_NUMBER() OVER(' . $orderStatement . ') AS RowNumber, ' . substr($innerSelect, 0, - 2) . ' FROM';
  180. //outer select can't use * because of the RowNumber column
  181. $outerSelect = 'SELECT ' . substr($outerSelect, 0, - 2) . ' FROM';
  182. //ROW_NUMBER() starts at 1 not 0
  183. $sql = $outerSelect . ' (' . $innerSelect . ' ' . $fromStatement . ') AS derivedb WHERE RowNumber BETWEEN ' . ($offset + 1) . ' AND ' . ($limit + $offset);
  184. return;
  185. }
  186. }