2013-06-21 00:32:08 +02:00
|
|
|
<?php
|
|
|
|
|
2016-06-15 06:03:16 +02:00
|
|
|
namespace SilverStripe\ORM\Queries;
|
|
|
|
|
|
|
|
use Deprecation;
|
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Represents a SQL query for an expression which interacts with existing rows
|
|
|
|
* (SELECT / DELETE / UPDATE) with a WHERE clause
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @package framework
|
2016-06-15 06:03:16 +02:00
|
|
|
* @subpackage orm
|
2013-06-21 00:32:08 +02:00
|
|
|
*/
|
|
|
|
abstract class SQLConditionalExpression extends SQLExpression {
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* An array of WHERE clauses.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* Each item in this array will be in the form of a single-length array
|
|
|
|
* in the format array('predicate' => array($parameters))
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $where = array();
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* The logical connective used to join WHERE clauses. Defaults to AND.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $connective = 'AND';
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* An array of tables. The first one is just the table name.
|
|
|
|
* Used as the FROM in DELETE/SELECT statements, the INTO in INSERT statements,
|
|
|
|
* and the target table in UPDATE statements
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* The keys of this array are the aliases of the tables (unquoted), where the
|
|
|
|
* values are either the literal table names, or an array with join details.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @see SQLConditionalExpression::addLeftJoin()
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @var array
|
|
|
|
*/
|
|
|
|
protected $from = array();
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Construct a new SQLInteractExpression.
|
|
|
|
*
|
|
|
|
* @param array|string $from An array of Tables (FROM clauses). The first one should be just the table name.
|
|
|
|
* @param array $where An array of WHERE clauses.
|
|
|
|
*/
|
|
|
|
function __construct($from = array(), $where = array()) {
|
|
|
|
$this->setFrom($from);
|
|
|
|
$this->setWhere($where);
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Sets the list of tables to query from or update
|
|
|
|
*
|
2014-07-14 23:31:03 +02:00
|
|
|
* @example $query->setFrom('"MyTable"'); // SELECT * FROM "MyTable"
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
2014-07-14 23:31:03 +02:00
|
|
|
* @param string|array $from Single, or list of, ANSI quoted table names
|
|
|
|
* @return self
|
2013-06-21 00:32:08 +02:00
|
|
|
*/
|
|
|
|
public function setFrom($from) {
|
|
|
|
$this->from = array();
|
|
|
|
return $this->addFrom($from);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add a table to include in the query or update
|
|
|
|
*
|
2014-07-14 23:31:03 +02:00
|
|
|
* @example $query->addFrom('"MyTable"'); // SELECT * FROM "MyTable"
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
2014-07-14 23:31:03 +02:00
|
|
|
* @param string|array $from Single, or list of, ANSI quoted table names
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return self Self reference
|
|
|
|
*/
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the connective property.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param string $value either 'AND' or 'OR'
|
|
|
|
*/
|
|
|
|
public function setConnective($value) {
|
|
|
|
$this->connective = $value;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the connective property.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return string 'AND' or 'OR'
|
|
|
|
*/
|
|
|
|
public function getConnective() {
|
|
|
|
return $this->connective;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause.
|
|
|
|
*/
|
|
|
|
public function useDisjunction() {
|
|
|
|
$this->setConnective('OR');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Use the conjunctive operator 'AND' to join filter expressions in the WHERE clause.
|
|
|
|
*/
|
|
|
|
public function useConjunction() {
|
|
|
|
$this->setConnective('AND');
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Add a LEFT JOIN criteria to the tables list.
|
|
|
|
*
|
|
|
|
* @param string $table Unquoted table name
|
2014-08-15 08:53:05 +02:00
|
|
|
* @param string $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement, Needs to be valid
|
2013-06-21 00:32:08 +02:00
|
|
|
* (quoted) SQL.
|
|
|
|
* @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
|
2014-08-15 08:53:05 +02:00
|
|
|
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
|
2013-06-21 00:32:08 +02:00
|
|
|
* will cause the query to appear first. The default is 20, and joins created automatically by the
|
|
|
|
* ORM have a value of 10.
|
2014-09-15 02:27:55 +02:00
|
|
|
* @param array $parameters Any additional parameters if the join is a parameterised subquery
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return self Self reference
|
|
|
|
*/
|
2014-09-15 02:27:55 +02:00
|
|
|
public function addLeftJoin($table, $onPredicate, $tableAlias = '', $order = 20, $parameters = array()) {
|
2013-06-21 00:32:08 +02:00
|
|
|
if(!$tableAlias) {
|
|
|
|
$tableAlias = $table;
|
|
|
|
}
|
|
|
|
$this->from[$tableAlias] = array(
|
|
|
|
'type' => 'LEFT',
|
|
|
|
'table' => $table,
|
2014-08-15 08:53:05 +02:00
|
|
|
'filter' => array($onPredicate),
|
2014-09-15 02:27:55 +02:00
|
|
|
'order' => $order,
|
|
|
|
'parameters' => $parameters
|
2013-06-21 00:32:08 +02:00
|
|
|
);
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an INNER JOIN criteria
|
|
|
|
*
|
|
|
|
* @param string $table Unquoted table name
|
2014-08-15 08:53:05 +02:00
|
|
|
* @param string $onPredicate The "ON" SQL fragment in an "INNER JOIN ... AS ... ON ..." statement. Needs to be
|
2013-06-21 00:32:08 +02:00
|
|
|
* 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.
|
2014-09-15 02:27:55 +02:00
|
|
|
* @param array $parameters Any additional parameters if the join is a parameterised subquery
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return self Self reference
|
|
|
|
*/
|
2014-09-15 02:27:55 +02:00
|
|
|
public function addInnerJoin($table, $onPredicate, $tableAlias = null, $order = 20, $parameters = array()) {
|
2013-06-21 00:32:08 +02:00
|
|
|
if(!$tableAlias) $tableAlias = $table;
|
|
|
|
$this->from[$tableAlias] = array(
|
|
|
|
'type' => 'INNER',
|
|
|
|
'table' => $table,
|
|
|
|
'filter' => array($onPredicate),
|
2014-09-15 02:27:55 +02:00
|
|
|
'order' => $order,
|
|
|
|
'parameters' => $parameters
|
2013-06-21 00:32:08 +02:00
|
|
|
);
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add an additional filter (part of the ON clause) on a join.
|
|
|
|
*
|
2014-07-14 23:31:03 +02:00
|
|
|
* @param string $table Table to join on from the original join (unquoted)
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param string $filter The "ON" SQL fragment (escaped)
|
|
|
|
* @return self Self reference
|
|
|
|
*/
|
|
|
|
public function addFilterToJoin($table, $filter) {
|
|
|
|
$this->from[$table]['filter'][] = $filter;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the filter (part of the ON clause) on a join.
|
|
|
|
*
|
2014-07-14 23:31:03 +02:00
|
|
|
* @param string $table Table to join on from the original join (unquoted)
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param string $filter The "ON" SQL fragment (escaped)
|
|
|
|
* @return self Self reference
|
|
|
|
*/
|
|
|
|
public function setJoinFilter($table, $filter) {
|
|
|
|
$this->from[$table]['filter'] = array($filter);
|
|
|
|
return $this;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Returns true if we are already joining to the given table alias
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2014-07-14 23:31:03 +02:00
|
|
|
* @param string $tableAlias Table name
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function isJoinedTo($tableAlias) {
|
|
|
|
return isset($this->from[$tableAlias]);
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Return a list of tables that this query is selecting from.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return array Unquoted table names
|
|
|
|
*/
|
|
|
|
public function queriedTables() {
|
|
|
|
$tables = array();
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
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);
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
$tables[] = preg_replace('/^"|"$/','',$table);
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
|
|
|
return $tables;
|
2013-06-21 00:32:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a list of tables queried
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getFrom() {
|
|
|
|
return $this->from;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Retrieves the finalised list of joins
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @todo This part of the code could be simplified
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2014-09-15 02:27:55 +02:00
|
|
|
* @param array $parameters Out variable for parameters required for this query
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return array List of joins as a mapping from array('Alias' => 'Join Expression')
|
|
|
|
*/
|
2014-09-15 02:27:55 +02:00
|
|
|
public function getJoins(&$parameters = array()) {
|
|
|
|
if(func_num_args() == 0) {
|
|
|
|
Deprecation::notice(
|
2015-06-19 01:59:27 +02:00
|
|
|
'4.0',
|
2014-09-15 02:27:55 +02:00
|
|
|
'SQLConditionalExpression::getJoins() now may produce parameters which are necessary to
|
|
|
|
execute this query'
|
|
|
|
);
|
|
|
|
}
|
2016-01-06 00:34:58 +01:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Sort the joins
|
2014-09-15 02:27:55 +02:00
|
|
|
$parameters = array();
|
2013-06-21 00:32:08 +02:00
|
|
|
$joins = $this->getOrderedJoins($this->from);
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Build from clauses
|
|
|
|
foreach($joins as $alias => $join) {
|
|
|
|
// $join can be something like this array structure
|
2014-08-15 08:53:05 +02:00
|
|
|
// array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1",
|
2013-06-21 00:32:08 +02:00
|
|
|
// "Status = 'approved'", 'order' => 20))
|
2014-09-15 02:27:55 +02:00
|
|
|
if(!is_array($join)) continue;
|
|
|
|
|
|
|
|
if(is_string($join['filter'])) {
|
|
|
|
$filter = $join['filter'];
|
|
|
|
} elseif(sizeof($join['filter']) == 1) {
|
|
|
|
$filter = $join['filter'][0];
|
|
|
|
} else {
|
|
|
|
$filter = "(" . implode(") AND (", $join['filter']) . ")";
|
|
|
|
}
|
2013-06-21 00:32:08 +02:00
|
|
|
|
2015-03-31 08:54:15 +02:00
|
|
|
// Ensure tables are quoted, unless the table is actually a sub-select
|
|
|
|
$table = preg_match('/\bSELECT\b/i', $join['table'])
|
|
|
|
? $join['table']
|
|
|
|
: "\"{$join['table']}\"";
|
|
|
|
$aliasClause = ($alias != $join['table'])
|
|
|
|
? " AS \"{$alias}\""
|
|
|
|
: "";
|
2014-09-15 02:27:55 +02:00
|
|
|
$joins[$alias] = strtoupper($join['type']) . " JOIN " . $table . "$aliasClause ON $filter";
|
|
|
|
if(!empty($join['parameters'])) {
|
|
|
|
$parameters = array_merge($parameters, $join['parameters']);
|
2013-06-21 00:32:08 +02:00
|
|
|
}
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
return $joins;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Ensure that framework "auto-generated" table JOINs are first in the finalised SQL query.
|
2014-08-15 08:53:05 +02:00
|
|
|
* 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:
|
2013-06-21 00:32:08 +02:00
|
|
|
*"...ERROR: missing FROM-clause..."
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param $from array - in the format of $this->from
|
|
|
|
* @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(
|
2014-08-15 08:53:05 +02:00
|
|
|
!is_array($firstJoin)
|
2013-06-21 00:32:08 +02:00
|
|
|
|| !is_array($secondJoin)
|
|
|
|
|| $firstJoin['order'] == $secondJoin['order']
|
|
|
|
) {
|
|
|
|
return 0;
|
|
|
|
} else {
|
|
|
|
return ($firstJoin['order'] < $secondJoin['order']) ? -1 : 1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2014-08-15 08:53:05 +02:00
|
|
|
// Put the first FROM table back into the results
|
2013-06-21 00:32:08 +02:00
|
|
|
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
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param array &$array - the array to sort
|
|
|
|
* @param callable $cmpFunction - the function to use for comparison
|
2014-08-15 08:53:05 +02:00
|
|
|
*/
|
2013-06-21 00:32:08 +02:00
|
|
|
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);
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Merge the remainder
|
|
|
|
while($val1) {
|
|
|
|
$array[key($array1)] = $val1;
|
|
|
|
$val1 = next($array1);
|
|
|
|
}
|
|
|
|
while($val2) {
|
|
|
|
$array[key($array2)] = $val2;
|
|
|
|
$val2 = next($array2);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Set a WHERE clause.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2015-06-15 08:35:01 +02:00
|
|
|
* @see SQLConditionalExpression::addWhere() for syntax examples
|
2013-06-21 00:32:08 +02:00
|
|
|
*
|
|
|
|
* @param mixed $where Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
|
|
|
* @param mixed $where,... Unlimited additional predicates
|
|
|
|
* @return self Self reference
|
|
|
|
*/
|
|
|
|
public function setWhere($where) {
|
|
|
|
$where = func_num_args() > 1 ? func_get_args() : $where;
|
|
|
|
$this->where = array();
|
|
|
|
return $this->addWhere($where);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a WHERE clause.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* Note that the database will execute any parameterised queries using
|
|
|
|
* prepared statements whenever available.
|
|
|
|
*
|
|
|
|
* There are several different ways of doing this.
|
|
|
|
*
|
|
|
|
* <code>
|
|
|
|
* // the entire predicate as a single string
|
|
|
|
* $query->addWhere("\"Column\" = 'Value'");
|
|
|
|
*
|
|
|
|
* // multiple predicates as an array
|
|
|
|
* $query->addWhere(array("\"Column\" = 'Value'", "\"Column\" != 'Value'"));
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* // Shorthand for the above using argument expansion
|
|
|
|
* $query->addWhere("\"Column\" = 'Value'", "\"Column\" != 'Value'");
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* // multiple predicates with parameters
|
|
|
|
* $query->addWhere(array('"Column" = ?' => $column, '"Name" = ?' => $value)));
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* // Shorthand for simple column comparison (as above), omitting the '?'
|
|
|
|
* $query->addWhere(array('"Column"' => $column, '"Name"' => $value));
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* // Multiple predicates, each with multiple parameters.
|
|
|
|
* $query->addWhere(array(
|
|
|
|
* '"ColumnOne" = ? OR "ColumnTwo" != ?' => array(1, 4),
|
|
|
|
* '"ID" != ?' => $value
|
|
|
|
* ));
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* // Using a dynamically generated condition (any object that implements SQLConditionGroup)
|
|
|
|
* $condition = new ObjectThatImplements_SQLConditionGroup();
|
|
|
|
* $query->addWhere($condition);
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* </code>
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* Note that if giving multiple parameters for a single predicate the array
|
|
|
|
* of values must be given as an indexed array, not an associative array.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* Also should be noted is that any null values for parameters may give unexpected
|
|
|
|
* behaviour. array('Column' => NULL) is shorthand for array('Column = ?', NULL), and
|
|
|
|
* will not match null values for that column, as 'Column IS NULL' is the correct syntax.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* Additionally, be careful of key conflicts. Adding two predicates with the same
|
|
|
|
* condition but different parameters can cause a key conflict if added in the same array.
|
|
|
|
* This can be solved by wrapping each individual condition in an array. E.g.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* <code>
|
|
|
|
* // Multiple predicates with duplicate conditions
|
|
|
|
* $query->addWhere(array(
|
|
|
|
* array('ID != ?' => 5),
|
|
|
|
* array('ID != ?' => 6)
|
|
|
|
* ));
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* // Alternatively this can be added in two separate calls to addWhere
|
|
|
|
* $query->addWhere(array('ID != ?' => 5));
|
|
|
|
* $query->addWhere(array('ID != ?' => 6));
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* // Or simply omit the outer array
|
|
|
|
* $query->addWhere(array('ID != ?' => 5), array('ID != ?' => 6));
|
|
|
|
* </code>
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* If it's necessary to force the parameter to be considered as a specific data type
|
|
|
|
* by the database connector's prepared query processor any parameter can be cast
|
|
|
|
* to that type by using the following format.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* <code>
|
|
|
|
* // Treat this value as a double type, regardless of its type within PHP
|
|
|
|
* $query->addWhere(array(
|
|
|
|
* 'Column' => array(
|
|
|
|
* 'value' => $variable,
|
|
|
|
* 'type' => 'double'
|
|
|
|
* )
|
|
|
|
* ));
|
|
|
|
* </code>
|
|
|
|
*
|
|
|
|
* @param mixed $where Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
|
|
|
* @param mixed $where,... Unlimited additional predicates
|
|
|
|
* @return self Self reference
|
|
|
|
*/
|
|
|
|
public function addWhere($where) {
|
|
|
|
$where = $this->normalisePredicates(func_get_args());
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// If the function is called with an array of items
|
|
|
|
$this->where = array_merge($this->where, $where);
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-06-15 08:35:01 +02:00
|
|
|
* @see SQLConditionalExpression::addWhere()
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param mixed $filters Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
|
|
|
* @param mixed $filters,... Unlimited additional predicates
|
|
|
|
* @return self Self reference
|
|
|
|
*/
|
|
|
|
public function setWhereAny($filters) {
|
|
|
|
$filters = func_num_args() > 1 ? func_get_args() : $filters;
|
|
|
|
return $this
|
|
|
|
->setWhere(array())
|
|
|
|
->addWhereAny($filters);
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
2015-06-15 08:35:01 +02:00
|
|
|
* @see SQLConditionalExpression::addWhere()
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param mixed $filters Predicate(s) to set, as escaped SQL statements or paramaterised queries
|
|
|
|
* @param mixed $filters,... Unlimited additional predicates
|
|
|
|
* @return self Self reference
|
|
|
|
*/
|
|
|
|
public function addWhereAny($filters) {
|
|
|
|
// Parse and split predicates along with any parameters
|
|
|
|
$filters = $this->normalisePredicates(func_get_args());
|
|
|
|
$this->splitQueryParameters($filters, $predicates, $parameters);
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
$clause = "(".implode(") OR (", $predicates).")";
|
|
|
|
return $this->addWhere(array($clause => $parameters));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return a list of WHERE clauses used internally.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getWhere() {
|
|
|
|
return $this->where;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Return a list of WHERE clauses used internally.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param array $parameters Out variable for parameters required for this query
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getWhereParameterised(&$parameters) {
|
|
|
|
$this->splitQueryParameters($this->where, $predicates, $parameters);
|
|
|
|
return $predicates;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Given a key / value pair, extract the predicate and any potential paramaters
|
|
|
|
* in a format suitable for storing internally as a list of paramaterised conditions.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param string|integer $key The left hand (key index) of this condition.
|
|
|
|
* Could be the predicate or an integer index.
|
|
|
|
* @param mixed $value The The right hand (array value) of this condition.
|
|
|
|
* Could be the predicate (if non-paramaterised), or the parameter(s). Could also be
|
|
|
|
* an array containing a nested condition in the similar format this function outputs.
|
|
|
|
* @return array|SQLConditionGroup A single item array in the format
|
|
|
|
* array($predicate => array($parameters)), unless it's a SQLConditionGroup
|
|
|
|
*/
|
|
|
|
protected function parsePredicate($key, $value) {
|
|
|
|
// If a string key is given then presume this is a paramaterised condition
|
|
|
|
if($value instanceof SQLConditionGroup) {
|
|
|
|
return $value;
|
|
|
|
} elseif(is_string($key)) {
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Extract the parameter(s) from the value
|
|
|
|
if(!is_array($value) || isset($value['type'])) {
|
|
|
|
$parameters = array($value);
|
|
|
|
} else {
|
|
|
|
$parameters = array_values($value);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append '= ?' if not present, parameters are given, and we have exactly one parameter
|
|
|
|
if(strpos($key, '?') === FALSE) {
|
|
|
|
$parameterCount = count($parameters);
|
|
|
|
if($parameterCount === 1) {
|
|
|
|
$key .= " = ?";
|
|
|
|
} elseif($parameterCount > 1) {
|
|
|
|
user_error("Incorrect number of '?' in predicate $key. Expected $parameterCount but none given.",
|
|
|
|
E_USER_ERROR);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return array($key => $parameters);
|
|
|
|
} elseif(is_array($value)) {
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// If predicates are nested one per array (as per the internal format)
|
|
|
|
// then run a quick check over the contents and recursively parse
|
|
|
|
if(count($value) != 1) {
|
|
|
|
user_error('Nested predicates should be given as a single item array in '
|
|
|
|
. 'array($predicate => array($prameters)) format)', E_USER_ERROR);
|
|
|
|
}
|
|
|
|
foreach($value as $key => $value) {
|
|
|
|
return $this->parsePredicate($key, $value);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Non-paramaterised condition
|
|
|
|
return array($value => array());
|
|
|
|
}
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Given a list of conditions in any user-acceptable format, convert this
|
|
|
|
* to an array of paramaterised predicates suitable for merging with $this->where.
|
|
|
|
*
|
|
|
|
* Normalised predicates are in the below format, in order to avoid key collisions.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* <code>
|
|
|
|
* array(
|
|
|
|
* array('Condition != ?' => array('parameter')),
|
|
|
|
* array('Condition != ?' => array('otherparameter')),
|
|
|
|
* array('Condition = 3' => array()),
|
|
|
|
* array('Condition = ? OR Condition = ?' => array('parameter1', 'parameter2))
|
|
|
|
* )
|
|
|
|
* </code>
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param array $predicates List of predicates. These should be wrapped in an array
|
|
|
|
* one level more than for addWhere, as query expansion is not supported here.
|
|
|
|
* @return array List of normalised predicates
|
|
|
|
*/
|
|
|
|
protected function normalisePredicates(array $predicates) {
|
|
|
|
// Since this function is called with func_get_args we should un-nest the single first parameter
|
|
|
|
if(count($predicates) == 1) $predicates = array_shift($predicates);
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// Ensure single predicates are iterable
|
|
|
|
if(!is_array($predicates)) $predicates = array($predicates);
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
$normalised = array();
|
|
|
|
foreach($predicates as $key => $value) {
|
|
|
|
if(empty($value) && (empty($key) || is_numeric($key))) continue; // Ignore empty conditions
|
|
|
|
$normalised[] = $this->parsePredicate($key, $value);
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
return $normalised;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Given a list of conditions as per the format of $this->where, split
|
|
|
|
* this into an array of predicates, and a separate array of ordered parameters
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* Note, that any SQLConditionGroup objects will be evaluated here.
|
|
|
|
* @see SQLConditionGroup
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @param array $conditions List of Conditions including parameters
|
|
|
|
* @param array $predicates Out parameter for the list of string predicates
|
|
|
|
* @param array $parameters Out parameter for the list of parameters
|
|
|
|
*/
|
|
|
|
public function splitQueryParameters($conditions, &$predicates, &$parameters) {
|
|
|
|
// Merge all filters with paramaterised queries
|
|
|
|
$predicates = array();
|
|
|
|
$parameters = array();
|
|
|
|
foreach($conditions as $condition) {
|
|
|
|
// Evaluate the result of SQLConditionGroup here
|
|
|
|
if($condition instanceof SQLConditionGroup) {
|
|
|
|
$conditionSQL = $condition->conditionSQL($conditionParameters);
|
|
|
|
if(!empty($conditionSQL)) {
|
|
|
|
$predicates[] = $conditionSQL;
|
|
|
|
$parameters = array_merge($parameters, $conditionParameters);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
foreach($condition as $key => $value) {
|
|
|
|
$predicates[] = $key;
|
|
|
|
$parameters = array_merge($parameters, $value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Checks whether this query is for a specific ID in a table
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ID=5")
|
|
|
|
*
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function filtersOnID() {
|
2016-05-17 08:30:33 +02:00
|
|
|
$regexp = '/^(.*\.)?("|`)?ID("|`)?\s?(=|IN)/';
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
foreach($this->getWhereParameterised($parameters) as $predicate) {
|
|
|
|
if(preg_match($regexp, $predicate)) return true;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
return false;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Checks whether this query is filtering on a foreign key, ie finding a has_many relationship
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @todo Doesn't work with combined statements (e.g. "Foo='bar' AND ParentID=5")
|
|
|
|
*
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public function filtersOnFK() {
|
2016-05-17 08:30:33 +02:00
|
|
|
$regexp = '/^(.*\.)?("|`)?[a-zA-Z]+ID("|`)?\s?(=|IN)/';
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
// @todo - Test this works with paramaterised queries
|
|
|
|
foreach($this->getWhereParameterised($parameters) as $predicate) {
|
|
|
|
if(preg_match($regexp, $predicate)) return true;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
return false;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
public function isEmpty() {
|
|
|
|
return empty($this->from);
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2013-06-21 00:32:08 +02:00
|
|
|
/**
|
|
|
|
* Generates an SQLDelete object using the currently specified parameters
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return SQLDelete
|
|
|
|
*/
|
|
|
|
public function toDelete() {
|
|
|
|
$delete = new SQLDelete();
|
|
|
|
$this->copyTo($delete);
|
|
|
|
return $delete;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2015-06-15 08:35:01 +02:00
|
|
|
* Generates an SQLSelect object using the currently specified parameters.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2015-06-15 08:35:01 +02:00
|
|
|
* @return SQLSelect
|
2013-06-21 00:32:08 +02:00
|
|
|
*/
|
|
|
|
public function toSelect() {
|
2015-06-15 08:35:01 +02:00
|
|
|
$select = new SQLSelect();
|
2013-06-21 00:32:08 +02:00
|
|
|
$this->copyTo($select);
|
|
|
|
return $select;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates an SQLUpdate object using the currently specified parameters.
|
|
|
|
* No fields will have any assigned values for the newly generated SQLUpdate
|
|
|
|
* object.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2013-06-21 00:32:08 +02:00
|
|
|
* @return SQLUpdate
|
|
|
|
*/
|
|
|
|
public function toUpdate() {
|
|
|
|
$update = new SQLUpdate();
|
|
|
|
$this->copyTo($update);
|
|
|
|
return $update;
|
|
|
|
}
|
|
|
|
}
|