Permalink
Browse files

MSSQL "ORDER BY" and "LIMIT" directive rewritten due DC-828, it now u…

…ses the existing API heavily instead of try to parse the stuff
  • Loading branch information...
1 parent 680b4ba commit 108be3fab81b19be9ba56b3d60c9c1b248a08ac1 @estahn estahn committed Sep 2, 2010
Showing with 258 additions and 71 deletions.
  1. +118 −64 lib/Doctrine/Connection/Mssql.php
  2. +3 −3 lib/Doctrine/Query.php
  3. +10 −4 lib/Doctrine/Query/Orderby.php
  4. +127 −0 tests/Ticket/DC828TestCase.php
@@ -140,93 +140,96 @@ public function quoteIdentifier($identifier, $checkOption = false)
* @link http://lists.bestpractical.com/pipermail/rt-devel/2005-June/007339.html
* @return string
*/
- public function modifyLimitQuery($query, $limit = false, $offset = false, $isManip = false, $isSubQuery = false)
+ public function modifyLimitQuery($query, $limit = false, $offset = false, $isManip = false, $isSubQuery = false, Doctrine_Query $queryOrigin = null)
@MoOx

MoOx Feb 24, 2012

This last parameter is not tested before used. It throws error in many case.

{
- if ($limit > 0) {
- $count = intval($limit);
- $offset = intval($offset);
+ if ($limit === false || !($limit > 0)) {
+ return $query;
+ }
- if ($offset < 0) {
- throw new Doctrine_Connection_Exception("LIMIT argument offset=$offset is not valid");
- }
+ $orderby = stristr($query, 'ORDER BY');
- $orderby = stristr($query, 'ORDER BY');
+ if ($offset !== false && $orderby === false) {
+ throw new Doctrine_Connection_Exception("OFFSET cannot be used in MSSQL without ORDER BY due to emulation reasons.");
+ }
+
+ $count = intval($limit);
+ $offset = intval($offset);
- if ($orderby !== false) {
- // Ticket #1835: Fix for ORDER BY alias
- // Ticket #2050: Fix for multiple ORDER BY clause
- $order = str_ireplace('ORDER BY', '', $orderby);
- $orders = explode(',', $order);
+ if ($offset < 0) {
+ throw new Doctrine_Connection_Exception("LIMIT argument offset=$offset is not valid");
+ }
- for ($i = 0; $i < count($orders); $i++) {
- $sorts[$i] = (stripos($orders[$i], ' desc') !== false) ? 'DESC' : 'ASC';
- $orders[$i] = trim(preg_replace('/\s+(ASC|DESC)$/i', '', $orders[$i]));
+ $orderbySql = $queryOrigin->getSqlQueryPart('orderby');
+ $orderbyDql = $queryOrigin->getDqlPart('orderby');
- // find alias in query string
- $helper_string = stristr($query, $orders[$i]);
+ if ($orderby !== false) {
+ $orders = $this->parseOrderBy(implode(', ', $queryOrigin->getDqlPart('orderby')));
- $from_clause_pos = strpos($helper_string, ' FROM ');
- $fields_string = substr($helper_string, 0, $from_clause_pos + 1);
+ for ($i = 0; $i < count($orders); $i++) {
+ $sorts[$i] = (stripos($orders[$i], ' desc') !== false) ? 'DESC' : 'ASC';
+ $orders[$i] = trim(preg_replace('/\s+(ASC|DESC)$/i', '', $orders[$i]));
- $fieldArray = explode(',', $fields_string);
- $fieldArray = array_shift($fieldArray);
- $aux2 = preg_split('/ as /i', $fieldArray);
- $aux2 = explode('.', end($aux2));
+ list($fieldAliases[$i], $fields[$i]) = strstr($orders[$i], '.') ? explode('.', $orders[$i]) : array('', $orders[$i]);
+ $columnAlias[$i] = $queryOrigin->getSqlTableAlias($queryOrigin->getExpressionOwner($orders[$i]));
- $aliases[$i] = trim(end($aux2));
- }
+ $cmp = $queryOrigin->getQueryComponent($queryOrigin->getExpressionOwner($orders[$i]));
+ $tables[$i] = $cmp['table'];
+ $columns[$i] = $cmp['table']->getColumnName($fields[$i]);
+
+ // TODO: This sould be refactored as method called Doctrine_Table::getColumnAlias(<column name>).
+ $aliases[$i] = $columnAlias[$i] . '__' . $columns[$i];
}
+ }
- // Ticket #1259: Fix for limit-subquery in MSSQL
- $selectRegExp = 'SELECT\s+';
- $selectReplace = 'SELECT ';
+ // Ticket #1259: Fix for limit-subquery in MSSQL
+ $selectRegExp = 'SELECT\s+';
+ $selectReplace = 'SELECT ';
- if (preg_match('/^SELECT(\s+)DISTINCT/i', $query)) {
- $selectRegExp .= 'DISTINCT\s+';
- $selectReplace .= 'DISTINCT ';
- }
+ if (preg_match('/^SELECT(\s+)DISTINCT/i', $query)) {
+ $selectRegExp .= 'DISTINCT\s+';
+ $selectReplace .= 'DISTINCT ';
+ }
- $fields_string = substr($query, strlen($selectReplace), strpos($query, ' FROM ') - strlen($selectReplace));
- $fieldArray = explode(',', $fields_string);
- $fieldArray = array_shift($fieldArray);
- $aux2 = preg_split('/ as /i', $fieldArray);
- $aux2 = explode('.', end($aux2));
- $key_field = trim(end($aux2));
+ $fields_string = substr($query, strlen($selectReplace), strpos($query, ' FROM ') - strlen($selectReplace));
+ $field_array = explode(',', $fields_string);
+ $field_array = array_shift($field_array);
+ $aux2 = preg_split('/ as /i', $field_array);
+ $aux2 = explode('.', end($aux2));
+ $key_field = trim(end($aux2));
- $query = preg_replace('/^'.$selectRegExp.'/i', $selectReplace . 'TOP ' . ($count + $offset) . ' ', $query);
+ $query = preg_replace('/^'.$selectRegExp.'/i', $selectReplace . 'TOP ' . ($count + $offset) . ' ', $query);
- if ($isSubQuery === true) {
- $query = 'SELECT TOP ' . $count . ' ' . $this->quoteIdentifier('inner_tbl') . '.' . $key_field . ' FROM (' . $query . ') AS ' . $this->quoteIdentifier('inner_tbl');
- } else {
- $query = 'SELECT * FROM (SELECT TOP ' . $count . ' * FROM (' . $query . ') AS ' . $this->quoteIdentifier('inner_tbl');
- }
+ if ($isSubQuery === true) {
+ $query = 'SELECT TOP ' . $count . ' ' . $this->quoteIdentifier('inner_tbl') . '.' . $key_field . ' FROM (' . $query . ') AS ' . $this->quoteIdentifier('inner_tbl');
+ } else {
+ $query = 'SELECT * FROM (SELECT TOP ' . $count . ' * FROM (' . $query . ') AS ' . $this->quoteIdentifier('inner_tbl');
+ }
- if ($orderby !== false) {
- $query .= ' ORDER BY ';
+ if ($orderby !== false) {
+ $query .= ' ORDER BY ';
- for ($i = 0, $l = count($orders); $i < $l; $i++) {
- if ($i > 0) { // not first order clause
- $query .= ', ';
- }
+ for ($i = 0, $l = count($orders); $i < $l; $i++) {
+ if ($i > 0) { // not first order clause
+ $query .= ', ';
+ }
- $query .= $this->quoteIdentifier('inner_tbl') . '.' . $aliases[$i] . ' ';
- $query .= (stripos($sorts[$i], 'asc') !== false) ? 'DESC' : 'ASC';
- }
+ $query .= $this->modifyOrderByColumn($tables[$i], $columns[$i], $this->quoteIdentifier('inner_tbl') . '.' . $this->quoteIdentifier($aliases[$i])) . ' ';
+ $query .= (stripos($sorts[$i], 'asc') !== false) ? 'DESC' : 'ASC';
}
+ }
- if ($isSubQuery !== true) {
- $query .= ') AS ' . $this->quoteIdentifier('outer_tbl');
-
- if ($orderby !== false) {
- $query .= ' ORDER BY ';
+ if ($isSubQuery !== true) {
+ $query .= ') AS ' . $this->quoteIdentifier('outer_tbl');
- for ($i = 0, $l = count($orders); $i < $l; $i++) {
- if ($i > 0) { // not first order clause
- $query .= ', ';
- }
+ if ($orderby !== false) {
+ $query .= ' ORDER BY ';
- $query .= $this->quoteIdentifier('outer_tbl') . '.' . $aliases[$i] . ' ' . $sorts[$i];
+ for ($i = 0, $l = count($orders); $i < $l; $i++) {
+ if ($i > 0) { // not first order clause
+ $query .= ', ';
}
+
+ $query .= $this->modifyOrderByColumn($tables[$i], $columns[$i], $this->quoteIdentifier('outer_tbl') . '.' . $this->quoteIdentifier($aliases[$i])) . ' ' . $sorts[$i];
}
}
}
@@ -235,6 +238,57 @@ public function modifyLimitQuery($query, $limit = false, $offset = false, $isMan
}
/**
+ * Parse an OrderBy-Statement into chunks
+ *
+ * @param string $orderby
+ */
+ private function parseOrderBy($orderby)
+ {
+ $matches = array();
+ $chunks = array();
+ $tokens = array();
+ $parsed = str_ireplace('ORDER BY', '', $orderby);
+
+ preg_match_all('/(\w+\(.+?\)\s+(ASC|DESC)),?/', $orderby, $matches);
+
+ $matchesWithExpressions = $matches[1];
+
+ foreach ($matchesWithExpressions as $match) {
+ $chunks[] = $match;
+ $parsed = str_replace($match, '##' . (count($chunks) - 1) . '##', $parsed);
+ }
+
+ $tokens = preg_split('/,/', $parsed);
+
+ for ($i = 0, $iMax = count($tokens); $i < $iMax; $i++) {
+ $tokens[$i] = trim(preg_replace('/##(\d+)##/e', "\$chunks[\\1]", $tokens[$i]));
+ }
+
+ return $tokens;
+ }
+
+ /**
+ * Order and Group By are not possible on columns from type text.
+ * This method fix this issue by wrap the given term (column) into a CAST directive.
+ *
+ * @see DC-828
+ * @param Doctrine_Table $table
+ * @param string $field
+ * @param string $term The term which will changed if it's necessary, depending to the field type.
+ * @return string
+ */
+ public function modifyOrderByColumn(Doctrine_Table $table, $field, $term)
+ {
+ $def = $table->getDefinitionOf($field);
+
+ if ($def['type'] == 'string' && $def['length'] === NULL) {
+ $term = 'CAST(' . $term . ' AS varchar(8000))';
+ }
+
+ return $term;
+ }
+
+ /**
* Creates dbms specific LIMIT/OFFSET SQL for the subqueries that are used in the
* context of the limit-subquery algorithm.
*
View
@@ -827,7 +827,7 @@ public function parseIdentifierReference($expr)
}
- public function parseFunctionExpression($expr)
+ public function parseFunctionExpression($expr, $parseCallback = null)
{
$pos = strpos($expr, '(');
$name = substr($expr, 0, $pos);
@@ -841,7 +841,7 @@ public function parseFunctionExpression($expr)
// parse args
foreach ($this->_tokenizer->sqlExplode($argStr, ',') as $arg) {
- $args[] = $this->parseClause($arg);
+ $args[] = $parseCallback ? call_user_func_array($parseCallback, array($arg)) : $this->parseClause($arg);
}
// convert DQL function to its RDBMS specific equivalent
@@ -1369,7 +1369,7 @@ public function buildSqlQuery($limitSubquery = true)
$q .= ( ! empty($this->_sqlParts['orderby'])) ? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : '';
if ($modifyLimit) {
- $q = $this->_conn->modifyLimitQuery($q, $this->_sqlParts['limit'], $this->_sqlParts['offset']);
+ $q = $this->_conn->modifyLimitQuery($q, $this->_sqlParts['limit'], $this->_sqlParts['offset'], false, false, $this);
}
$q .= $this->_sqlParts['forUpdate'] === true ? ' FOR UPDATE ' : '';
@@ -51,7 +51,7 @@ public function parse($clause, $append = false)
if ($pos !== false) {
$name = substr($term[0], 0, $pos);
- $term[0] = $this->query->parseFunctionExpression($term[0]);
+ $term[0] = $this->query->parseFunctionExpression($term[0], array($this, 'parse'));
} else {
if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'") {
@@ -102,6 +102,9 @@ public function parse($clause, $append = false)
// build sql expression
$term[0] = $conn->quoteIdentifier($tableAlias) . '.' . $conn->quoteIdentifier($field);
+
+ // driver specific modifications
+ $term[0] = method_exists($conn, 'modifyOrderByColumn') ? $conn->modifyOrderByColumn($table, $field, $term[0]) : $term[0];
} else {
// build sql expression
$field = $this->query->getRoot()->getColumnName($field);
@@ -136,7 +139,7 @@ public function parse($clause, $append = false)
$def = $table->getDefinitionOf($term[0]);
// get the actual column name from field name
- $term[0] = $table->getColumnName($term[0]);
+ $field = $table->getColumnName($term[0]);
if (isset($def['owner'])) {
@@ -149,11 +152,14 @@ public function parse($clause, $append = false)
if ($this->query->getType() === Doctrine_Query::SELECT) {
// build sql expression
$term[0] = $conn->quoteIdentifier($tableAlias)
- . '.' . $conn->quoteIdentifier($term[0]);
+ . '.' . $conn->quoteIdentifier($field);
} else {
// build sql expression
- $term[0] = $conn->quoteIdentifier($term[0]);
+ $term[0] = $conn->quoteIdentifier($field);
}
+
+ // driver specific modifications
+ $term[0] = method_exists($conn, 'modifyOrderByColumn') ? $conn->modifyOrderByColumn($table, $field, $term[0]) : $term[0];
} else {
$found = false;
}
Oops, something went wrong.

0 comments on commit 108be3f

Please sign in to comment.