select($select); // @todo $this->from = is_array($from) ? $from : array(str_replace(array('"','`'),'',$from) => $from); $this->where($where); $this->orderby($orderby); $this->groupby($groupby); $this->having($having); $this->limit($limit); } /** * Specify the list of columns to be selected by the query. * * * // pass fields to select as single parameter array * $query->select(array("Col1","Col2"))->from("MyTable"); * * // pass fields to select as multiple parameters * $query->select("Col1", "Col2")->from("MyTable"); * * * @param mixed $fields * @return SQLQuery */ public function select($fields) { if (func_num_args() > 1) { $this->select = func_get_args(); } else { $this->select = is_array($fields) ? $fields : array($fields); } return $this; } /** * Add addition columns to the select clause */ public function selectMore($fields) { if (func_num_args() > 1) $fields = func_get_args(); if(is_array($fields)) { foreach($fields as $field) $this->select[] = $field; } else { $this->select[] = $fields; } } /** * Return the SQL expression for the given field * @todo This should be refactored after $this->select is changed to make that easier */ public function expressionForField($field) { foreach($this->select as $sel) { if(preg_match('/AS +"?([^"]*)"?/i', $sel, $matches)) $selField = $matches[1]; else if(preg_match('/"([^"]*)"\."([^"]*)"/', $sel, $matches)) $selField = $matches[2]; else if(preg_match('/"?([^"]*)"?/', $sel, $matches)) $selField = $matches[2]; if($selField == $field) return $sel; } } /** * Specify the target table to select from. * * * $query->from("MyTable"); // SELECT * FROM MyTable * * * @param string $table * @return SQLQuery This instance */ public function from($table) { $this->from[str_replace(array('"','`'),'',$table)] = $table; return $this; } /** * Add a LEFT JOIN criteria to the FROM clause. * * @param String $table Table name (unquoted) * @param String $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement. * Needs to be valid (quoted) SQL. * @param String $tableAlias Optional alias which makes it easier to identify and replace joins later on * @return SQLQuery This instance */ public function leftJoin($table, $onPredicate, $tableAlias=null) { if( !$tableAlias ) { $tableAlias = $table; } $this->from[$tableAlias] = array('type' => 'LEFT', 'table' => $table, 'filter' => array($onPredicate)); return $this; } /** * Add an INNER JOIN criteria to the FROM clause. * * @param String $table Table name (unquoted) * @param String $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement. * Needs to be valid (quoted) SQL. * @param String $tableAlias Optional alias which makes it easier to identify and replace joins later on * @return SQLQuery This instance */ public function innerJoin($table, $onPredicate, $tableAlias=null) { if( !$tableAlias ) { $tableAlias = $table; } $this->from[$tableAlias] = array('type' => 'INNER', 'table' => $table, 'filter' => array($onPredicate)); return $this; } /** * Add an additional filter (part of the ON clause) on a join */ public function addFilterToJoin($tableAlias, $filter) { $this->from[$tableAlias]['filter'][] = $filter; } /** * Replace the existing filter (ON clause) on a join */ public function setJoinFilter($tableAlias, $filter) { if(is_string($this->from[$tableAlias])) {Debug::message($tableAlias); Debug::dump($this->from);} $this->from[$tableAlias]['filter'] = array($filter); } /** * Returns true if we are already joining to the given table alias */ public function isJoinedTo($tableAlias) { return isset($this->from[$tableAlias]); } /** * Return a list of tables that this query is selecting from. */ public function queriedTables() { $tables = array(); foreach($this->from as $key => $tableClause) { if(is_array($tableClause)) $table = '"'.$tableClause['table'].'"'; else if(is_string($tableClause) && preg_match('/JOIN +("[^"]+") +(AS|ON) +/i', $tableClause, $matches)) $table = $matches[1]; else $table = $tableClause; // Handle string replacements if($this->replacementsOld) $table = str_replace($this->replacementsOld, $this->replacementsNew, $table); $tables[] = preg_replace('/^"|"$/','',$table); } return $tables; } /** * Pass LIMIT clause either as SQL snippet or in array format. * Internally, limit will always be stored as a map containing the keys 'start' and 'limit' * * @param string|array $limit * @return SQLQuery This instance */ public function limit($limit) { if($limit && is_numeric($limit)) { $this->limit = array( 'start' => 0, 'limit' => $limit, ); } else if($limit && is_string($limit)) { if(strpos($limit,',') !== false) list($start, $innerLimit) = explode(',', $limit, 2); else list($innerLimit, $start) = explode(' OFFSET ', strtoupper($limit), 2); $this->limit = array( 'start' => trim($start), 'limit' => trim($innerLimit), ); } else { $this->limit = $limit; } return $this; } /** * Pass ORDER BY clause either as SQL snippet or in array format. * * @todo Implement passing of multiple orderby pairs in nested array syntax, * e.g. array(array('sort'=>'A','dir'=>'asc'),array('sort'=>'B')) * * @param string|array $orderby * @return SQLQuery This instance */ public function orderby($orderby) { // if passed as an array, assume two array values with column and direction (asc|desc) if(is_array($orderby)) { if(!array_key_exists('sort', $orderby)) user_error('SQLQuery::orderby(): Wrong format for $orderby array', E_USER_ERROR); if(isset($orderby['sort']) && !empty($orderby['sort']) && isset($orderby['dir']) && !empty($orderby['dir'])) { $combinedOrderby = "\"" . Convert::raw2sql($orderby['sort']) . "\" " . Convert::raw2sql(strtoupper($orderby['dir'])); } elseif(isset($orderby['sort']) && !empty($orderby['sort'])) { $combinedOrderby = "\"" . Convert::raw2sql($orderby['sort']) . "\""; } else { $combinedOrderby = false; } } else { $combinedOrderby = $orderby; } // If sort contains a function call, let's move the sort clause into a separate selected field. // Some versions of MySQL choke if you have a group function referenced directly in the ORDER BY if($combinedOrderby && strpos($combinedOrderby,'(') !== false && strtoupper(trim($combinedOrderby)) != DB::getConn()->random()) { // Sort can be "Col1 DESC|ASC, Col2 DESC|ASC", we need to handle that $sortParts = explode(",", $combinedOrderby); // If you have select if(X,A,B),C then the array will return 'if(X','A','B)','C'. // Turn this into 'if(X,A,B)','C' by counting brackets while(list($i,$sortPart) = each($sortParts)) { while(substr_count($sortPart,'(') > substr_count($sortPart,')')) { list($i,$nextSortPart) = each($sortParts); if($i === null) break; $sortPart .= ',' . $nextSortPart; } $lumpedSortParts[] = $sortPart; } foreach($lumpedSortParts as $i => $sortPart) { $sortPart = trim($sortPart); if(substr(strtolower($sortPart),-5) == ' desc') { $this->select[] = substr($sortPart,0,-5) . " AS \"_SortColumn{$i}\""; $newSorts[] = "\"_SortColumn{$i}\" DESC"; } else if(substr(strtolower($sortPart),-4) == ' asc') { $this->select[] = substr($sortPart,0,-4) . " AS \"_SortColumn{$i}\""; $newSorts[] = "\"_SortColumn{$i}\" ASC"; } else { $this->select[] = "$sortPart AS \"_SortColumn{$i}\""; $newSorts[] = "\"_SortColumn{$i}\" ASC"; } } $combinedOrderby = implode(", ", $newSorts); } if(!empty($combinedOrderby)) $this->orderby = $combinedOrderby; return $this; } /** * Add a GROUP BY clause. * * @param string|array $groupby * @return SQLQuery */ public function groupby($groupby) { if(is_array($groupby)) { $this->groupby = array_merge($this->groupby, $groupby); } elseif(!empty($groupby)) { $this->groupby[] = $groupby; } return $this; } /** * Add a HAVING clause. * * @param string|array $having * @return SQLQuery */ public function having($having) { if(is_array($having)) { $this->having = array_merge($this->having, $having); } elseif(!empty($having)) { $this->having[] = $having; } return $this; } /** * Apply a predicate filter to the where clause. * * Accepts a variable length of arguments, which represent * different ways of formatting a predicate in a where clause: * * * // the entire predicate as a single string * $query->where("Column = 'Value'"); * * // an exact match predicate with a key value pair * $query->where("Column", "Value"); * * // a predicate with user defined operator * $query->where("Column", "!=", "Value"); * * */ public function where() { $args = func_get_args(); if (func_num_args() == 3) { $filter = "{$args[0]} {$args[1]} '{$args[2]}'"; } elseif (func_num_args() == 2) { $filter = "{$args[0]} = '{$args[1]}'"; } else { $filter = $args[0]; } if(is_array($filter)) { $this->where = array_merge($this->where,$filter); } elseif(!empty($filter)) { $this->where[] = $filter; } return $this; } /** * Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause. */ public function useDisjunction() { $this->connective = 'OR'; } /** * Use the conjunctive operator 'AND' to join filter expressions in the WHERE clause. */ public function useConjunction() { $this->connective = 'AND'; } /** * Swap the use of one table with another. * @param string $old Name of the old table. * @param string $new Name of the new table. */ function renameTable($old, $new) { $this->replaceText("`$old`", "`$new`"); $this->replaceText("\"$old\"", "\"$new\""); } /** * Swap some text in the SQL query with another. * @param string $old The old text. * @param string $new The new text. */ function replaceText($old, $new) { $this->replacementsOld[] = $old; $this->replacementsNew[] = $new; } /** * Return an SQL WHERE clause to filter a SELECT query. * * @return string */ function getFilter() { return ($this->where) ? implode(") {$this->connective} (" , $this->where) : ''; } /** * Generate the SQL statement for this query. * * @return string */ function sql() { // TODO: Don't require this internal-state manipulate-and-preserve - let sqlQueryToString() handle the new syntax $origFrom = $this->from; // Build from clauses foreach($this->from as $alias => $join) { // $join can be something like this array structure // array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1", "Status = 'approved'")) if(is_array($join)) { if(is_string($join['filter'])) $filter = $join['filter']; else if(sizeof($join['filter']) == 1) $filter = $join['filter'][0]; else $filter = "(" . implode(") AND (", $join['filter']) . ")"; $this->from[$alias] = strtoupper($join['type']) . " JOIN \"{$join['table']}\" AS \"$alias\" ON $filter"; } } $sql = DB::getConn()->sqlQueryToString($this); if($this->replacementsOld) { $sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql); } $this->from = $origFrom; // The query was most likely just created and then exectued. if($sql === 'SELECT *') { return ''; } return $sql; } /** * Return the generated SQL string for this query * * @return string */ function __toString() { try { return $this->sql(); } catch(Exception $e) { return ""; } } /** * Execute this query. * @return SS_Query */ function execute() { return DB::query($this->sql(), E_USER_ERROR); } /** * Checks whether this query is for a specific ID in a table * * @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ID=5") * * @return boolean */ function filtersOnID() { $regexp = '/^(.*\.)?("|`)?ID("|`)?\s?=/'; // Sometimes the ID filter will be the 2nd element, if there's a ClasssName filter first. if(isset($this->where[0]) && preg_match($regexp, $this->where[0])) return true; if(isset($this->where[1]) && preg_match($regexp, $this->where[1])) return true; return false; } /** * Checks whether this query is filtering on a foreign key, ie finding a has_many relationship * * @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ParentID=5") * * @return boolean */ function filtersOnFK() { return ( $this->where && preg_match('/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?=/', $this->where[0]) ); } /// VARIOUS TRANSFORMATIONS BELOW /** * Return the number of rows in this query if the limit were removed. Useful in paged data sets. * @return int */ function unlimitedRowCount( $column = null) { // we can't clear the select if we're relying on its output by a HAVING clause if(count($this->having)) { $records = $this->execute(); return $records->numRecords(); } $clone = clone $this; $clone->limit = null; $clone->orderby = null; // Choose a default column if($column == null) { if($this->groupby) { $countQuery = new SQLQuery(); $countQuery->select = array("count(*)"); $countQuery->from = array('(' . $clone->sql() . ') all_distinct'); return $countQuery->execute()->value(); } else { $clone->select = array("count(*)"); } } else { $clone->select = array("count($column)"); } $clone->groupby = null; return $clone->execute()->value(); } /** * Returns true if this query can be sorted by the given field. * Note that the implementation of this method is a little crude at the moment, it wil return * "false" more often that is strictly necessary. */ function canSortBy($fieldName) { $fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName); $sql = $this->sql(); $selects = $this->select; foreach($selects as $i => $sel) { if (preg_match('/"(.*)"\."(.*)"/', $sel, $matches)) $selects[$i] = $matches[2]; } $SQL_fieldName = Convert::raw2sql($fieldName); return (in_array($SQL_fieldName,$selects) || stripos($sql,"AS {$SQL_fieldName}")); } /** * Return the number of rows in this query if the limit were removed. Useful in paged data sets. * @return int * * TODO Respect HAVING and GROUPBY, which can affect the result-count */ function count( $column = null) { // Choose a default column if($column == null) { if($this->groupby) { $column = 'DISTINCT ' . implode(", ", $this->groupby); } else { $column = '*'; } } $clone = clone $this; $clone->select = array("count($column)"); $clone->limit = null; $clone->orderby = null; $clone->groupby = null; $count = $clone->execute()->value(); // If there's a limit set, then that limit is going to heavily affect the count if($this->limit) { if($count >= ($this->limit['start'] + $this->limit['limit'])) return $this->limit['limit']; else return max(0, $count - $this->limit['start']); // Otherwise, the count is going to be the output of the SQL query } else { return $count; } } /** * Return a new SQLQuery that calls the given aggregate functions on this data. * @param $columns An aggregate expression, such as 'MAX("Balance")', or an array of them. */ function aggregate($columns) { if(!is_array($columns)) $columns = array($columns); if($this->groupby || $this->limit) { throw new Exception("SQLQuery::aggregate() doesn't work with groupby or limit, yet"); } $clone = clone $this; $clone->limit = null; $clone->orderby = null; $clone->groupby = null; $clone->select = $columns; return $clone; } /** * Returns a query that returns only the first row of this query */ function firstRow() { $query = clone $this; $offset = $this->limit ? $this->limit['start'] : 0; $query->limit(array('start' => $offset, 'limit' => 1)); return $query; } /** * Returns a query that returns only the last row of this query */ function lastRow() { $query = clone $this; $offset = $this->limit ? $this->limit['start'] : 0; $query->limit(array('start' => $this->count() + $offset - 1, 'limit' => 1)); return $query; } } ?>