|
1 | 1 | <?php |
2 | 2 | /* |
3 | | - * $Id: Mssql.php 7659 2010-06-08 18:16:17Z jwage $ |
| 3 | + * $Id: Mssql.php 7690 2010-08-31 17:11:24Z jwage $ |
4 | 4 | * |
5 | 5 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
6 | 6 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
27 | 27 | * @license http://www.opensource.org/licenses/lgpl-license.php LGPL |
28 | 28 | * @author Konsta Vesterinen <kvesteri@cc.hut.fi> |
29 | 29 | * @author Lukas Smith <smith@pooteeweet.org> (PEAR MDB2 library) |
30 | | - * @version $Revision: 7659 $ |
| 30 | + * @version $Revision: 7690 $ |
31 | 31 | * @link www.doctrine-project.org |
32 | 32 | * @since 1.0 |
33 | 33 | */ |
@@ -140,100 +140,154 @@ public function quoteIdentifier($identifier, $checkOption = false) |
140 | 140 | * @link http://lists.bestpractical.com/pipermail/rt-devel/2005-June/007339.html |
141 | 141 | * @return string |
142 | 142 | */ |
143 | | - public function modifyLimitQuery($query, $limit = false, $offset = false, $isManip = false, $isSubQuery = false) |
| 143 | + public function modifyLimitQuery($query, $limit = false, $offset = false, $isManip = false, $isSubQuery = false, Doctrine_Query $queryOrigin = null) |
144 | 144 | { |
145 | | - if ($limit > 0) { |
146 | | - $count = intval($limit); |
147 | | - $offset = intval($offset); |
| 145 | + if ($limit === false || !($limit > 0)) { |
| 146 | + return $query; |
| 147 | + } |
148 | 148 |
|
149 | | - if ($offset < 0) { |
150 | | - throw new Doctrine_Connection_Exception("LIMIT argument offset=$offset is not valid"); |
151 | | - } |
| 149 | + $orderby = stristr($query, 'ORDER BY'); |
152 | 150 |
|
153 | | - $orderby = stristr($query, 'ORDER BY'); |
| 151 | + if ($offset !== false && $orderby === false) { |
| 152 | + throw new Doctrine_Connection_Exception("OFFSET cannot be used in MSSQL without ORDER BY due to emulation reasons."); |
| 153 | + } |
| 154 | + |
| 155 | + $count = intval($limit); |
| 156 | + $offset = intval($offset); |
154 | 157 |
|
155 | | - if ($orderby !== false) { |
156 | | - // Ticket #1835: Fix for ORDER BY alias |
157 | | - // Ticket #2050: Fix for multiple ORDER BY clause |
158 | | - $order = str_ireplace('ORDER BY', '', $orderby); |
159 | | - $orders = explode(',', $order); |
| 158 | + if ($offset < 0) { |
| 159 | + throw new Doctrine_Connection_Exception("LIMIT argument offset=$offset is not valid"); |
| 160 | + } |
160 | 161 |
|
161 | | - for ($i = 0; $i < count($orders); $i++) { |
162 | | - $sorts[$i] = (stripos($orders[$i], ' desc') !== false) ? 'DESC' : 'ASC'; |
163 | | - $orders[$i] = trim(preg_replace('/\s+(ASC|DESC)$/i', '', $orders[$i])); |
| 162 | + $orderbySql = $queryOrigin->getSqlQueryPart('orderby'); |
| 163 | + $orderbyDql = $queryOrigin->getDqlPart('orderby'); |
164 | 164 |
|
165 | | - // find alias in query string |
166 | | - $helper_string = stristr($query, $orders[$i]); |
| 165 | + if ($orderby !== false) { |
| 166 | + $orders = $this->parseOrderBy(implode(', ', $queryOrigin->getDqlPart('orderby'))); |
167 | 167 |
|
168 | | - $from_clause_pos = strpos($helper_string, ' FROM '); |
169 | | - $fields_string = substr($helper_string, 0, $from_clause_pos + 1); |
| 168 | + for ($i = 0; $i < count($orders); $i++) { |
| 169 | + $sorts[$i] = (stripos($orders[$i], ' desc') !== false) ? 'DESC' : 'ASC'; |
| 170 | + $orders[$i] = trim(preg_replace('/\s+(ASC|DESC)$/i', '', $orders[$i])); |
170 | 171 |
|
171 | | - $field_array = explode(',', $fields_string); |
172 | | - $field_array = array_shift($field_array); |
173 | | - $aux2 = spliti(' as ', $field_array); |
174 | | - $aux2 = explode('.', end($aux2)); |
| 172 | + list($fieldAliases[$i], $fields[$i]) = strstr($orders[$i], '.') ? explode('.', $orders[$i]) : array('', $orders[$i]); |
| 173 | + $columnAlias[$i] = $queryOrigin->getSqlTableAlias($queryOrigin->getExpressionOwner($orders[$i])); |
175 | 174 |
|
176 | | - $aliases[$i] = trim(end($aux2)); |
177 | | - } |
| 175 | + $cmp = $queryOrigin->getQueryComponent($queryOrigin->getExpressionOwner($orders[$i])); |
| 176 | + $tables[$i] = $cmp['table']; |
| 177 | + $columns[$i] = $cmp['table']->getColumnName($fields[$i]); |
| 178 | + |
| 179 | + // TODO: This sould be refactored as method called Doctrine_Table::getColumnAlias(<column name>). |
| 180 | + $aliases[$i] = $columnAlias[$i] . '__' . $columns[$i]; |
178 | 181 | } |
| 182 | + } |
179 | 183 |
|
180 | | - // Ticket #1259: Fix for limit-subquery in MSSQL |
181 | | - $selectRegExp = 'SELECT\s+'; |
182 | | - $selectReplace = 'SELECT '; |
| 184 | + // Ticket #1259: Fix for limit-subquery in MSSQL |
| 185 | + $selectRegExp = 'SELECT\s+'; |
| 186 | + $selectReplace = 'SELECT '; |
183 | 187 |
|
184 | | - if (preg_match('/^SELECT(\s+)DISTINCT/i', $query)) { |
185 | | - $selectRegExp .= 'DISTINCT\s+'; |
186 | | - $selectReplace .= 'DISTINCT '; |
187 | | - } |
| 188 | + if (preg_match('/^SELECT(\s+)DISTINCT/i', $query)) { |
| 189 | + $selectRegExp .= 'DISTINCT\s+'; |
| 190 | + $selectReplace .= 'DISTINCT '; |
| 191 | + } |
188 | 192 |
|
189 | | - $fields_string = substr($query, strlen($selectReplace), strpos($query, ' FROM ') - strlen($selectReplace)); |
190 | | - $field_array = explode(',', $fields_string); |
191 | | - $field_array = array_shift($field_array); |
192 | | - $aux2 = spliti(' as ', $field_array); |
193 | | - $aux2 = explode('.', end($aux2)); |
194 | | - $key_field = trim(end($aux2)); |
| 193 | + $fields_string = substr($query, strlen($selectReplace), strpos($query, ' FROM ') - strlen($selectReplace)); |
| 194 | + $field_array = explode(',', $fields_string); |
| 195 | + $field_array = array_shift($field_array); |
| 196 | + $aux2 = preg_split('/ as /i', $field_array); |
| 197 | + $aux2 = explode('.', end($aux2)); |
| 198 | + $key_field = trim(end($aux2)); |
195 | 199 |
|
196 | | - $query = preg_replace('/^'.$selectRegExp.'/i', $selectReplace . 'TOP ' . ($count + $offset) . ' ', $query); |
| 200 | + $query = preg_replace('/^'.$selectRegExp.'/i', $selectReplace . 'TOP ' . ($count + $offset) . ' ', $query); |
197 | 201 |
|
198 | | - if ($isSubQuery === true) { |
199 | | - $query = 'SELECT TOP ' . $count . ' ' . $this->quoteIdentifier('inner_tbl') . '.' . $key_field . ' FROM (' . $query . ') AS ' . $this->quoteIdentifier('inner_tbl'); |
200 | | - } else { |
201 | | - $query = 'SELECT * FROM (SELECT TOP ' . $count . ' * FROM (' . $query . ') AS ' . $this->quoteIdentifier('inner_tbl'); |
202 | | - } |
| 202 | + if ($isSubQuery === true) { |
| 203 | + $query = 'SELECT TOP ' . $count . ' ' . $this->quoteIdentifier('inner_tbl') . '.' . $key_field . ' FROM (' . $query . ') AS ' . $this->quoteIdentifier('inner_tbl'); |
| 204 | + } else { |
| 205 | + $query = 'SELECT * FROM (SELECT TOP ' . $count . ' * FROM (' . $query . ') AS ' . $this->quoteIdentifier('inner_tbl'); |
| 206 | + } |
203 | 207 |
|
204 | | - if ($orderby !== false) { |
205 | | - $query .= ' ORDER BY '; |
| 208 | + if ($orderby !== false) { |
| 209 | + $query .= ' ORDER BY '; |
206 | 210 |
|
207 | | - for ($i = 0, $l = count($orders); $i < $l; $i++) { |
208 | | - if ($i > 0) { // not first order clause |
209 | | - $query .= ', '; |
210 | | - } |
| 211 | + for ($i = 0, $l = count($orders); $i < $l; $i++) { |
| 212 | + if ($i > 0) { // not first order clause |
| 213 | + $query .= ', '; |
| 214 | + } |
211 | 215 |
|
212 | | - $query .= $this->quoteIdentifier('inner_tbl') . '.' . $aliases[$i] . ' '; |
213 | | - $query .= (stripos($sorts[$i], 'asc') !== false) ? 'DESC' : 'ASC'; |
214 | | - } |
| 216 | + $query .= $this->modifyOrderByColumn($tables[$i], $columns[$i], $this->quoteIdentifier('inner_tbl') . '.' . $this->quoteIdentifier($aliases[$i])) . ' '; |
| 217 | + $query .= (stripos($sorts[$i], 'asc') !== false) ? 'DESC' : 'ASC'; |
215 | 218 | } |
| 219 | + } |
216 | 220 |
|
217 | | - if ($isSubQuery !== true) { |
218 | | - $query .= ') AS ' . $this->quoteIdentifier('outer_tbl'); |
219 | | - |
220 | | - if ($orderby !== false) { |
221 | | - $query .= ' ORDER BY '; |
| 221 | + if ($isSubQuery !== true) { |
| 222 | + $query .= ') AS ' . $this->quoteIdentifier('outer_tbl'); |
222 | 223 |
|
223 | | - for ($i = 0, $l = count($orders); $i < $l; $i++) { |
224 | | - if ($i > 0) { // not first order clause |
225 | | - $query .= ', '; |
226 | | - } |
| 224 | + if ($orderby !== false) { |
| 225 | + $query .= ' ORDER BY '; |
227 | 226 |
|
228 | | - $query .= $this->quoteIdentifier('outer_tbl') . '.' . $aliases[$i] . ' ' . $sorts[$i]; |
| 227 | + for ($i = 0, $l = count($orders); $i < $l; $i++) { |
| 228 | + if ($i > 0) { // not first order clause |
| 229 | + $query .= ', '; |
229 | 230 | } |
| 231 | + |
| 232 | + $query .= $this->modifyOrderByColumn($tables[$i], $columns[$i], $this->quoteIdentifier('outer_tbl') . '.' . $this->quoteIdentifier($aliases[$i])) . ' ' . $sorts[$i]; |
230 | 233 | } |
231 | 234 | } |
232 | 235 | } |
233 | 236 |
|
234 | 237 | return $query; |
235 | 238 | } |
236 | 239 |
|
| 240 | + /** |
| 241 | + * Parse an OrderBy-Statement into chunks |
| 242 | + * |
| 243 | + * @param string $orderby |
| 244 | + */ |
| 245 | + private function parseOrderBy($orderby) |
| 246 | + { |
| 247 | + $matches = array(); |
| 248 | + $chunks = array(); |
| 249 | + $tokens = array(); |
| 250 | + $parsed = str_ireplace('ORDER BY', '', $orderby); |
| 251 | + |
| 252 | + preg_match_all('/(\w+\(.+?\)\s+(ASC|DESC)),?/', $orderby, $matches); |
| 253 | + |
| 254 | + $matchesWithExpressions = $matches[1]; |
| 255 | + |
| 256 | + foreach ($matchesWithExpressions as $match) { |
| 257 | + $chunks[] = $match; |
| 258 | + $parsed = str_replace($match, '##' . (count($chunks) - 1) . '##', $parsed); |
| 259 | + } |
| 260 | + |
| 261 | + $tokens = preg_split('/,/', $parsed); |
| 262 | + |
| 263 | + for ($i = 0, $iMax = count($tokens); $i < $iMax; $i++) { |
| 264 | + $tokens[$i] = trim(preg_replace('/##(\d+)##/e', "\$chunks[\\1]", $tokens[$i])); |
| 265 | + } |
| 266 | + |
| 267 | + return $tokens; |
| 268 | + } |
| 269 | + |
| 270 | + /** |
| 271 | + * Order and Group By are not possible on columns from type text. |
| 272 | + * This method fix this issue by wrap the given term (column) into a CAST directive. |
| 273 | + * |
| 274 | + * @see DC-828 |
| 275 | + * @param Doctrine_Table $table |
| 276 | + * @param string $field |
| 277 | + * @param string $term The term which will changed if it's necessary, depending to the field type. |
| 278 | + * @return string |
| 279 | + */ |
| 280 | + public function modifyOrderByColumn(Doctrine_Table $table, $field, $term) |
| 281 | + { |
| 282 | + $def = $table->getDefinitionOf($field); |
| 283 | + |
| 284 | + if ($def['type'] == 'string' && $def['length'] === NULL) { |
| 285 | + $term = 'CAST(' . $term . ' AS varchar(8000))'; |
| 286 | + } |
| 287 | + |
| 288 | + return $term; |
| 289 | + } |
| 290 | + |
237 | 291 | /** |
238 | 292 | * Creates dbms specific LIMIT/OFFSET SQL for the subqueries that are used in the |
239 | 293 | * context of the limit-subquery algorithm. |
@@ -347,18 +401,12 @@ public function exec($query, array $params = array()) |
347 | 401 | protected function replaceBoundParamsWithInlineValuesInQuery($query, array $params) { |
348 | 402 |
|
349 | 403 | foreach($params as $key => $value) { |
350 | | - if(is_null($value)) { |
351 | | - $value = 'NULL'; |
352 | | - } |
353 | | - else { |
354 | | - $value = $this->quote($value); |
355 | | - } |
356 | | - |
357 | | - $re = '/([=,\(][^\\\']*)(\?)/iU'; |
358 | | - |
359 | | - $query = preg_replace($re, "\\1 {$value}", $query, 1); |
360 | | - |
| 404 | + $re = '/(?<=WHERE|VALUES|SET|JOIN)(.*?)(\?)/'; |
| 405 | + $query = preg_replace($re, "\\1##{$key}##", $query, 1); |
361 | 406 | } |
| 407 | + |
| 408 | + $replacement = 'is_null($value) ? \'NULL\' : $this->quote($params[\\1])'; |
| 409 | + $query = preg_replace('/##(\d+)##/e', $replacement, $query); |
362 | 410 |
|
363 | 411 | return $query; |
364 | 412 |
|
|
0 commit comments