includes/query.inc

Go to the documentation of this file.
00001 <?php
00002 // $Id: query.inc,v 1.50.2.1 2009/11/02 22:56:44 merlinofchaos Exp $
00011 class views_query {
00012 
00016   var $table_queue = array();
00017 
00021   var $tables = array();
00022 
00027   var $relationships = array();
00028 
00034   var $where = array();
00040   var $having = array();
00045   var $group_operator = 'AND';
00046 
00050   var $orderby = array();
00051 
00055   var $groupby = array();
00056 
00061   var $header = array();
00062 
00066   var $distinct = FALSE;
00067 
00071   function views_query($base_table = 'node', $base_field = 'nid') {
00072     $this->base_table = $base_table;  // Predefine these above, for clarity.
00073     $this->base_field = $base_field;
00074     $this->relationships[$base_table] = array(
00075       'link' => NULL,
00076       'table' => $base_table,
00077       'alias' => $base_table,
00078       'base' => $base_table
00079     );
00080 
00081     // init the table queue with our primary table.
00082     $this->table_queue[$base_table] = array(
00083       'alias' => $base_table,
00084       'table' => $base_table,
00085       'relationship' => $base_table,
00086       'join' => NULL,
00087     );
00088 
00089     // init the tables with our primary table
00090     $this->tables[$base_table][$base_table] = array(
00091       'count' => 1,
00092       'alias' => $base_table,
00093     );
00094 
00095     if ($base_field) {
00096       $this->fields[$base_field] = array(
00097         'table' => $base_table,
00098         'field' => $base_field,
00099         'alias' => $base_field,
00100       );
00101     }
00102 
00103     $this->count_field = array(
00104       'table' => $base_table,
00105       'field' => $base_field,
00106       'alias' => $base_field,
00107       'count' => TRUE,
00108     );
00109   }
00110 
00111   // ----------------------------------------------------------------
00112   // Utility methods to set flags and data.
00113 
00117   function set_distinct($value = TRUE) {
00118     if (!(isset($this->no_distinct) && $value)) {
00119       $this->distinct = $value;
00120     }
00121   }
00122 
00126   function set_count_field($table, $field, $alias = NULL) {
00127     if (empty($alias)) {
00128       $alias = $table . '_' . $field;
00129     }
00130     $this->count_field = array(
00131       'table' => $table,
00132       'field' => $field,
00133       'alias' => $alias,
00134       'count' => TRUE,
00135     );
00136   }
00137 
00142   function set_header($header) {
00143     $this->header = $header;
00144   }
00145 
00146   // ----------------------------------------------------------------
00147   // Table/join adding
00148 
00177   function add_relationship($alias, $join, $base, $link_point = NULL) {
00178     if (empty($link_point)) {
00179       $link_point = $this->base_table;
00180     }
00181     else if (!array_key_exists($link_point, $this->relationships)) {
00182       return FALSE;
00183     }
00184 
00185     // Make sure $alias isn't already used; if it, start adding stuff.
00186     $alias_base = $alias;
00187     $count = 1;
00188     while (!empty($this->relationships[$alias])) {
00189       $alias = $alias_base . '_' . $count++;
00190     }
00191 
00192     // Make sure this join is adjusted for our relationship.
00193     if ($link_point && isset($this->relationships[$link_point])) {
00194       $join = $this->adjust_join($join, $link_point);
00195     }
00196 
00197     // Add the table directly to the queue to avoid accidentally marking
00198     // it.
00199     $this->table_queue[$alias] = array(
00200       'table' => $join->table,
00201       'num' => 1,
00202       'alias' => $alias,
00203       'join' => $join,
00204       'relationship' => $link_point,
00205     );
00206 
00207     $this->relationships[$alias] = array(
00208       'link' => $link_point,
00209       'table' => $join->table,
00210       'base' => $base,
00211     );
00212     return $alias;
00213   }
00214 
00245   function add_table($table, $relationship = NULL, $join = NULL, $alias = NULL) {
00246     if (!$this->ensure_path($table, $relationship, $join)) {
00247       return FALSE;
00248     }
00249 
00250     if (!$join) {
00251       $join = $this->get_join_data($table, $this->relationships[$relationship]['base']);
00252     }
00253 
00254     $join = $this->adjust_join($join, $relationship);
00255 
00256     return $this->queue_table($table, $relationship, $join, $alias);
00257   }
00258 
00286   function queue_table($table, $relationship = NULL, $join = NULL, $alias = NULL) {
00287     // If the alias is set, make sure it doesn't already exist.
00288     if (isset($this->table_queue[$alias])) {
00289       return $alias;
00290     }
00291 
00292     if (empty($relationship)) {
00293       $relationship = $this->base_table;
00294     }
00295 
00296     if (!array_key_exists($relationship, $this->relationships)) {
00297       return FALSE;
00298     }
00299 
00300     if (!$alias && $join && $relationship && !empty($join->adjusted) && $table != $join->table) {
00301       if ($relationship == $this->base_table) {
00302         $alias = $table;
00303       }
00304       else {
00305         $alias = $relationship . '_' . $table;
00306       }
00307     }
00308 
00309     // Check this again to make sure we don't blow up existing aliases for already
00310     // adjusted joins.
00311     if (isset($this->table_queue[$alias])) {
00312       return $alias;
00313     }
00314 
00315     $alias = $this->mark_table($table, $relationship, $alias);
00316 
00317     // If no alias is specified, give it the default.
00318     if (!isset($alias)) {
00319       $alias = $this->tables[$relationship][$table]['alias'] . $this->tables[$relationship][$table]['count'];
00320     }
00321 
00322     // If this is a relationship based table, add a marker with
00323     // the relationship as a primary table for the alias.
00324     if ($table != $alias) {
00325       $this->mark_table($alias, $this->base_table, $alias);
00326     }
00327 
00328     // If no join is specified, pull it from the table data.
00329     if (!isset($join)) {
00330       $join = $this->get_join_data($table, $this->relationships[$relationship]['base']);
00331       if (empty($join)) {
00332         return FALSE;
00333       }
00334 
00335       $join = $this->adjust_join($join, $relationship);
00336     }
00337 
00338     $this->table_queue[$alias] = array(
00339       'table' => $table,
00340       'num' => $this->tables[$relationship][$table]['count'],
00341       'alias' => $alias,
00342       'join' => $join,
00343       'relationship' => $relationship,
00344     );
00345 
00346     return $alias;
00347   }
00348 
00349   function mark_table($table, $relationship, $alias) {
00350     // Mark that this table has been added.
00351     if (empty($this->tables[$relationship][$table])) {
00352       if (!isset($alias)) {
00353         $alias = '';
00354         if ($relationship != $this->base_table) {
00355           // double underscore will help prevent accidental name
00356           // space collisions.
00357           $alias = $relationship . '__';
00358         }
00359         $alias .= $table;
00360       }
00361       $this->tables[$relationship][$table] = array(
00362         'count' => 1,
00363         'alias' => $alias,
00364       );
00365     }
00366     else {
00367       $this->tables[$relationship][$table]['count']++;
00368     }
00369 
00370     return $alias;
00371   }
00372 
00391   function ensure_table($table, $relationship = NULL, $join = NULL) {
00392     // ensure a relationship
00393     if (empty($relationship)) {
00394       $relationship = $this->base_table;
00395     }
00396 
00397     // If the relationship is the primary table, this actually be a relationship
00398     // link back from an alias. We store all aliases along with the primary table
00399     // to detect this state, because eventually it'll hit a table we already
00400     // have and that's when we want to stop.
00401     if ($relationship == $this->base_table && !empty($this->tables[$relationship][$table])) {
00402       return $this->tables[$relationship][$table]['alias'];
00403     }
00404 
00405     if (!array_key_exists($relationship, $this->relationships)) {
00406       return FALSE;
00407     }
00408 
00409     if ($table == $this->relationships[$relationship]['base']) {
00410       return $relationship;
00411     }
00412 
00413     // If we do not have join info, fetch it.
00414     if (!isset($join)) {
00415       $join = $this->get_join_data($table, $this->relationships[$relationship]['base']);
00416     }
00417 
00418     // If it can't be fetched, this won't work.
00419     if (empty($join)) {
00420       return;
00421     }
00422 
00423     // Adjust this join for the relationship, which will ensure that the 'base'
00424     // table it links to is correct. Tables adjoined to a relationship
00425     // join to a link point, not the base table.
00426     $join = $this->adjust_join($join, $relationship);
00427 
00428     if ($this->ensure_path($table, $relationship, $join)) {
00429       // Attempt to eliminate redundant joins.  If this table's
00430       // relationship and join exactly matches an existing table's
00431       // relationship and join, we do not have to join to it again;
00432       // just return the existing table's alias.  See
00433       // http://groups.drupal.org/node/11288 for details.
00434       //
00435       // This can be done safely here but not lower down in
00436       // queue_table(), because queue_table() is also used by
00437       // add_table() which requires the ability to intentionally add
00438       // the same table with the same join multiple times.  For
00439       // example, a view that filters on 3 taxonomy terms using AND
00440       // needs to join term_data 3 times with the same join.
00441 
00442       // scan through the table queue to see if a matching join and
00443       // relationship exists.  If so, use it instead of this join.
00444 
00445       // TODO: Scanning through $this->table_queue results in an
00446       // O(N^2) algorithm, and this code runs every time the view is
00447       // instantiated (Views 2 does not currently cache queries).
00448       // There are a couple possible "improvements" but we should do
00449       // some performance testing before picking one.
00450       foreach ($this->table_queue as $queued_table) {
00451         // In PHP 4 and 5, the == operation returns TRUE for two objects
00452         // if they are instances of the same class and have the same
00453         // attributes and values.
00454         if ($queued_table['relationship'] == $relationship && $queued_table['join'] == $join) {
00455           return $queued_table['alias'];
00456         }
00457       }
00458 
00459       return $this->queue_table($table, $relationship, $join);
00460     }
00461   }
00462 
00470   function ensure_path($table, $relationship = NULL, $join = NULL, $traced = array(), $add = array()) {
00471     if (!isset($relationship)) {
00472       $relationship = $this->base_table;
00473     }
00474 
00475     if (!array_key_exists($relationship, $this->relationships)) {
00476       return FALSE;
00477     }
00478 
00479     // If we do not have join info, fetch it.
00480     if (!isset($join)) {
00481       $join = $this->get_join_data($table, $this->relationships[$relationship]['base']);
00482     }
00483 
00484     // If it can't be fetched, this won't work.
00485     if (empty($join)) {
00486       return FALSE;
00487     }
00488 
00489     // Does a table along this path exist?
00490     if (isset($this->tables[$relationship][$table]) ||
00491       ($join && $join->left_table == $relationship) ||
00492       ($join && $join->left_table == $this->relationships[$relationship]['table'])) {
00493 
00494       // Make sure that we're linking to the correct table for our relationship.
00495       foreach (array_reverse($add) as $table => $path_join) {
00496         $this->queue_table($table, $relationship, $this->adjust_join($path_join, $relationship));
00497       }
00498       return TRUE;
00499     }
00500 
00501     // Have we been this way?
00502     if (isset($traced[$join->left_table])) {
00503       // we looped. Broked.
00504       return FALSE;
00505     }
00506 
00507     // Do we have to add this table?
00508     $left_join = $this->get_join_data($join->left_table, $this->relationships[$relationship]['base']);
00509     if (!isset($this->tables[$relationship][$join->left_table])) {
00510       $add[$join->left_table] = $left_join;
00511     }
00512 
00513     // Keep looking.
00514     $traced[$join->left_table] = TRUE;
00515     return $this->ensure_path($join->left_table, $relationship, $left_join, $traced, $add);
00516   }
00517 
00522   function adjust_join($join, $relationship) {
00523     if (!empty($join->adjusted)) {
00524       return $join;
00525     }
00526 
00527     if (empty($relationship) || empty($this->relationships[$relationship])) {
00528       return $join;
00529     }
00530 
00531     // Adjusts the left table for our relationship.
00532     if ($relationship != $this->base_table) {
00533       // If we're linking to the primary table, the relationship to use will
00534       // be the prior relationship. Unless it's a direct link.
00535 
00536       // Safety! Don't modify an original here.
00537       $join = drupal_clone($join);
00538 
00539       // Do we need to try to ensure a path?
00540       if ($join->left_table != $this->relationships[$relationship]['table'] &&
00541           $join->left_table != $this->relationships[$relationship]['base'] &&
00542           !isset($this->tables[$relationship][$join->left_table]['alias'])) {
00543         $this->ensure_table($join->left_table, $relationship);
00544       }
00545 
00546       // First, if this is our link point/anchor table, just use the relationship
00547       if ($join->left_table == $this->relationships[$relationship]['table']) {
00548         $join->left_table = $relationship;
00549       }
00550       // then, try the base alias.
00551       else if (isset($this->tables[$relationship][$join->left_table]['alias'])) {
00552         $join->left_table = $this->tables[$relationship][$join->left_table]['alias'];
00553       }
00554       // But if we're already looking at an alias, use that instead.
00555       else if (isset($this->table_queue[$relationship]['alias'])) {
00556         $join->left_table = $this->table_queue[$relationship]['alias'];
00557       }
00558     }
00559 
00560     $join->adjusted = TRUE;
00561     return $join;
00562   }
00563 
00575   function get_join_data($table, $base_table) {
00576     // Check to see if we're linking to a known alias. If so, get the real
00577     // table's data instead.
00578     if (!empty($this->table_queue[$table])) {
00579       $table = $this->table_queue[$table]['table'];
00580     }
00581     return views_get_table_join($table, $base_table);
00582 
00583   }
00584 
00591   function get_table_info($table) {
00592     if (!empty($this->table_queue[$table])) {
00593       return $this->table_queue[$table];
00594     }
00595 
00596     // In rare cases we might *only* have aliased versions of the table.
00597     if (!empty($this->tables[$this->base_table][$table])) {
00598       $alias = $this->tables[$this->base_table][$table]['alias'];
00599       if (!empty($this->table_queue[$alias])) {
00600         return $this->table_queue[$alias];
00601       }
00602     }
00603   }
00604 
00624   function add_field($table, $field, $alias = '', $params = NULL) {
00625     // We check for this specifically because it gets a special alias.
00626     if ($table == $this->base_table && $field == $this->base_field && empty($alias)) {
00627       $alias = $this->base_field;
00628     }
00629 
00630     if ($table && empty($this->table_queue[$table])) {
00631       $this->ensure_table($table);
00632     }
00633 
00634     if (!$alias && $table) {
00635       $alias = $table . '_' . $field;
00636     }
00637 
00638     $name = $alias ? $alias : $field;
00639 
00640     // @todo FIXME -- $alias, then $name is inconsistent
00641     if (empty($this->fields[$alias])) {
00642       $this->fields[$name] = array(
00643         'field' => $field,
00644         'table' => $table,
00645         'alias' => $alias,
00646       );
00647     }
00648 
00649     foreach ((array)$params as $key => $value) {
00650       $this->fields[$name][$key] = $value;
00651     }
00652 
00653     return $name;
00654   }
00655 
00660   function clear_fields() {
00661     $this->fields = array();
00662   }
00663 
00678   function set_where_group($type = 'AND', $group = NULL, $where = 'where') {
00679     // Set an alias.
00680     $groups = &$this->$where;
00681 
00682     if (!isset($group)) {
00683       $group = empty($groups) ? 1 : max(array_keys($groups)) + 1;
00684     }
00685 
00686     // Create an empty group
00687     if (empty($groups[$group])) {
00688       $groups[$group] = array('clauses' => array(), 'args' => array());
00689     }
00690 
00691     $groups[$group]['type'] = strtoupper($type);
00692     return $group;
00693   }
00694 
00701   function set_group_operator($type = 'AND') {
00702     $this->group_operator = strtoupper($type);
00703   }
00704 
00723   function add_where($group, $clause) {
00724     $args = func_get_args();
00725     array_shift($args); // ditch $group
00726     array_shift($args); // ditch $clause
00727 
00728     // Expand an array of args if it came in.
00729     if (count($args) == 1 && is_array(reset($args))) {
00730       $args = current($args);
00731     }
00732 
00733     // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all
00734     // the default group.
00735     if (empty($group)) {
00736       $group = 0;
00737     }
00738 
00739     // Check for a group.
00740     if (!isset($this->where[$group])) {
00741       $this->set_where_group('AND', $group);
00742     }
00743 
00744     // Add the clause and the args.
00745     if (is_array($args)) {
00746       $this->where[$group]['clauses'][] = $clause;
00747       // we use array_values() here to prevent array_merge errors as keys from multiple
00748       // sources occasionally collide.
00749       $this->where[$group]['args'] = array_merge($this->where[$group]['args'], array_values($args));
00750     }
00751   }
00752 
00771   function add_having($group, $clause) {
00772     $args = func_get_args();
00773     array_shift($args); // ditch $group
00774     array_shift($args); // ditch $clause
00775 
00776     // Expand an array of args if it came in.
00777     if (count($args) == 1 && is_array(reset($args))) {
00778       $args = current($args);
00779     }
00780 
00781     // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all
00782     // the default group.
00783     if (empty($group)) {
00784       $group = 0;
00785     }
00786 
00787     // Check for a group.
00788     if (!isset($this->having[$group])) {
00789       $this->set_where_group('AND', $group, 'having');
00790     }
00791 
00792     // Add the clause and the args.
00793     if (is_array($args)) {
00794       $this->having[$group]['clauses'][] = $clause;
00795       $this->having[$group]['args'] = array_merge($this->having[$group]['args'], array_values($args));
00796     }
00797   }
00798 
00815   function add_orderby($table, $field, $order, $alias = '') {
00816     if ($table) {
00817       $this->ensure_table($table);
00818     }
00819 
00820     // Only fill out this aliasing if there is a table;
00821     // otherwise we assume it is a formula.
00822     if (!$alias && $table) {
00823       $as = $table . '_' . $field;
00824     }
00825     else {
00826       $as = $alias;
00827     }
00828 
00829     if ($field) {
00830       $this->add_field($table, $field, $as);
00831     }
00832 
00833     $this->orderby[] = "$as " . strtoupper($order);
00834 
00835     // If grouping, all items in the order by must also be in the
00836     // group by clause. Check $table to ensure that this is not a
00837     // formula.
00838     if ($this->groupby && $table) {
00839       $this->add_groupby($as);
00840     }
00841   }
00842 
00848   function add_groupby($clause) {
00849     // Only add it if it's not already in there.
00850     if (!in_array($clause, $this->groupby)) {
00851       $this->groupby[] = $clause;
00852     }
00853   }
00854 
00861   function condition_sql($where = 'where') {
00862     $clauses = array();
00863     foreach ($this->$where as $group => $info) {
00864       $clause = implode(") " . $info['type'] . " (", $info['clauses']);
00865       if (count($info['clauses']) > 1) {
00866         $clause = '(' . $clause . ')';
00867       }
00868       $clauses[] = $clause;
00869     }
00870 
00871     if ($clauses) {
00872       $keyword = drupal_strtoupper($where);
00873       if (count($clauses) > 1) {
00874         return "$keyword (" . implode(")\n    " . $this->group_operator . ' (', $clauses) . ")\n";
00875       }
00876       else {
00877         return "$keyword " . array_shift($clauses) . "\n";
00878       }
00879     }
00880     return "";
00881   }
00882 
00890   function query($get_count = FALSE) {
00891     // Check query distinct value.
00892     if (empty($this->no_distinct) && $this->distinct && !empty($this->fields)) {
00893       if (!empty($this->fields[$this->base_field])) {
00894         $this->fields[$this->base_field]['distinct'] = TRUE;
00895         $this->add_groupby($this->base_field);
00896       }
00897     }
00898 
00903     $fields_array = $this->fields;
00904     if ($get_count && !$this->groupby) {
00905       foreach ($fields_array as $field) {
00906         if (!empty($field['distinct'])) {
00907           $get_count_optimized = FALSE;
00908           break;
00909         }
00910       }
00911     }
00912     else {
00913       $get_count_optimized = FALSE;
00914     }
00915     if (!isset($get_count_optimized)) {
00916       $get_count_optimized = TRUE;
00917     }
00918 
00919     $joins = $fields = $where = $having = $orderby = $groupby = '';
00920     // Add all the tables to the query via joins. We assume all LEFT joins.
00921     foreach ($this->table_queue as $table) {
00922       if (is_object($table['join'])) {
00923         $joins .= $table['join']->join($table, $this) . "\n";
00924       }
00925     }
00926 
00927     $has_aggregate = FALSE;
00928     $non_aggregates = array();
00929 
00930     foreach ($fields_array as $field) {
00931       if ($fields) {
00932         $fields .= ",\n   ";
00933       }
00934       $string = '';
00935       if (!empty($field['table'])) {
00936         $string .= $field['table'] . '.';
00937       }
00938       $string .= $field['field'];
00939 
00940       // store for use with non-aggregates below
00941       $fieldname = (!empty($field['alias']) ? $field['alias'] : $string);
00942 
00943       if (!empty($field['distinct'])) {
00944         $string = "DISTINCT($string)";
00945       }
00946       if (!empty($field['count'])) {
00947         $string = "COUNT($string)";
00948         $has_aggregate = TRUE;
00949       }
00950       else if (!empty($field['aggregate'])) {
00951         $has_aggregate = TRUE;
00952       }
00953       elseif ($this->distinct && !in_array($fieldname, $this->groupby)) {
00954         $string = $GLOBALS['db_type'] == 'pgsql' ? "FIRST($string)" : $string;
00955       }
00956       else {
00957         $non_aggregates[] = $fieldname;
00958       }
00959       if ($field['alias']) {
00960         $string .= " AS $field[alias]";
00961       }
00962       $fields .= $string;
00963 
00964       if ($get_count_optimized) {
00965         // We only want the first field in this case.
00966         break;
00967       }
00968     }
00969 
00970     if ($has_aggregate || $this->groupby) {
00971       $groupby = "GROUP BY " . implode(', ', array_unique(array_merge($this->groupby, $non_aggregates))) . "\n";
00972       if ($this->having) {
00973         $having = $this->condition_sql('having');
00974       }
00975     }
00976 
00977     if (!$get_count_optimized) {
00978       // we only add the groupby if we're not counting.
00979       if ($this->orderby) {
00980         $orderby = "ORDER BY " . implode(', ', $this->orderby) . "\n";
00981       }
00982     }
00983 
00984     $where = $this->condition_sql();
00985 
00986     $query = "SELECT $fields\n FROM {" . $this->base_table . "} $this->base_table \n$joins $where $groupby $having $orderby";
00987 
00988     $replace = array('&gt;' => '>', '&lt;' => '<');
00989     $query = strtr($query, $replace);
00990 
00991     return $query;
00992   }
00993 
00997   function get_where_args() {
00998     $args = array();
00999     foreach ($this->where as $group => $where) {
01000       $args = array_merge($args, $where['args']);
01001     }
01002     foreach ($this->having as $group => $having) {
01003       $args = array_merge($args, $having['args']);
01004     }
01005     return $args;
01006   }
01007 }
01008 

Generated on Mon Nov 30 15:05:58 2009 for Views by  doxygen 1.4.7