mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-08-29 22:16:01 +02:00
1178 lines
32 KiB
PHP
1178 lines
32 KiB
PHP
<?php
|
|
/**
|
|
* Object representing a SQL query.
|
|
* The various parts of the SQL query can be manipulated individually.
|
|
*
|
|
* Caution: Only supports SELECT (default) and DELETE at the moment.
|
|
*
|
|
* @todo Add support for INSERT and UPDATE queries
|
|
*
|
|
* @package framework
|
|
* @subpackage model
|
|
*/
|
|
class SQLQuery {
|
|
|
|
/**
|
|
* An array of SELECT fields, keyed by an optional alias.
|
|
* @var array
|
|
*/
|
|
protected $select = array();
|
|
|
|
/**
|
|
* An array of FROM clauses. The first one is just the table name.
|
|
* @var array
|
|
*/
|
|
protected $from = array();
|
|
|
|
/**
|
|
* An array of WHERE clauses.
|
|
* @var array
|
|
*/
|
|
protected $where = array();
|
|
|
|
/**
|
|
* An array of ORDER BY clauses, functions. Stores as an associative
|
|
* array of column / function to direction.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $orderby = array();
|
|
|
|
/**
|
|
* An array of GROUP BY clauses.
|
|
* @var array
|
|
*/
|
|
protected $groupby = array();
|
|
|
|
/**
|
|
* An array of having clauses.
|
|
* @var array
|
|
*/
|
|
protected $having = array();
|
|
|
|
/**
|
|
* An array containing limit and offset keys for LIMIT clause.
|
|
* @var array
|
|
*/
|
|
protected $limit = array();
|
|
|
|
/**
|
|
* If this is true DISTINCT will be added to the SQL.
|
|
* @var boolean
|
|
*/
|
|
protected $distinct = false;
|
|
|
|
/**
|
|
* If this is true, this statement will delete rather than select.
|
|
* @var boolean
|
|
*/
|
|
protected $delete = false;
|
|
|
|
/**
|
|
* The logical connective used to join WHERE clauses. Defaults to AND.
|
|
* @var string
|
|
*/
|
|
protected $connective = 'AND';
|
|
|
|
/**
|
|
* Keep an internal register of find/replace pairs to execute when it's time to actually get the
|
|
* query SQL.
|
|
* @var array
|
|
*/
|
|
protected $replacementsOld = array();
|
|
|
|
/**
|
|
* Keep an internal register of find/replace pairs to execute when it's time to actually get the
|
|
* query SQL.
|
|
* @var array
|
|
*/
|
|
protected $replacementsNew = array();
|
|
|
|
/**
|
|
* Construct a new SQLQuery.
|
|
*
|
|
* @param array $select An array of SELECT fields.
|
|
* @param array $from An array of FROM clauses. The first one should be just the table name.
|
|
* @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()) {
|
|
|
|
$this->setSelect($select);
|
|
$this->setFrom($from);
|
|
$this->setWhere($where);
|
|
$this->setOrderBy($orderby);
|
|
$this->setGroupBy($groupby);
|
|
$this->setHaving($having);
|
|
$this->setLimit($limit);
|
|
}
|
|
|
|
public function __get($field) {
|
|
if(strtolower($field) == 'select') Deprecation::notice('3.0', 'Please use getSelect() instead');
|
|
if(strtolower($field) == 'from') Deprecation::notice('3.0', 'Please use getFrom() instead');
|
|
if(strtolower($field) == 'groupby') Deprecation::notice('3.0', 'Please use getGroupBy() instead');
|
|
if(strtolower($field) == 'orderby') Deprecation::notice('3.0', 'Please use getOrderBy() instead');
|
|
if(strtolower($field) == 'having') Deprecation::notice('3.0', 'Please use getHaving() instead');
|
|
if(strtolower($field) == 'limit') Deprecation::notice('3.0', 'Please use getLimit() instead');
|
|
if(strtolower($field) == 'delete') Deprecation::notice('3.0', 'Please use getDelete() instead');
|
|
if(strtolower($field) == 'connective') Deprecation::notice('3.0', 'Please use getConnective() instead');
|
|
if(strtolower($field) == 'distinct') Deprecation::notice('3.0', 'Please use getDistinct() instead');
|
|
|
|
return $this->$field;
|
|
}
|
|
|
|
public function __set($field, $value) {
|
|
if(strtolower($field) == 'select') Deprecation::notice('3.0', 'Please use setSelect() or addSelect() instead');
|
|
if(strtolower($field) == 'from') Deprecation::notice('3.0', 'Please use setFrom() or addFrom() instead');
|
|
if(strtolower($field) == 'groupby') {
|
|
Deprecation::notice('3.0', 'Please use setGroupBy() or addGroupBy() instead');
|
|
}
|
|
if(strtolower($field) == 'orderby') {
|
|
Deprecation::notice('3.0', 'Please use setOrderBy() or addOrderBy() instead');
|
|
}
|
|
if(strtolower($field) == 'having') Deprecation::notice('3.0', 'Please use setHaving() or addHaving() instead');
|
|
if(strtolower($field) == 'limit') Deprecation::notice('3.0', 'Please use setLimit() instead');
|
|
if(strtolower($field) == 'delete') Deprecation::notice('3.0', 'Please use setDelete() instead');
|
|
if(strtolower($field) == 'connective') Deprecation::notice('3.0', 'Please use setConnective() instead');
|
|
if(strtolower($field) == 'distinct') Deprecation::notice('3.0', 'Please use setDistinct() instead');
|
|
|
|
return $this->$field = $value;
|
|
}
|
|
|
|
/**
|
|
* Set the list of columns to be selected by the query.
|
|
*
|
|
* <code>
|
|
* // 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");
|
|
* </code>
|
|
*
|
|
* @param string|array $fields
|
|
* @param boolean $clear Clear existing select fields?
|
|
* @return SQLQuery
|
|
*/
|
|
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.
|
|
*
|
|
* <code>
|
|
* // 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");
|
|
* </code>
|
|
*
|
|
* @param string|array $fields
|
|
* @param boolean $clear Clear existing select fields?
|
|
* @return SQLQuery
|
|
*/
|
|
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;
|
|
}
|
|
|
|
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 SQLQuery
|
|
*/
|
|
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 table for the SELECT clause.
|
|
*
|
|
* @example $query->setFrom("MyTable"); // SELECT * FROM MyTable
|
|
*
|
|
* @param string|array $from Escaped SQL statement, usually an unquoted table name
|
|
* @return SQLQuery
|
|
*/
|
|
public function setFrom($from) {
|
|
$this->from = array();
|
|
return $this->addFrom($from);
|
|
}
|
|
|
|
/**
|
|
* Add a table to the SELECT clause.
|
|
*
|
|
* @example $query->addFrom("MyTable"); // SELECT * FROM MyTable
|
|
*
|
|
* @param string|array $from Escaped SQL statement, usually an unquoted table name
|
|
* @return SQLQuery
|
|
*/
|
|
public function addFrom($from) {
|
|
if(is_array($from)) {
|
|
$this->from = array_merge($this->from, $from);
|
|
} elseif(!empty($from)) {
|
|
$this->from[str_replace(array('"','`'), '', $from)] = $from;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function from($from) {
|
|
Deprecation::notice('3.0', 'Please use setFrom() or addFrom() instead!');
|
|
return $this->setFrom($from);
|
|
}
|
|
|
|
/**
|
|
* Add a LEFT JOIN criteria to the FROM clause.
|
|
*
|
|
* @param string $table Unquoted table name
|
|
* @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
|
|
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
|
|
* will cause the query to appear first. The default is 20, and joins created automatically by the
|
|
* ORM have a value of 10.
|
|
* @return SQLQuery
|
|
*/
|
|
public function addLeftJoin($table, $onPredicate, $tableAlias = '', $order = 20) {
|
|
if(!$tableAlias) {
|
|
$tableAlias = $table;
|
|
}
|
|
$this->from[$tableAlias] = array(
|
|
'type' => 'LEFT',
|
|
'table' => $table,
|
|
'filter' => array($onPredicate),
|
|
'order' => $order
|
|
);
|
|
return $this;
|
|
}
|
|
|
|
public function leftjoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
|
Deprecation::notice('3.0', 'Please use addLeftJoin() instead!');
|
|
$this->addLeftJoin($table, $onPredicate, $tableAlias);
|
|
}
|
|
|
|
/**
|
|
* Add an INNER JOIN criteria to the FROM clause.
|
|
*
|
|
* @param string $table Unquoted table name
|
|
* @param string $onPredicate The "ON" SQL fragment in an "INNER 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
|
|
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
|
|
* will cause the query to appear first. The default is 20, and joins created automatically by the
|
|
* ORM have a value of 10.
|
|
* @return SQLQuery
|
|
*/
|
|
public function addInnerJoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
|
if(!$tableAlias) $tableAlias = $table;
|
|
$this->from[$tableAlias] = array(
|
|
'type' => 'INNER',
|
|
'table' => $table,
|
|
'filter' => array($onPredicate),
|
|
'order' => $order
|
|
);
|
|
return $this;
|
|
}
|
|
|
|
public function innerjoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
|
Deprecation::notice('3.0', 'Please use addInnerJoin() instead!');
|
|
return $this->addInnerJoin($table, $onPredicate, $tableAlias, $order);
|
|
}
|
|
|
|
/**
|
|
* Add an additional filter (part of the ON clause) on a join.
|
|
*
|
|
* @param string $table Table to join on from the original join
|
|
* @param string $filter The "ON" SQL fragment (escaped)
|
|
* @return SQLQuery
|
|
*/
|
|
public function addFilterToJoin($table, $filter) {
|
|
$this->from[$table]['filter'][] = $filter;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set the filter (part of the ON clause) on a join.
|
|
*
|
|
* @param string $table Table to join on from the original join
|
|
* @param string $filter The "ON" SQL fragment (escaped)
|
|
* @return SQLQuery
|
|
*/
|
|
public function setJoinFilter($table, $filter) {
|
|
$this->from[$table]['filter'] = array($filter);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns true if we are already joining to the given table alias
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isJoinedTo($tableAlias) {
|
|
return isset($this->from[$tableAlias]);
|
|
}
|
|
|
|
/**
|
|
* Return a list of tables that this query is selecting from.
|
|
*
|
|
* @return array Unquoted table names
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Set distinct property.
|
|
* @param boolean $value
|
|
*/
|
|
public function setDistinct($value) {
|
|
$this->distinct = $value;
|
|
}
|
|
|
|
/**
|
|
* Get the distinct property.
|
|
* @return boolean
|
|
*/
|
|
public function getDistinct() {
|
|
return $this->distinct;
|
|
}
|
|
|
|
/**
|
|
* Set the delete property.
|
|
* @param boolean $value
|
|
*/
|
|
public function setDelete($value) {
|
|
$this->delete = $value;
|
|
}
|
|
|
|
/**
|
|
* Get the delete property.
|
|
* @return boolean
|
|
*/
|
|
public function getDelete() {
|
|
return $this->delete;
|
|
}
|
|
|
|
/**
|
|
* Set the connective property.
|
|
* @param boolean $value
|
|
*/
|
|
public function setConnective($value) {
|
|
$this->connective = $value;
|
|
}
|
|
|
|
/**
|
|
* Get the connective property.
|
|
* @return string
|
|
*/
|
|
public function getConnective() {
|
|
return $this->connective;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @param int $offset
|
|
*
|
|
* @throws InvalidArgumentException
|
|
*
|
|
* @return SQLQuery This instance
|
|
*/
|
|
public function setLimit($limit, $offset = 0) {
|
|
if((is_numeric($limit) && $limit < 0) || (is_numeric($offset) && $offset < 0)) {
|
|
throw new InvalidArgumentException("SQLQuery::setLimit() only takes positive values");
|
|
}
|
|
|
|
if($limit && is_numeric($limit)) {
|
|
$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;
|
|
}
|
|
|
|
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->orderby("Column");
|
|
* @example $sql->orderby("Column DESC");
|
|
* @example $sql->orderby("Column DESC, ColumnTwo ASC");
|
|
* @example $sql->orderby("Column", "DESC");
|
|
* @example $sql->orderby(array("Column" => "ASC", "ColumnTwo" => "DESC"));
|
|
*
|
|
* @param string|array $orderby Clauses to add (escaped SQL statement)
|
|
* @param string $dir Sort direction, ASC or DESC
|
|
*
|
|
* @return SQLQuery
|
|
*/
|
|
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->orderby("Column");
|
|
* @example $sql->orderby("Column DESC");
|
|
* @example $sql->orderby("Column DESC, ColumnTwo ASC");
|
|
* @example $sql->orderby("Column", "DESC");
|
|
* @example $sql->orderby(array("Column" => "ASC", "ColumnTwo" => "DESC"));
|
|
*
|
|
* @param string|array $orderby Clauses to add (escaped SQL statements)
|
|
* @param string $dir Sort direction, ASC or DESC
|
|
*
|
|
* @return SQLQuery
|
|
*/
|
|
public function addOrderBy($clauses = null, $direction = null) {
|
|
if(!$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('SQLQuery::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;
|
|
foreach($this->orderby as $clause => $dir) {
|
|
// public function calls and multi-word columns like "CASE WHEN ..."
|
|
if(strpos($clause, '(') !== false || strpos($clause, " ") !== false ) {
|
|
// remove the old orderby
|
|
unset($this->orderby[$clause]);
|
|
|
|
$clause = trim($clause);
|
|
$column = "_SortColumn{$i}";
|
|
|
|
$this->selectField($clause, $column);
|
|
$this->addOrderBy('"' . $column . '"', $dir);
|
|
$i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
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 SQLQuery
|
|
*/
|
|
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 SQLQuery
|
|
*/
|
|
public function setGroupBy($groupby) {
|
|
$this->groupby = array();
|
|
return $this->addGroupBy($groupby);
|
|
}
|
|
|
|
/**
|
|
* Add a GROUP BY clause.
|
|
*
|
|
* @param string|array $groupby Escaped SQL statement
|
|
* @return SQLQuery
|
|
*/
|
|
public function addGroupBy($groupby) {
|
|
if(is_array($groupby)) {
|
|
$this->groupby = array_merge($this->groupby, $groupby);
|
|
} elseif(!empty($groupby)) {
|
|
$this->groupby[] = $groupby;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function groupby($where) {
|
|
Deprecation::notice('3.0', 'Please use setGroupBy() or addHaving() instead!');
|
|
return $this->setGroupBy($where);
|
|
}
|
|
|
|
/**
|
|
* Set a HAVING clause.
|
|
*
|
|
* @param string|array $having
|
|
* @return SQLQuery
|
|
*/
|
|
public function setHaving($having) {
|
|
$this->having = array();
|
|
return $this->addHaving($having);
|
|
}
|
|
|
|
/**
|
|
* Add a HAVING clause
|
|
*
|
|
* @param string|array $having Escaped SQL statement
|
|
* @return SQLQuery
|
|
*/
|
|
public function addHaving($having) {
|
|
if(is_array($having)) {
|
|
$this->having = array_merge($this->having, $having);
|
|
} elseif(!empty($having)) {
|
|
$this->having[] = $having;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function having($having) {
|
|
Deprecation::notice('3.0', 'Please use setHaving() or addHaving() instead!');
|
|
return $this->setHaving($having);
|
|
}
|
|
|
|
/**
|
|
* Set a WHERE clause.
|
|
*
|
|
* There are two different ways of doing this:
|
|
*
|
|
* <code>
|
|
* // the entire predicate as a single string
|
|
* $query->where("Column = 'Value'");
|
|
*
|
|
* // multiple predicates as an array
|
|
* $query->where(array("Column = 'Value'", "Column != 'Value'"));
|
|
* </code>
|
|
*
|
|
* @param string|array $where Predicate(s) to set, as escaped SQL statements.
|
|
* @return SQLQuery
|
|
*/
|
|
public function setWhere($where) {
|
|
$this->where = array();
|
|
return $this->addWhere($where);
|
|
}
|
|
|
|
/**
|
|
* Add a WHERE predicate.
|
|
*
|
|
* There are two different ways of doing this:
|
|
*
|
|
* <code>
|
|
* // the entire predicate as a single string
|
|
* $query->where("Column = 'Value'");
|
|
*
|
|
* // multiple predicates as an array
|
|
* $query->where(array("Column = 'Value'", "Column != 'Value'"));
|
|
* </code>
|
|
*
|
|
* @param string|array $where Predicate(s) to set, as escaped SQL statements.
|
|
* @return SQLQuery
|
|
*/
|
|
public function addWhere($where) {
|
|
if(is_array($where)) {
|
|
$this->where = array_merge($this->where, $where);
|
|
} elseif(!empty($where)) {
|
|
$this->where[] = $where;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function where($where) {
|
|
Deprecation::notice('3.0', 'Please use setWhere() or addWhere() instead!');
|
|
return $this->setWhere($where);
|
|
}
|
|
|
|
public function whereAny($where) {
|
|
Deprecation::notice('3.0', 'Please use setWhereAny() or setWhereAny() instead!');
|
|
return $this->setWhereAny($where);
|
|
}
|
|
|
|
/**
|
|
* @param String|array $filters Predicate(s) to set, as escaped SQL statements.
|
|
*/
|
|
public function setWhereAny($filters) {
|
|
if(is_string($filters)) $filters = func_get_args();
|
|
$clause = implode(" OR ", $filters);
|
|
return $this->setWhere($clause);
|
|
}
|
|
|
|
/**
|
|
* @param String|array $filters Predicate(s) to set, as escaped SQL statements.
|
|
*/
|
|
public function addWhereAny($filters) {
|
|
if(is_string($filters)) $filters = func_get_args();
|
|
$clause = implode(" OR ", $filters);
|
|
return $this->addWhere($clause);
|
|
}
|
|
|
|
/**
|
|
* 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 (unquoted, escaped)
|
|
* @param string $new Name of the new table (unquoted, escaped)
|
|
*/
|
|
public 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 (escaped)
|
|
* @param string $new The new text (escaped)
|
|
*/
|
|
public function replaceText($old, $new) {
|
|
$this->replacementsOld[] = $old;
|
|
$this->replacementsNew[] = $new;
|
|
}
|
|
|
|
public function getFilter() {
|
|
Deprecation::notice('3.0', 'Please use itemized filters in getWhere() instead of getFilter()');
|
|
return DB::getConn()->sqlWhereToString($this->getWhere(), $this->getConnective());
|
|
}
|
|
|
|
/**
|
|
* Return a list of FROM clauses used internally.
|
|
* @return array
|
|
*/
|
|
public function getFrom() {
|
|
return $this->from;
|
|
}
|
|
|
|
/**
|
|
* Return a list of HAVING clauses used internally.
|
|
* @return array
|
|
*/
|
|
public function getHaving() {
|
|
return $this->having;
|
|
}
|
|
|
|
/**
|
|
* Return a list of GROUP BY clauses used internally.
|
|
* @return array
|
|
*/
|
|
public function getGroupBy() {
|
|
return $this->groupby;
|
|
}
|
|
|
|
/**
|
|
* Return a list of WHERE clauses used internally.
|
|
* @return array
|
|
*/
|
|
public function getWhere() {
|
|
return $this->where;
|
|
}
|
|
|
|
/**
|
|
* 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"'.
|
|
*/
|
|
public function getSelect() {
|
|
return $this->select;
|
|
}
|
|
|
|
/**
|
|
* Generate the SQL statement for this query.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function sql() {
|
|
// TODO: Don't require this internal-state manipulate-and-preserve - let sqlQueryToString() handle the new
|
|
// syntax
|
|
$origFrom = $this->from;
|
|
// Sort the joins
|
|
$this->from = $this->getOrderedJoins($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'", 'order' => 20))
|
|
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']) . ")";
|
|
|
|
$aliasClause = ($alias != $join['table']) ? " AS \"" . Convert::raw2sql($alias) . "\"" : "";
|
|
$this->from[$alias] = strtoupper($join['type']) . " JOIN \""
|
|
. $join['table'] . "\"$aliasClause 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(trim($sql) === 'SELECT * FROM') {
|
|
return '';
|
|
}
|
|
return $sql;
|
|
}
|
|
|
|
/**
|
|
* Return the generated SQL string for this query
|
|
*
|
|
* @return string
|
|
*/
|
|
public function __toString() {
|
|
try {
|
|
return $this->sql();
|
|
} catch(Exception $e) {
|
|
return "<sql query>";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute this query.
|
|
* @return SS_Query
|
|
*/
|
|
public 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
|
|
*/
|
|
public 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
|
|
*/
|
|
public 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
|
|
*/
|
|
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) {
|
|
$countQuery = new SQLQuery();
|
|
$countQuery->select("count(*)");
|
|
$countQuery->from = array('(' . $clone->sql() . ') all_distinct');
|
|
|
|
return $countQuery->execute()->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.
|
|
*/
|
|
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($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 $column An aggregate expression, such as 'MAX("Balance")', or a set of them (as an escaped SQL statement)
|
|
*/
|
|
public function aggregate($column) {
|
|
if($this->groupby || $this->limit) {
|
|
throw new Exception("SQLQuery::aggregate() doesn't work with groupby or limit, yet");
|
|
}
|
|
|
|
$clone = clone $this;
|
|
$clone->setLimit(array());
|
|
$clone->setOrderBy(array());
|
|
$clone->setGroupBy(array());
|
|
$clone->setSelect($column);
|
|
|
|
return $clone;
|
|
}
|
|
|
|
/**
|
|
* Returns a query that returns only the first row of this query
|
|
*/
|
|
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
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Ensure that framework "auto-generated" table JOINs are first in the finalised SQL query.
|
|
* This prevents issues where developer-initiated JOINs attempt to JOIN using relations that haven't actually
|
|
* yet been scaffolded by the framework. Demonstrated by PostGres in errors like:
|
|
*"...ERROR: missing FROM-clause..."
|
|
*
|
|
* @param $from array - in the format of $this->select
|
|
* @return array - and reorderded list of selects
|
|
*/
|
|
protected function getOrderedJoins($from) {
|
|
// shift the first FROM table out from so we only deal with the JOINs
|
|
$baseFrom = array_shift($from);
|
|
$this->mergesort($from, function($firstJoin, $secondJoin) {
|
|
if($firstJoin['order'] == $secondJoin['order']) {
|
|
return 0;
|
|
}
|
|
return ($firstJoin['order'] < $secondJoin['order']) ? -1 : 1;
|
|
});
|
|
|
|
// Put the first FROM table back into the results
|
|
array_unshift($from, $baseFrom);
|
|
return $from;
|
|
}
|
|
|
|
/**
|
|
* Since uasort don't preserve the order of an array if the comparison is equal
|
|
* we have to resort to a merge sort. It's quick and stable: O(n*log(n)).
|
|
*
|
|
* @see http://stackoverflow.com/q/4353739/139301
|
|
*
|
|
* @param array &$array - the array to sort
|
|
* @param callable $cmpFunction - the function to use for comparison
|
|
*/
|
|
protected function mergesort(&$array, $cmpFunction = 'strcmp') {
|
|
// Arrays of size < 2 require no action.
|
|
if (count($array) < 2) {
|
|
return;
|
|
}
|
|
// Split the array in half
|
|
$halfway = count($array) / 2;
|
|
$array1 = array_slice($array, 0, $halfway);
|
|
$array2 = array_slice($array, $halfway);
|
|
// Recurse to sort the two halves
|
|
$this->mergesort($array1, $cmpFunction);
|
|
$this->mergesort($array2, $cmpFunction);
|
|
// If all of $array1 is <= all of $array2, just append them.
|
|
if(call_user_func($cmpFunction, end($array1), reset($array2)) < 1) {
|
|
$array = array_merge($array1, $array2);
|
|
return;
|
|
}
|
|
// Merge the two sorted arrays into a single sorted array
|
|
$array = array();
|
|
$val1 = reset($array1);
|
|
$val2 = reset($array2);
|
|
do {
|
|
if (call_user_func($cmpFunction, $val1, $val2) < 1) {
|
|
$array[key($array1)] = $val1;
|
|
$val1 = next($array1);
|
|
} else {
|
|
$array[key($array2)] = $val2;
|
|
$val2 = next($array2);
|
|
}
|
|
} while($val1 && $val2);
|
|
|
|
// Merge the remainder
|
|
while($val1) {
|
|
$array[key($array1)] = $val1;
|
|
$val1 = next($array1);
|
|
}
|
|
while($val2) {
|
|
$array[key($array2)] = $val2;
|
|
$val2 = next($array2);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|