2007-07-19 12:40:28 +02:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* Object representing a SQL query.
|
|
|
|
* The various parts of the SQL query can be manipulated individually.
|
2008-08-09 06:39:35 +02:00
|
|
|
*
|
|
|
|
* Caution: Only supports SELECT (default) and DELETE at the moment.
|
|
|
|
*
|
|
|
|
* @todo Add support for INSERT and UPDATE queries
|
|
|
|
*
|
2012-04-12 08:02:46 +02:00
|
|
|
* @package framework
|
2008-02-25 03:10:37 +01:00
|
|
|
* @subpackage model
|
2007-07-19 12:40:28 +02:00
|
|
|
*/
|
2009-10-13 00:28:47 +02:00
|
|
|
class SQLQuery {
|
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
/**
|
2012-05-01 14:19:09 +02:00
|
|
|
* An array of fields to select, keyed by an optional alias.
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @var array
|
|
|
|
*/
|
2008-08-09 07:57:44 +02:00
|
|
|
public $select = array();
|
2007-07-19 12:40:28 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* An array of join clauses. The first one is just the table name.
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @var array
|
|
|
|
*/
|
2008-08-09 07:57:44 +02:00
|
|
|
public $from = array();
|
2007-07-19 12:40:28 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* An array of filters.
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @var array
|
|
|
|
*/
|
2008-08-09 07:57:44 +02:00
|
|
|
public $where = array();
|
2007-07-19 12:40:28 +02:00
|
|
|
|
|
|
|
/**
|
2012-04-15 10:34:10 +02:00
|
|
|
* An array of order by clauses, functions. Stores as an associative
|
|
|
|
* array of column / function to direction.
|
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @var string
|
|
|
|
*/
|
2012-04-15 10:34:10 +02:00
|
|
|
public $orderby = array();
|
2007-07-19 12:40:28 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* An array of fields to group by.
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @var array
|
|
|
|
*/
|
2008-08-09 07:57:44 +02:00
|
|
|
public $groupby = array();
|
2007-07-19 12:40:28 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* An array of having clauses.
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @var array
|
|
|
|
*/
|
2008-08-09 07:57:44 +02:00
|
|
|
public $having = array();
|
2007-07-19 12:40:28 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A limit clause.
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
public $limit;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If this is true DISTINCT will be added to the SQL.
|
|
|
|
* @var boolean
|
|
|
|
*/
|
2008-08-09 07:57:44 +02:00
|
|
|
public $distinct = false;
|
2007-07-19 12:40:28 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* If this is true, this statement will delete rather than select.
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @var boolean
|
|
|
|
*/
|
2008-08-09 07:57:44 +02:00
|
|
|
public $delete = false;
|
2007-07-19 12:40:28 +02:00
|
|
|
|
2008-08-09 06:06:52 +02:00
|
|
|
/**
|
|
|
|
* The logical connective used to join WHERE clauses. Defaults to AND.
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
2008-08-09 06:06:52 +02:00
|
|
|
* @var string
|
|
|
|
*/
|
2008-08-11 04:25:44 +02:00
|
|
|
public $connective = 'AND';
|
2008-08-09 06:06:52 +02:00
|
|
|
|
2009-06-27 15:00:51 +02:00
|
|
|
/**
|
|
|
|
* Keep an internal register of find/replace pairs to execute when it's time to actually get the
|
|
|
|
* query SQL.
|
|
|
|
*/
|
|
|
|
private $replacementsOld = array(), $replacementsNew = array();
|
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
/**
|
|
|
|
* Construct a new SQLQuery.
|
2008-08-09 06:38:44 +02:00
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @param array $select An array of fields to select.
|
|
|
|
* @param array $from An array of join clauses. The first one should be just the table name.
|
|
|
|
* @param array $where An array of filters, to be inserted into the WHERE clause.
|
|
|
|
* @param string $orderby An ORDER BY clause.
|
|
|
|
* @param array $groupby An array of fields to group by.
|
|
|
|
* @param array $having An array of having clauses.
|
|
|
|
* @param string $limit A LIMIT clause.
|
2009-09-17 02:07:32 +02:00
|
|
|
*
|
|
|
|
* TODO: perhaps we can quote things here instead of requiring all the parameters to be quoted
|
|
|
|
* by this stage.
|
2007-07-19 12:40:28 +02:00
|
|
|
*/
|
2008-08-09 06:38:44 +02:00
|
|
|
function __construct($select = "*", $from = array(), $where = "", $orderby = "", $groupby = "", $having = "", $limit = "") {
|
2008-08-09 07:57:44 +02:00
|
|
|
$this->select($select);
|
|
|
|
// @todo
|
2008-11-22 04:51:04 +01:00
|
|
|
$this->from = is_array($from) ? $from : array(str_replace(array('"','`'),'',$from) => $from);
|
2008-08-09 07:57:44 +02:00
|
|
|
$this->where($where);
|
|
|
|
$this->orderby($orderby);
|
|
|
|
$this->groupby($groupby);
|
|
|
|
$this->having($having);
|
|
|
|
$this->limit($limit);
|
2007-07-19 12:40:28 +02:00
|
|
|
}
|
2008-08-09 06:38:44 +02:00
|
|
|
|
2012-05-01 06:42:14 +02:00
|
|
|
/**
|
|
|
|
* Clear the selected fields to start over
|
|
|
|
*/
|
|
|
|
function clearSelect() {
|
|
|
|
$this->select = array();
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2008-08-09 06:38:44 +02:00
|
|
|
/**
|
|
|
|
* Specify the list of columns to be selected by the query.
|
|
|
|
*
|
|
|
|
* <code>
|
|
|
|
* // 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");
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* @param mixed $fields
|
|
|
|
* @return SQLQuery
|
|
|
|
*/
|
|
|
|
public function select($fields) {
|
|
|
|
if (func_num_args() > 1) {
|
2012-05-01 07:44:31 +02:00
|
|
|
$fields = func_get_args();
|
|
|
|
} else if(!is_array($fields)) {
|
|
|
|
$fields = array($fields);
|
2008-08-09 06:38:44 +02:00
|
|
|
}
|
2012-05-01 07:44:31 +02:00
|
|
|
|
|
|
|
$this->select = array();
|
|
|
|
$this->selectMore($fields);
|
2008-08-09 07:57:44 +02:00
|
|
|
|
2008-08-09 06:38:44 +02:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2009-11-22 06:16:38 +01:00
|
|
|
/**
|
|
|
|
* Add addition columns to the select clause
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
|
|
|
* @param array|string
|
2009-11-22 06:16:38 +01:00
|
|
|
*/
|
2012-04-15 10:34:10 +02:00
|
|
|
public function selectMore($fields) {
|
2012-05-01 07:44:31 +02:00
|
|
|
if (func_num_args() > 1) {
|
|
|
|
$fields = func_get_args();
|
|
|
|
} else if(!is_array($fields)) {
|
|
|
|
$fields = array($fields);
|
|
|
|
}
|
2012-04-15 10:34:10 +02:00
|
|
|
|
2012-05-01 07:44:31 +02:00
|
|
|
$this->select = array();
|
|
|
|
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);
|
2012-04-15 10:34:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2012-05-01 06:42:14 +02:00
|
|
|
|
2012-04-15 10:34:10 +02:00
|
|
|
/**
|
2012-05-01 06:42:14 +02:00
|
|
|
* Select an additional field
|
2012-04-15 10:34:10 +02:00
|
|
|
*
|
2012-05-01 06:42:14 +02:00
|
|
|
* @param $field The field to select
|
|
|
|
* @param $alias The alias of that field
|
2012-04-15 10:34:10 +02:00
|
|
|
*/
|
2012-05-01 06:42:14 +02:00
|
|
|
public function selectField($field, $alias = null) {
|
2012-05-01 07:44:31 +02:00
|
|
|
if(!$alias) {
|
|
|
|
if(preg_match('/"([^"]+)"$/', $field, $matches)) $alias = $matches[1];
|
|
|
|
else $alias = $field;
|
2009-11-22 06:16:38 +01:00
|
|
|
}
|
2012-05-01 07:44:31 +02:00
|
|
|
$this->select[$alias] = $field;
|
2012-05-01 06:42:14 +02:00
|
|
|
}
|
2012-04-15 10:34:10 +02:00
|
|
|
|
|
|
|
/**
|
2012-05-01 06:42:14 +02:00
|
|
|
* Return the SQL expression for the given field alias.
|
|
|
|
* Returns null if the given alias doesn't exist.
|
2012-04-15 10:34:10 +02:00
|
|
|
*/
|
|
|
|
public function expressionForField($field) {
|
2012-05-01 07:44:31 +02:00
|
|
|
return isset($this->select[$field]) ? $this->select[$field] : null;
|
2012-04-15 10:34:10 +02:00
|
|
|
}
|
2009-11-22 06:16:38 +01:00
|
|
|
|
2008-08-09 06:38:44 +02:00
|
|
|
/**
|
|
|
|
* Specify the target table to select from.
|
|
|
|
*
|
|
|
|
* <code>
|
|
|
|
* $query->from("MyTable"); // SELECT * FROM MyTable
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* @param string $table
|
2008-08-09 07:57:44 +02:00
|
|
|
* @return SQLQuery This instance
|
2008-08-09 06:38:44 +02:00
|
|
|
*/
|
|
|
|
public function from($table) {
|
2008-11-22 04:51:04 +01:00
|
|
|
$this->from[str_replace(array('"','`'),'',$table)] = $table;
|
2008-08-09 07:57:44 +02:00
|
|
|
|
2008-08-09 06:38:44 +02:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2008-08-09 06:53:34 +02:00
|
|
|
/**
|
2008-08-09 07:57:44 +02:00
|
|
|
* Add a LEFT JOIN criteria to the FROM clause.
|
|
|
|
*
|
2010-10-19 00:58:43 +02:00
|
|
|
* @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
|
2008-08-09 07:57:44 +02:00
|
|
|
* @return SQLQuery This instance
|
2008-08-09 06:53:34 +02:00
|
|
|
*/
|
2010-10-19 00:58:43 +02:00
|
|
|
public function leftJoin($table, $onPredicate, $tableAlias=null) {
|
|
|
|
if( !$tableAlias ) {
|
|
|
|
$tableAlias = $table;
|
|
|
|
}
|
2009-11-22 06:16:38 +01:00
|
|
|
$this->from[$tableAlias] = array('type' => 'LEFT', 'table' => $table, 'filter' => array($onPredicate));
|
2008-08-09 08:53:26 +02:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an INNER JOIN criteria to the FROM clause.
|
|
|
|
*
|
2010-10-19 00:58:43 +02:00
|
|
|
* @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
|
2008-08-09 08:53:26 +02:00
|
|
|
* @return SQLQuery This instance
|
|
|
|
*/
|
2010-10-19 00:58:43 +02:00
|
|
|
public function innerJoin($table, $onPredicate, $tableAlias=null) {
|
|
|
|
if( !$tableAlias ) {
|
|
|
|
$tableAlias = $table;
|
|
|
|
}
|
2009-11-22 06:16:38 +01:00
|
|
|
$this->from[$tableAlias] = array('type' => 'INNER', 'table' => $table, 'filter' => array($onPredicate));
|
2008-08-09 07:57:44 +02:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2009-11-22 06:16:38 +01:00
|
|
|
/**
|
|
|
|
* Add an additional filter (part of the ON clause) on a join
|
|
|
|
*/
|
|
|
|
public function addFilterToJoin($tableAlias, $filter) {
|
2012-04-15 10:34:10 +02:00
|
|
|
$this->from[$tableAlias]['filter'][] = $filter;
|
|
|
|
}
|
2009-11-22 06:16:38 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Replace the existing filter (ON clause) on a join
|
|
|
|
*/
|
|
|
|
public function setJoinFilter($tableAlias, $filter) {
|
2012-04-15 10:34:10 +02:00
|
|
|
if(is_string($this->from[$tableAlias])) {Debug::message($tableAlias); Debug::dump($this->from);}
|
|
|
|
$this->from[$tableAlias]['filter'] = array($filter);
|
|
|
|
}
|
2009-11-22 06:16:38 +01:00
|
|
|
|
2008-08-11 01:29:30 +02:00
|
|
|
/**
|
|
|
|
* Returns true if we are already joining to the given table alias
|
|
|
|
*/
|
|
|
|
public function isJoinedTo($tableAlias) {
|
|
|
|
return isset($this->from[$tableAlias]);
|
|
|
|
}
|
|
|
|
|
2009-11-22 06:16:38 +01:00
|
|
|
/**
|
|
|
|
* Return a list of tables that this query is selecting from.
|
|
|
|
*/
|
|
|
|
public function queriedTables() {
|
|
|
|
$tables = array();
|
2012-04-15 10:34:10 +02:00
|
|
|
|
2009-11-22 06:16:38 +01:00
|
|
|
foreach($this->from as $key => $tableClause) {
|
2012-04-15 10:34:10 +02:00
|
|
|
if(is_array($tableClause)) $table = '"'.$tableClause['table'].'"';
|
2009-11-22 06:16:38 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2012-04-15 10:34:10 +02:00
|
|
|
return $tables;
|
2009-11-22 06:16:38 +01:00
|
|
|
}
|
|
|
|
|
2008-08-09 07:57:44 +02:00
|
|
|
/**
|
|
|
|
* Pass LIMIT clause either as SQL snippet or in array format.
|
2009-11-22 04:40:32 +01:00
|
|
|
* Internally, limit will always be stored as a map containing the keys 'start' and 'limit'
|
2008-08-09 07:57:44 +02:00
|
|
|
*
|
|
|
|
* @param string|array $limit
|
|
|
|
* @return SQLQuery This instance
|
|
|
|
*/
|
2012-03-09 02:02:37 +01:00
|
|
|
public function limit($limit, $offset = 0) {
|
2009-11-22 04:40:32 +01:00
|
|
|
if($limit && is_numeric($limit)) {
|
|
|
|
$this->limit = array(
|
2012-03-09 02:02:37 +01:00
|
|
|
'start' => $offset,
|
2009-11-22 04:40:32 +01:00
|
|
|
'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;
|
|
|
|
}
|
2008-08-09 07:57:44 +02:00
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pass ORDER BY clause either as SQL snippet or in array format.
|
|
|
|
*
|
2012-04-15 10:34:10 +02:00
|
|
|
* @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"));
|
|
|
|
*
|
2008-08-09 07:57:44 +02:00
|
|
|
* @param string|array $orderby
|
2012-04-15 10:34:10 +02:00
|
|
|
* @param string $dir
|
|
|
|
* @param bool $clear remove existing order by clauses
|
|
|
|
*
|
|
|
|
* @return SQLQuery
|
2008-08-09 07:57:44 +02:00
|
|
|
*/
|
2012-04-15 10:34:10 +02:00
|
|
|
public function orderby($clauses = null, $direction = null, $clear = true) {
|
|
|
|
if($clear) $this->orderby = array();
|
2008-08-09 07:57:44 +02:00
|
|
|
|
2012-04-15 10:34:10 +02:00
|
|
|
if(!$clauses) {
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(is_string($clauses)) {
|
|
|
|
if(strpos($clauses, "(") !== false) {
|
|
|
|
$sort = preg_split("/,(?![^()]*+\\))/", $clauses);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$sort = explode(",", $clauses);
|
|
|
|
}
|
2012-05-01 07:09:57 +02:00
|
|
|
|
2012-04-15 10:34:10 +02:00
|
|
|
$clauses = array();
|
|
|
|
|
|
|
|
foreach($sort as $clause) {
|
2012-05-01 07:09:57 +02:00
|
|
|
list($column, $direction) = $this->getDirectionFromString($clause, $direction);
|
|
|
|
$clauses[$column] = $direction;
|
2008-08-09 07:57:44 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-04-15 10:34:10 +02:00
|
|
|
if(is_array($clauses)) {
|
|
|
|
foreach($clauses as $key => $value) {
|
|
|
|
if(!is_numeric($key)) {
|
|
|
|
$column = trim($key);
|
2012-05-01 07:09:57 +02:00
|
|
|
$columnDir = strtoupper(trim($value));
|
2012-04-15 10:34:10 +02:00
|
|
|
}
|
|
|
|
else {
|
2012-05-01 07:09:57 +02:00
|
|
|
list($column, $columnDir) = $this->getDirectionFromString($value);
|
2008-08-09 07:57:44 +02:00
|
|
|
}
|
|
|
|
|
2012-05-01 07:09:57 +02:00
|
|
|
$this->orderby[$column] = $columnDir;
|
2012-04-15 10:34:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
user_error('SQLQuery::orderby() incorrect format for $orderby', E_USER_WARNING);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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($this->orderby) {
|
|
|
|
$i = 0;
|
|
|
|
foreach($this->orderby as $clause => $dir) {
|
2012-05-01 07:09:57 +02:00
|
|
|
// Function calls and multi-word columns like "CASE WHEN ..."
|
|
|
|
if(strpos($clause, '(') !== false || strpos($clause, " ") !== false ) {
|
2012-04-15 10:34:10 +02:00
|
|
|
// remove the old orderby
|
|
|
|
unset($this->orderby[$clause]);
|
|
|
|
|
|
|
|
$clause = trim($clause);
|
|
|
|
$column = "_SortColumn{$i}";
|
2012-05-03 02:02:21 +02:00
|
|
|
|
2012-05-01 07:44:31 +02:00
|
|
|
$this->selectField($clause, $column);
|
2012-05-03 02:02:21 +02:00
|
|
|
$this->orderby('"' . $column . '"', $dir, false);
|
2012-04-15 10:34:10 +02:00
|
|
|
$i++;
|
2008-08-09 07:57:44 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-04-15 10:34:10 +02:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2012-05-01 07:09:57 +02:00
|
|
|
/**
|
|
|
|
* Extract the direction part of a single-column order by clause.
|
|
|
|
*
|
|
|
|
* Return a 2 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);
|
|
|
|
}
|
|
|
|
|
2012-04-15 10:34:10 +02:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* @todo When $orderby is a private variable and all orderby statements
|
|
|
|
* set through
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getOrderBy() {
|
|
|
|
$orderby = $this->orderby;
|
|
|
|
|
|
|
|
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->orderby($clause, $dir, false);
|
|
|
|
}
|
2008-08-09 07:57:44 +02:00
|
|
|
|
|
|
|
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;
|
2008-08-09 06:53:34 +02:00
|
|
|
}
|
|
|
|
|
2008-08-09 06:38:44 +02:00
|
|
|
/**
|
|
|
|
* 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:
|
|
|
|
*
|
|
|
|
* <code>
|
|
|
|
* // 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");
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
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];
|
|
|
|
}
|
2008-08-09 07:57:44 +02:00
|
|
|
|
|
|
|
if(is_array($filter)) {
|
|
|
|
$this->where = array_merge($this->where,$filter);
|
|
|
|
} elseif(!empty($filter)) {
|
|
|
|
$this->where[] = $filter;
|
|
|
|
}
|
|
|
|
|
2008-08-09 06:38:44 +02:00
|
|
|
return $this;
|
|
|
|
}
|
2011-12-09 14:09:07 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
function whereAny($filters) {
|
|
|
|
if(is_string($filters)) $filters = func_get_args();
|
|
|
|
$clause = implode(" OR ", $filters);
|
|
|
|
return $this->where($clause);
|
|
|
|
}
|
|
|
|
|
2008-08-09 06:06:52 +02:00
|
|
|
/**
|
|
|
|
* 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';
|
|
|
|
}
|
2007-07-19 12:40:28 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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`");
|
2008-11-22 04:51:04 +01:00
|
|
|
$this->replaceText("\"$old\"", "\"$new\"");
|
2007-07-19 12:40:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2009-06-27 15:00:51 +02:00
|
|
|
$this->replacementsOld[] = $old;
|
|
|
|
$this->replacementsNew[] = $new;
|
2007-07-19 12:40:28 +02:00
|
|
|
}
|
|
|
|
|
2012-04-27 06:56:07 +02:00
|
|
|
public function getFilter() {
|
2012-05-01 02:04:11 +02:00
|
|
|
Deprecation::notice('3.0', 'Please use prepareWhere() instead of getFilter()');
|
|
|
|
return $this->prepareWhere();
|
2012-04-27 06:56:07 +02:00
|
|
|
}
|
2012-05-01 06:42:14 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 itemisedSelect() {
|
2012-05-01 07:44:31 +02:00
|
|
|
return $this->select;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the WHERE clauses ready for inserting into a query.
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function prepareSelect() {
|
|
|
|
$clauses = array();
|
|
|
|
foreach($this->select as $alias => $field) {
|
|
|
|
// Don't include redundant aliases.
|
|
|
|
if($alias === $field || preg_match('/"' . preg_quote($alias) . '"$/', $field)) $clauses[] = $field;
|
|
|
|
else $clauses[] = "$field AS \"$alias\"";
|
2012-05-01 06:42:14 +02:00
|
|
|
}
|
2012-05-01 07:44:31 +02:00
|
|
|
return implode(", ", $clauses);
|
2012-05-01 06:42:14 +02:00
|
|
|
}
|
2012-04-27 06:56:07 +02:00
|
|
|
|
2008-08-09 06:06:52 +02:00
|
|
|
/**
|
2012-05-01 02:04:11 +02:00
|
|
|
* Returns the WHERE clauses ready for inserting into a query.
|
2008-08-09 06:06:52 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
2012-05-01 02:04:11 +02:00
|
|
|
public function prepareWhere() {
|
2008-08-09 06:38:44 +02:00
|
|
|
return ($this->where) ? implode(") {$this->connective} (" , $this->where) : '';
|
2008-08-09 06:06:52 +02:00
|
|
|
}
|
|
|
|
|
2012-04-15 10:34:10 +02:00
|
|
|
/**
|
2012-05-01 02:04:11 +02:00
|
|
|
* Returns the ORDER BY clauses ready for inserting into a query.
|
2012-04-15 10:34:10 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function prepareOrderBy() {
|
|
|
|
$statments = array();
|
|
|
|
|
2012-05-01 02:04:11 +02:00
|
|
|
if($order = $this->getOrderBy()) {
|
2012-04-15 10:34:10 +02:00
|
|
|
foreach($order as $clause => $dir) {
|
|
|
|
$statements[] = trim($clause . ' '. $dir);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return implode(", ", $statements);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2012-05-01 02:04:11 +02:00
|
|
|
* Returns the GROUP BY clauses ready for inserting into a query.
|
2012-04-15 10:34:10 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function prepareGroupBy() {
|
|
|
|
return implode(", ", $this->groupby);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2012-05-01 02:04:11 +02:00
|
|
|
* Returns the HAVING clauses ready for inserting into a query.
|
2012-04-15 10:34:10 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function prepareHaving() {
|
|
|
|
return implode(" ) AND ( ", $sqlQuery->having);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
/**
|
|
|
|
* Generate the SQL statement for this query.
|
2008-08-09 06:38:44 +02:00
|
|
|
*
|
2007-07-19 12:40:28 +02:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
function sql() {
|
2012-04-15 10:34:10 +02:00
|
|
|
// 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']) . ")";
|
2011-09-26 23:18:23 +02:00
|
|
|
|
2011-10-29 06:06:42 +02:00
|
|
|
$aliasClause = ($alias != $join['table']) ? " AS \"$alias\"" : "";
|
2012-04-15 10:34:10 +02:00
|
|
|
$this->from[$alias] = strtoupper($join['type']) . " JOIN \"{$join['table']}\"$aliasClause ON $filter";
|
|
|
|
}
|
|
|
|
}
|
2011-09-27 02:28:19 +02:00
|
|
|
|
2009-06-27 15:00:51 +02:00
|
|
|
$sql = DB::getConn()->sqlQueryToString($this);
|
2012-04-15 10:34:10 +02:00
|
|
|
|
2011-09-27 02:28:19 +02:00
|
|
|
if($this->replacementsOld) {
|
|
|
|
$sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql);
|
|
|
|
}
|
2011-03-30 07:06:33 +02:00
|
|
|
|
2012-04-15 10:34:10 +02:00
|
|
|
$this->from = $origFrom;
|
2011-03-30 07:06:33 +02:00
|
|
|
|
2011-09-27 02:28:19 +02:00
|
|
|
// The query was most likely just created and then exectued.
|
|
|
|
if($sql === 'SELECT *') {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2009-06-27 15:00:51 +02:00
|
|
|
return $sql;
|
2007-07-19 12:40:28 +02:00
|
|
|
}
|
|
|
|
|
2008-08-09 06:38:44 +02:00
|
|
|
/**
|
|
|
|
* Return the generated SQL string for this query
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
function __toString() {
|
2011-03-21 08:01:28 +01:00
|
|
|
try {
|
|
|
|
return $this->sql();
|
|
|
|
} catch(Exception $e) {
|
|
|
|
return "<sql query>";
|
|
|
|
}
|
2008-08-09 06:38:44 +02:00
|
|
|
}
|
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
/**
|
|
|
|
* Execute this query.
|
API CHANGE: Renamed conflicting classes to have an "SS_" namespace, and renamed existing "SS" namespace to "SS_". The affected classes are: HTTPRequest, HTTPResponse, Query, Database, SSBacktrace, SSCli, SSDatetime, SSDatetimeTest, SSLog, SSLogTest, SSLogEmailWriter, SSLogErrorEmailFormatter, SSLogErrorFileFormatter, SSLogFileWriter and SSZendLog.
MINOR: Replaced usage of renamed classes with the new namespaced name.
From: Andrew Short <andrewjshort@gmail.com>
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@90075 467b73ca-7a2a-4603-9d3b-597d59a354a9
2009-10-26 04:06:31 +01:00
|
|
|
* @return SS_Query
|
2007-07-19 12:40:28 +02:00
|
|
|
*/
|
|
|
|
function execute() {
|
2009-09-17 02:24:36 +02:00
|
|
|
return DB::query($this->sql(), E_USER_ERROR);
|
2007-07-19 12:40:28 +02:00
|
|
|
}
|
|
|
|
|
2008-08-27 07:16:44 +02:00
|
|
|
/**
|
|
|
|
* Checks whether this query is for a specific ID in a table
|
2009-01-10 12:35:50 +01:00
|
|
|
*
|
|
|
|
* @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ID=5")
|
2008-08-27 07:16:44 +02:00
|
|
|
*
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
function filtersOnID() {
|
2009-08-10 06:36:17 +02:00
|
|
|
$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;
|
2009-01-10 12:35:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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])
|
2008-08-27 07:16:44 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
/// VARIOUS TRANSFORMATIONS BELOW
|
|
|
|
|
2010-10-13 06:04:32 +02:00
|
|
|
/**
|
|
|
|
* Return the number of rows in this query if the limit were removed. Useful in paged data sets.
|
|
|
|
* @return int
|
|
|
|
*/
|
2012-03-27 06:04:11 +02:00
|
|
|
function unlimitedRowCount($column = null) {
|
2010-04-13 04:05:45 +02:00
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
|
2010-10-13 06:00:14 +02:00
|
|
|
$clone = clone $this;
|
|
|
|
$clone->limit = null;
|
|
|
|
$clone->orderby = null;
|
|
|
|
|
2009-01-05 07:19:48 +01:00
|
|
|
// Choose a default column
|
|
|
|
if($column == null) {
|
|
|
|
if($this->groupby) {
|
2010-10-13 06:00:14 +02:00
|
|
|
$countQuery = new SQLQuery();
|
2012-05-01 07:44:31 +02:00
|
|
|
$countQuery->select("count(*)");
|
2010-10-19 02:51:53 +02:00
|
|
|
$countQuery->from = array('(' . $clone->sql() . ') all_distinct');
|
2010-10-13 06:00:14 +02:00
|
|
|
|
|
|
|
return $countQuery->execute()->value();
|
|
|
|
|
2009-01-05 07:19:48 +01:00
|
|
|
} else {
|
2010-10-13 06:00:14 +02:00
|
|
|
$clone->select = array("count(*)");
|
2009-01-05 07:19:48 +01:00
|
|
|
}
|
2010-10-13 06:00:14 +02:00
|
|
|
} else {
|
|
|
|
$clone->select = array("count($column)");
|
2009-01-05 07:19:48 +01:00
|
|
|
}
|
2010-10-13 06:00:14 +02:00
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
$clone->groupby = null;
|
|
|
|
return $clone->execute()->value();
|
|
|
|
}
|
2010-04-13 04:06:12 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if this query can be sorted by the given field.
|
|
|
|
*/
|
|
|
|
function canSortBy($fieldName) {
|
2010-04-13 04:16:50 +02:00
|
|
|
$fieldName = preg_replace('/(\s+?)(A|DE)SC$/', '', $fieldName);
|
|
|
|
|
2012-05-01 07:44:31 +02:00
|
|
|
return isset($this->select[$fieldName]);
|
2010-04-13 04:06:12 +02:00
|
|
|
}
|
|
|
|
|
2009-11-22 04:42:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
}
|
2009-11-22 06:16:38 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a new SQLQuery that calls the given aggregate functions on this data.
|
2012-05-01 07:44:31 +02:00
|
|
|
* @param $column An aggregate expression, such as 'MAX("Balance")', or a set of them.
|
2009-11-22 06:16:38 +01:00
|
|
|
*/
|
2012-05-01 07:44:31 +02:00
|
|
|
function aggregate($column) {
|
2009-11-22 06:16:38 +01:00
|
|
|
if($this->groupby || $this->limit) {
|
|
|
|
throw new Exception("SQLQuery::aggregate() doesn't work with groupby or limit, yet");
|
|
|
|
}
|
2012-05-01 07:44:31 +02:00
|
|
|
|
|
|
|
$clone = clone $this;
|
2009-11-22 06:16:38 +01:00
|
|
|
$clone->limit = null;
|
|
|
|
$clone->orderby = null;
|
|
|
|
$clone->groupby = null;
|
2012-05-01 07:44:31 +02:00
|
|
|
$clone->select($column);
|
2009-11-22 06:16:38 +01:00
|
|
|
|
2012-05-01 07:44:31 +02:00
|
|
|
return $clone;
|
2009-11-22 06:16:38 +01:00
|
|
|
}
|
2009-11-22 04:42:29 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a query that returns only the first row of this query
|
|
|
|
*/
|
|
|
|
function firstRow() {
|
|
|
|
$query = clone $this;
|
|
|
|
$offset = $this->limit ? $this->limit['start'] : 0;
|
2012-03-09 02:02:37 +01:00
|
|
|
$query->limit(1, $offset);
|
2009-11-22 04:42:29 +01:00
|
|
|
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;
|
2012-03-09 02:02:37 +01:00
|
|
|
$query->limit(1, $this->count() + $offset - 1);
|
2009-11-22 04:42:29 +01:00
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
}
|
|
|
|
|