=== modified file 'includes/database.inc' --- includes/database.inc 2010-03-04 02:32:39 +0000 +++ includes/database.inc 2010-03-29 02:51:24 +0000 @@ -402,7 +402,7 @@ */ function db_distinct_field($table, $field, $query) { $matches = array(); - if (!preg_match('/^SELECT\s*DISTINCT/i', $query, $matches)) { + if (!preg_match('/^SELECT\s*DISTINCT/i', $query, $matches) && db_distinct_allowed($query)) { // Only add distinct to the outer SELECT to avoid messing up subqueries. $query = preg_replace('/^SELECT/i', 'SELECT DISTINCT', $query); } @@ -410,6 +410,90 @@ } /** + * Helper function for db_distinct_field + * + * This tries to establish whether adding a DISTINCT to a query is allowed. + * It checks the ORDER BY clause for this, on non-mysql systems; + * if any fields in it are not present in the FROM clause, this is llegal in + * strict SQL92 dialects like PostgreSQL (and also illogical). + * + * This function should die as soon as db_distinct_field does. + * + * @param $query + * SQL to check + * @return + * FALSE if it is established that this query cannot be DISTINCT; TRUE otherwise. + * The presence of 'DISTINCT' in $query has no effect on the return value. + */ +function db_distinct_allowed($query) { + global $db_type; + $distinct = TRUE; + + if (strpos($db_type, 'mysql') !== 0 && preg_match('/ORDER\s+BY\s+(.*)$/is', $query, $ordermatches)) { + + // Break the SELECT part down into different fields AND their aliases, since an 'ORDER BY' can be + // on a field, an alias (or an order number). + //if (preg_match('/^SELECT\s+(.+?)\s+FROM/is', $query, $matches)) { <-- this one is ok when calling from within db_distinct_field() + if (preg_match('/^SELECT\s+(?:DISTINCT\s+)?(.+?)\s+FROM/is', $query, $matches)) { + // Get fields. + // (Treat all commas as field separators, and all spaces as separating the field from the alias. + // It's imperfect, but it'll do for our purpose. + // We're just living with the imperfection of db_distinct_field() until the end of its lifetime, anyway.) + $f = explode(',', $matches[1]); + $fields = array_map("strtolower", preg_replace('/^\s*(\S+).*$/s', '$1', $f)); + $fieldnames = array_map("_db_distinct_strip_tablealias", $fields); + // Add field aliases. (Or fieldnames again - we don't care about duplicates in $fields.) + $fields = array_merge($fields, array_map("strtolower", preg_replace('/^.*?(\S+)\s*$/s', '$1', $f))); + + // Break ORDER clause apart into fields. + // (Instead of stripping off ' ASC' / ' DESC', strip off anything after a space, since we did that with $fields too. + // This will also get rid of a ' LIMIT n' directive that may be following the ORDER BY) + $orderfields = array_map("strtolower", preg_replace('/^\s*(\S+).*$/s', '$1', explode(',', $ordermatches[1]))); + + // Try to find all order fields in $fields + // If one field is not found there, do not add the DISTINCT. + foreach ($orderfields as $orderfield) { + + if (!is_int($orderfield)) { + if (array_search($orderfield, $fields) === FALSE) { + // Not found. Extra check based on presence of a table alias: + if ($pos = strpos($orderfield, '.')) { + // look for .* in $fields + if (array_search(substr($orderfield, 0, $pos) . '.*', $fields) === FALSE) { + // 'order field' not found; bail + $distinct = FALSE; + break; + } + } + else { + // look for .$orderfield in $fields ==> for $orderfield in $fieldnames + if (array_search($orderfield, $fieldnames) === FALSE) { + // 'order field' not found; bail + $distinct = FALSE; + break; + } + } + } + } + } + } + } + + return $distinct; +} + +/** + * Helper function for db_distinct_allowed + */ +function _db_distinct_strip_tablealias($field) { + if ($pos = strpos($field, '.')) { + return substr($field, $pos + 1); + } + // Note: If $field started with a dot, that's just too bad. + return $field; +} + +/** * Restrict a dynamic table, column or constraint name to safe characters. * * Only keeps alphanumeric and underscores. === modified file 'modules/node/node.module'