array($parameters)) * * @var array */ protected $having = array(); /** * If this is true DISTINCT will be added to the SQL. * * @var bool */ protected $distinct = false; /** * An array of ORDER BY clauses, functions. Stores as an associative * array of column / function to direction. * * May be used on SELECT or single table DELETE queries in some adapters * * @var string */ protected $orderby = array(); /** * An array containing limit and offset keys for LIMIT clause. * * May be used on SELECT or single table DELETE queries in some adapters * * @var array */ protected $limit = array(); /** * Construct a new SQLSelect. * * @param array $select An array of SELECT fields. * @param array|string $from An array of FROM clauses. The first one should be just the table name. * Each should be ANSI quoted. * @param array $where An array of WHERE clauses. * @param array $orderby An array ORDER BY clause. * @param array $groupby An array of GROUP BY clauses. * @param array $having An array of HAVING clauses. * @param array|string $limit A LIMIT clause or array with limit and offset keys * @return static */ public static function create($select = "*", $from = array(), $where = array(), $orderby = array(), $groupby = array(), $having = array(), $limit = array()) { return Injector::inst()->createWithArgs(__CLASS__, func_get_args()); } /** * Construct a new SQLSelect. * * @param array $select An array of SELECT fields. * @param array|string $from An array of FROM clauses. The first one should be just the table name. * Each should be ANSI quoted. * @param array $where An array of WHERE clauses. * @param array $orderby An array ORDER BY clause. * @param array $groupby An array of GROUP BY clauses. * @param array $having An array of HAVING clauses. * @param array|string $limit A LIMIT clause or array with limit and offset keys */ public function __construct($select = "*", $from = array(), $where = array(), $orderby = array(), $groupby = array(), $having = array(), $limit = array()) { parent::__construct($from, $where); $this->setSelect($select); $this->setOrderBy($orderby); $this->setGroupBy($groupby); $this->setHaving($having); $this->setLimit($limit); } /** * Set the list of columns to be selected by the query. * * * // pass fields to select as single parameter array * $query->setSelect(array("Col1","Col2"))->setFrom("MyTable"); * * // pass fields to select as multiple parameters * $query->setSelect("Col1", "Col2")->setFrom("MyTable"); * * * @param string|array $fields * @param bool $clear Clear existing select fields? * @return self Self reference */ public function setSelect($fields) { $this->select = array(); if (func_num_args() > 1) { $fields = func_get_args(); } else if(!is_array($fields)) { $fields = array($fields); } return $this->addSelect($fields); } /** * Add to the list of columns to be selected by the query. * * * // pass fields to select as single parameter array * $query->addSelect(array("Col1","Col2"))->setFrom("MyTable"); * * // pass fields to select as multiple parameters * $query->addSelect("Col1", "Col2")->setFrom("MyTable"); * * * @param string|array $fields * @param bool $clear Clear existing select fields? * @return self Self reference */ public function addSelect($fields) { if (func_num_args() > 1) { $fields = func_get_args(); } else if(!is_array($fields)) { $fields = array($fields); } foreach($fields as $idx => $field) { if(preg_match('/^(.*) +AS +"?([^"]*)"?/i', $field, $matches)) { Deprecation::notice("3.0", "Use selectField() to specify column aliases"); $this->selectField($matches[1], $matches[2]); } else { $this->selectField($field, is_numeric($idx) ? null : $idx); } } return $this; } /** * @deprecated since version 3.0 */ public function select($fields) { Deprecation::notice('3.0', 'Please use setSelect() or addSelect() instead!'); $this->setSelect($fields); } /** * Select an additional field. * * @param $field string The field to select (escaped SQL statement) * @param $alias string The alias of that field (escaped SQL statement). * Defaults to the unquoted column name of the $field parameter. * @return self Self reference */ public function selectField($field, $alias = null) { if(!$alias) { if(preg_match('/"([^"]+)"$/', $field, $matches)) $alias = $matches[1]; else $alias = $field; } $this->select[$alias] = $field; return $this; } /** * Return the SQL expression for the given field alias. * Returns null if the given alias doesn't exist. * See {@link selectField()} for details on alias generation. * * @param string $field * @return string */ public function expressionForField($field) { return isset($this->select[$field]) ? $this->select[$field] : null; } /** * Set distinct property. * * @param bool $value * @return self Self reference */ public function setDistinct($value) { $this->distinct = $value; return $this; } /** * Get the distinct property. * * @return bool */ public function getDistinct() { return $this->distinct; } /** * Get the limit property. * @return array */ public function getLimit() { return $this->limit; } /** * 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 int|string|array $limit If passed as a string or array, assumes SQL escaped data. * Only applies for positive values, or if an $offset is set as well. * @param int $offset * @throws InvalidArgumentException * @return self Self reference */ public function setLimit($limit, $offset = 0) { if((is_numeric($limit) && $limit < 0) || (is_numeric($offset) && $offset < 0)) { throw new InvalidArgumentException("SQLSelect::setLimit() only takes positive values"); } if(is_numeric($limit) && ($limit || $offset)) { $this->limit = array( 'start' => $offset, '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; } /** * @deprecated since version 3.0 */ public function limit($limit, $offset = 0) { Deprecation::notice('3.0', 'Please use setLimit() instead!'); return $this->setLimit($limit, $offset); } /** * Set ORDER BY clause either as SQL snippet or in array format. * * @example $sql->setOrderBy("Column"); * @example $sql->setOrderBy("Column DESC"); * @example $sql->setOrderBy("Column DESC, ColumnTwo ASC"); * @example $sql->setOrderBy("Column", "DESC"); * @example $sql->setOrderBy(array("Column" => "ASC", "ColumnTwo" => "DESC")); * * @param string|array $clauses Clauses to add (escaped SQL statement) * @param string $dir Sort direction, ASC or DESC * * @return self Self reference */ public function setOrderBy($clauses = null, $direction = null) { $this->orderby = array(); return $this->addOrderBy($clauses, $direction); } /** * Add ORDER BY clause either as SQL snippet or in array format. * * @example $sql->addOrderBy("Column"); * @example $sql->addOrderBy("Column DESC"); * @example $sql->addOrderBy("Column DESC, ColumnTwo ASC"); * @example $sql->addOrderBy("Column", "DESC"); * @example $sql->addOrderBy(array("Column" => "ASC", "ColumnTwo" => "DESC")); * * @param string|array $clauses Clauses to add (escaped SQL statements) * @param string $direction Sort direction, ASC or DESC * @return self Self reference */ public function addOrderBy($clauses = null, $direction = null) { if(empty($clauses)) return $this; if(is_string($clauses)) { if(strpos($clauses, "(") !== false) { $sort = preg_split("/,(?![^()]*+\\))/", $clauses); } else { $sort = explode(",", $clauses); } $clauses = array(); foreach($sort as $clause) { list($column, $direction) = $this->getDirectionFromString($clause, $direction); $clauses[$column] = $direction; } } if(is_array($clauses)) { foreach($clauses as $key => $value) { if(!is_numeric($key)) { $column = trim($key); $columnDir = strtoupper(trim($value)); } else { list($column, $columnDir) = $this->getDirectionFromString($value); } $this->orderby[$column] = $columnDir; } } else { user_error('SQLSelect::orderby() incorrect format for $orderby', E_USER_WARNING); } // If sort contains a public function call, let's move the sort clause into a // separate selected field. // // Some versions of MySQL choke if you have a group public function referenced // directly in the ORDER BY if($this->orderby) { $i = 0; $orderby = array(); foreach($this->orderby as $clause => $dir) { // public function calls and multi-word columns like "CASE WHEN ..." if(strpos($clause, '(') !== false || strpos($clause, " ") !== false ) { // Move the clause to the select fragment, substituting a placeholder column in the sort fragment. $clause = trim($clause); $column = "_SortColumn{$i}"; $this->selectField($clause, $column); $clause = '"' . $column . '"'; $i++; } $orderby[$clause] = $dir; } $this->orderby = $orderby; } return $this; } /** * @deprecated since version 3.0 */ public function orderby($clauses = null, $direction = null) { Deprecation::notice('3.0', 'Please use setOrderBy() instead!'); return $this->setOrderBy($clauses, $direction); } /** * Extract the direction part of a single-column order by clause. * * @param String * @param String * @return Array A two element array: array($column, $direction) */ private function getDirectionFromString($value, $defaultDirection = null) { if(preg_match('/^(.*)(asc|desc)$/i', $value, $matches)) { $column = trim($matches[1]); $direction = strtoupper($matches[2]); } else { $column = $value; $direction = $defaultDirection ? $defaultDirection : "ASC"; } return array($column, $direction); } /** * Returns the current order by as array if not already. To handle legacy * statements which are stored as strings. Without clauses and directions, * convert the orderby clause to something readable. * * @return array */ public function getOrderBy() { $orderby = $this->orderby; if(!$orderby) $orderby = array(); if(!is_array($orderby)) { // spilt by any commas not within brackets $orderby = preg_split('/,(?![^()]*+\\))/', $orderby); } foreach($orderby as $k => $v) { if(strpos($v, ' ') !== false) { unset($orderby[$k]); $rule = explode(' ', trim($v)); $clause = $rule[0]; $dir = (isset($rule[1])) ? $rule[1] : 'ASC'; $orderby[$clause] = $dir; } } return $orderby; } /** * Reverses the order by clause by replacing ASC or DESC references in the * current order by with it's corollary. * * @return self Self reference */ public function reverseOrderBy() { $order = $this->getOrderBy(); $this->orderby = array(); foreach($order as $clause => $dir) { $dir = (strtoupper($dir) == 'DESC') ? 'ASC' : 'DESC'; $this->addOrderBy($clause, $dir); } return $this; } /** * Set a GROUP BY clause. * * @param string|array $groupby Escaped SQL statement * @return self Self reference */ public function setGroupBy($groupby) { $this->groupby = array(); return $this->addGroupBy($groupby); } /** * Add a GROUP BY clause. * * @param string|array $groupby Escaped SQL statement * @return self Self reference */ public function addGroupBy($groupby) { if(is_array($groupby)) { $this->groupby = array_merge($this->groupby, $groupby); } elseif(!empty($groupby)) { $this->groupby[] = $groupby; } return $this; } /** * @deprecated since version 3.0 */ public function groupby($where) { Deprecation::notice('3.0', 'Please use setGroupBy() or addHaving() instead!'); return $this->setGroupBy($where); } /** * Set a HAVING clause. * * @see SQLSelect::addWhere() for syntax examples * * @param mixed $having Predicate(s) to set, as escaped SQL statements or paramaterised queries * @param mixed $having,... Unlimited additional predicates * @return self Self reference */ public function setHaving($having) { $having = func_num_args() > 1 ? func_get_args() : $having; $this->having = array(); return $this->addHaving($having); } /** * Add a HAVING clause * * @see SQLSelect::addWhere() for syntax examples * * @param mixed $having Predicate(s) to set, as escaped SQL statements or paramaterised queries * @param mixed $having,... Unlimited additional predicates * @return self Self reference */ public function addHaving($having) { $having = $this->normalisePredicates(func_get_args()); // If the function is called with an array of items $this->having = array_merge($this->having, $having); return $this; } /** * @deprecated since version 3.0 */ public function having($having) { Deprecation::notice('3.0', 'Please use setHaving() or addHaving() instead!'); return $this->setHaving($having); } /** * Return a list of HAVING clauses used internally. * @return array */ public function getHaving() { return $this->having; } /** * Return a list of HAVING clauses used internally. * * @param array $parameters Out variable for parameters required for this query * @return array */ public function getHavingParameterised(&$parameters) { $this->splitQueryParameters($this->having, $conditions, $parameters); return $conditions; } /** * Return a list of GROUP BY clauses used internally. * * @return array */ public function getGroupBy() { return $this->groupby; } /** * Return an itemised select list as a map, where keys are the aliases, and values are the column sources. * Aliases will always be provided (if the alias is implicit, the alias value will be inferred), and won't be * quoted. * E.g., 'Title' => '"SiteTree"."Title"'. * * @return array */ public function getSelect() { return $this->select; } /// VARIOUS TRANSFORMATIONS BELOW /** * Return the number of rows in this query if the limit were removed. Useful in paged data sets. * * @param string $column * @return int */ public 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) { // @todo Test case required here $countQuery = new SQLSelect(); $countQuery->select("count(*)"); $countQuery->addFrom(array('(' . $clone->sql($innerParameters) . ') all_distinct')); $sql = $countQuery->sql($parameters); // $parameters should be empty $result = DB::prepared_query($sql, $innerParameters); return $result->value(); } else { $clone->setSelect(array("count(*)")); } } else { $clone->setSelect(array("count($column)")); } $clone->setGroupBy(array());; return $clone->execute()->value(); } /** * Returns true if this query can be sorted by the given field. * * @param string $fieldName * @return bool */ public function canSortBy($fieldName) { $fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName); return isset($this->select[$fieldName]); } /** * Return the number of rows in this query if the limit were removed. Useful in paged data sets. * * @todo Respect HAVING and GROUPBY, which can affect the result-count * * @param string $column Quoted, escaped column name * @return int */ public 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' => "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 SQLSelect that calls the given aggregate functions on this data. * * @param string $column An aggregate expression, such as 'MAX("Balance")', or a set of them * (as an escaped SQL statement) * @param string $alias An optional alias for the aggregate column. * @return SQLSelect A clone of this object with the given aggregate function */ public function aggregate($column, $alias = null) { $clone = clone $this; // don't set an ORDER BY clause if no limit has been set. It doesn't make // sense to add an ORDER BY if there is no limit, and it will break // queries to databases like MSSQL if you do so. Note that the reason // this came up is because DataQuery::initialiseQuery() introduces // a default sort. if($this->limit) { $clone->setLimit($this->limit); $clone->setOrderBy($this->orderby); } else { $clone->setOrderBy(array()); } $clone->setGroupBy($this->groupby); if($alias) { $clone->setSelect(array()); $clone->selectField($column, $alias); } else { $clone->setSelect($column); } return $clone; } /** * Returns a query that returns only the first row of this query * * @return SQLSelect A clone of this object with the first row only */ public function firstRow() { $query = clone $this; $offset = $this->limit ? $this->limit['start'] : 0; $query->setLimit(1, $offset); return $query; } /** * Returns a query that returns only the last row of this query * * @return SQLSelect A clone of this object with the last row only */ public function lastRow() { $query = clone $this; $offset = $this->limit ? $this->limit['start'] : 0; // Limit index to start in case of empty results $index = max($this->count() + $offset - 1, 0); $query->setLimit(1, $index); return $query; } }