Commit a5140e33 authored by Benjamin Morel's avatar Benjamin Morel

Introduce properties for SQL parts in QueryBuilder

parent 6d673d77
......@@ -5,10 +5,9 @@
1. The `select()`, `addSelect()`, `groupBy()` and `addGroupBy()` methods no longer accept an array of arguments. Pass each expression as an individual argument or expand an array of expressions using the `...` operator.
2. The `select()`, `addSelect()`, `groupBy()` and `addGroupBy()` methods no longer ignore the first argument if it's empty.
3. The `addSelect()` method can be no longer called without arguments.
## BC BREAK: `QueryBuilder::insert()`, `update()` and `delete()` signatures changed
These methods now require the `$table` parameter, and do not support aliases anymore.
4. The `insert()`, `update()` and `delete()` methods now require the `$table` parameter, and do not support aliases anymore.
5. The `add()`, `getQueryPart()`, `getQueryParts()`, `resetQueryPart()` and `resetQueryParts()` methods are removed.
6. For a `select()` query, the `getSQL()` method now throws an expression if no `SELECT` expressions have been provided.
## BC BREAK: `OCI8Statement::convertPositionalToNamedPlaceholders()` is removed.
......
......@@ -14,12 +14,10 @@ use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_shift;
use function array_unshift;
use function count;
use function implode;
use function is_array;
use function is_object;
use function key;
use function substr;
/**
......@@ -55,30 +53,6 @@ class QueryBuilder
*/
private $connection;
/*
* The default values of SQL parts collection
*/
private const SQL_PARTS_DEFAULTS = [
'select' => [],
'distinct' => false,
'from' => [],
'table' => null,
'join' => [],
'set' => [],
'where' => null,
'groupBy' => [],
'having' => null,
'orderBy' => [],
'values' => [],
];
/**
* The array of SQL parts collected.
*
* @var array<string, mixed>
*/
private $sqlParts = self::SQL_PARTS_DEFAULTS;
/**
* The complete SQL string for this query.
*
......@@ -135,6 +109,83 @@ class QueryBuilder
*/
private $boundCounter = 0;
/**
* The SELECT parts of the query.
*
* @var string[]
*/
private $select = [];
/**
* Whether this is a SELECT DISTINCT query.
*
* @var bool
*/
private $distinct = false;
/**
* The FROM parts of a SELECT query.
*
* @var From[]
*/
private $from = [];
/**
* The table name for an INSERT, UPDATE or DELETE query.
*
* @var string|null
*/
private $table;
/**
* The list of joins, indexed by from alias.
*
* @var array<string, Join[]>
*/
private $join = [];
/**
* The SET parts of an UPDATE query.
*
* @var string[]
*/
private $set = [];
/**
* The WHERE part of a SELECT, UPDATE or DELETE query.
*
* @var string|CompositeExpression|null
*/
private $where;
/**
* The GROUP BY part of a SELECT query.
*
* @var string[]
*/
private $groupBy = [];
/**
* The HAVING part of a SELECT query.
*
* @var string|CompositeExpression|null
*/
private $having;
/**
* The ORDER BY parts of a SELECT query.
*
* @var string[]
*/
private $orderBy = [];
/**
* The values of an INSERT query.
*
* @var array<string, mixed>
*/
private $values = [];
/**
* Initializes a new <tt>QueryBuilder</tt>.
*
......@@ -218,6 +269,8 @@ class QueryBuilder
* </code>
*
* @return string The SQL query string.
*
* @throws QueryException If the object doesn't represent a valid query in its current state.
*/
public function getSQL() : string
{
......@@ -399,49 +452,6 @@ class QueryBuilder
return $this->maxResults;
}
/**
* Either appends to or replaces a single, generic query part.
*
* The available parts are: 'select', 'from', 'set', 'where',
* 'groupBy', 'having' and 'orderBy'.
*
* @param mixed $sqlPart
*
* @return $this This QueryBuilder instance.
*/
public function add(string $sqlPartName, $sqlPart, bool $append = false) : self
{
$isArray = is_array($sqlPart);
$isMultiple = is_array($this->sqlParts[$sqlPartName]);
if ($isMultiple && ! $isArray) {
$sqlPart = [$sqlPart];
}
$this->state = self::STATE_DIRTY;
if ($append) {
if ($sqlPartName === 'orderBy' || $sqlPartName === 'groupBy' || $sqlPartName === 'select' || $sqlPartName === 'set' || $sqlPartName === 'from') {
foreach ($sqlPart as $part) {
$this->sqlParts[$sqlPartName][] = $part;
}
} elseif ($isArray && (is_array($sqlPart[key($sqlPart)]) || is_object($sqlPart[key($sqlPart)]))) {
$key = key($sqlPart);
$this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key];
} elseif ($isMultiple) {
$this->sqlParts[$sqlPartName][] = $sqlPart;
} else {
$this->sqlParts[$sqlPartName] = $sqlPart;
}
return $this;
}
$this->sqlParts[$sqlPartName] = $sqlPart;
return $this;
}
/**
* Specifies an item that is to be returned in the query result.
* Replaces any previously specified selections, if any.
......@@ -465,7 +475,11 @@ class QueryBuilder
return $this;
}
return $this->add('select', $expressions);
$this->select = $expressions;
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -482,7 +496,9 @@ class QueryBuilder
*/
public function distinct() : self
{
$this->sqlParts['distinct'] = true;
$this->distinct = true;
$this->state = self::STATE_DIRTY;
return $this;
}
......@@ -507,7 +523,11 @@ class QueryBuilder
{
$this->type = self::SELECT;
return $this->add('select', array_merge([$expression], $expressions), true);
$this->select = array_merge($this->select, [$expression], $expressions);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -529,7 +549,11 @@ class QueryBuilder
{
$this->type = self::DELETE;
return $this->add('table', $table);
$this->table = $table;
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -551,7 +575,11 @@ class QueryBuilder
{
$this->type = self::UPDATE;
return $this->add('table', $table);
$this->table = $table;
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -577,7 +605,11 @@ class QueryBuilder
{
$this->type = self::INSERT;
return $this->add('table', $table);
$this->table = $table;
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -590,14 +622,18 @@ class QueryBuilder
* ->from('users', 'u')
* </code>
*
* @param string $from The table.
* @param string $table The table.
* @param string|null $alias The alias of the table.
*
* @return $this This QueryBuilder instance.
*/
public function from(string $from, ?string $alias = null) : self
public function from(string $table, ?string $alias = null) : self
{
return $this->add('from', new From($from, $alias), true);
$this->from[] = new From($table, $alias);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -641,9 +677,11 @@ class QueryBuilder
*/
public function innerJoin(string $fromAlias, string $join, string $alias, ?string $condition = null) : self
{
return $this->add('join', [
$fromAlias => Join::inner($join, $alias, $condition),
], true);
$this->join[$fromAlias][] = Join::inner($join, $alias, $condition);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -665,9 +703,11 @@ class QueryBuilder
*/
public function leftJoin(string $fromAlias, string $join, string $alias, ?string $condition = null) : self
{
return $this->add('join', [
$fromAlias => Join::left($join, $alias, $condition),
], true);
$this->join[$fromAlias][] = Join::left($join, $alias, $condition);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -689,9 +729,11 @@ class QueryBuilder
*/
public function rightJoin(string $fromAlias, string $join, string $alias, ?string $condition = null) : self
{
return $this->add('join', [
$fromAlias => Join::right($join, $alias, $condition),
], true);
$this->join[$fromAlias][] = Join::right($join, $alias, $condition);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -711,7 +753,11 @@ class QueryBuilder
*/
public function set(string $key, string $value) : self
{
return $this->add('set', $key . ' = ' . $value, true);
$this->set[] = $key . ' = ' . $value;
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -743,7 +789,11 @@ class QueryBuilder
*/
public function where($predicate, ...$predicates) : self
{
return $this->setPredicates('where', $predicate, ...$predicates);
$this->where = $this->createPredicate($predicate, ...$predicates);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -767,7 +817,11 @@ class QueryBuilder
*/
public function andWhere($predicate, ...$predicates) : self
{
return $this->appendPredicates('where', CompositeExpression::TYPE_AND, $predicate, ...$predicates);
$this->where = $this->appendToPredicate($this->where, CompositeExpression::TYPE_AND, $predicate, ...$predicates);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -791,7 +845,11 @@ class QueryBuilder
*/
public function orWhere($predicate, ...$predicates) : self
{
return $this->appendPredicates('where', CompositeExpression::TYPE_OR, $predicate, ...$predicates);
$this->where = $this->appendToPredicate($this->where, CompositeExpression::TYPE_OR, $predicate, ...$predicates);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -812,7 +870,11 @@ class QueryBuilder
*/
public function groupBy(string $expression, string ...$expressions) : self
{
return $this->add('groupBy', array_merge([$expression], $expressions), false);
$this->groupBy = array_merge([$expression], $expressions);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -833,7 +895,11 @@ class QueryBuilder
*/
public function addGroupBy(string $expression, string ...$expressions) : self
{
return $this->add('groupBy', array_merge([$expression], $expressions), true);
$this->groupBy = array_merge($this->groupBy, [$expression], $expressions);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -857,7 +923,7 @@ class QueryBuilder
*/
public function setValue(string $column, string $value) : self
{
$this->sqlParts['values'][$column] = $value;
$this->values[$column] = $value;
return $this;
}
......@@ -883,7 +949,11 @@ class QueryBuilder
*/
public function values(array $values) : self
{
return $this->add('values', $values);
$this->values = $values;
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -897,7 +967,11 @@ class QueryBuilder
*/
public function having($predicate, ...$predicates) : self
{
return $this->setPredicates('having', $predicate, ...$predicates);
$this->having = $this->createPredicate($predicate, ...$predicates);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -911,7 +985,11 @@ class QueryBuilder
*/
public function andHaving($predicate, ...$predicates) : self
{
return $this->appendPredicates('having', CompositeExpression::TYPE_AND, $predicate, ...$predicates);
$this->having = $this->appendToPredicate($this->having, CompositeExpression::TYPE_AND, $predicate, ...$predicates);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -925,52 +1003,51 @@ class QueryBuilder
*/
public function orHaving($predicate, ...$predicates) : self
{
return $this->appendPredicates('having', CompositeExpression::TYPE_OR, $predicate, ...$predicates);
$this->having = $this->appendToPredicate($this->having, CompositeExpression::TYPE_OR, $predicate, ...$predicates);
$this->state = self::STATE_DIRTY;
return $this;
}
/**
* Sets one or more predicates combined by the AND logic as the given query clause.
* Replaces any previously specified predicates.
* Creates a CompositeExpression from one or more predicates combined by the AND logic.
*
* @param string|CompositeExpression $predicate
* @param string|CompositeExpression ...$predicates
*
* @return $this This QueryBuilder instance.
* @return string|CompositeExpression
*/
private function setPredicates(string $clause, ...$predicates) : self
private function createPredicate($predicate, ...$predicates)
{
if (count($predicates) > 1) {
$predicate = new CompositeExpression(
CompositeExpression::TYPE_AND,
$predicates
);
} else {
$predicate = array_shift($predicates);
if (count($predicates) === 0) {
return $predicate;
}
return $this->add($clause, $predicate);
return new CompositeExpression(CompositeExpression::TYPE_AND, array_merge([$predicate], $predicates));
}
/**
* Appends the given predicates combined by the given type of logic to the given query clause.
* Appends the given predicates combined by the given type of logic to the current predicate.
*
* @param string|CompositeExpression|null $currentPredicate
* @param string|CompositeExpression ...$predicates
*
* @return $this This QueryBuilder instance.
* @return string|CompositeExpression
*/
private function appendPredicates(string $clause, string $type, ...$predicates) : self
private function appendToPredicate($currentPredicate, string $type, ...$predicates)
{
$predicate = $this->getQueryPart($clause);
if ($currentPredicate instanceof CompositeExpression && $currentPredicate->getType() === $type) {
return $currentPredicate->addMultiple($predicates);
}
if ($predicate instanceof CompositeExpression && $predicate->getType() === $type) {
$predicate->addMultiple($predicates);
} else {
$predicate = new CompositeExpression(
$type,
array_merge([$predicate], $predicates)
);
if ($currentPredicate !== null) {
array_unshift($predicates, $currentPredicate);
} elseif (count($predicates) === 1) {
return $predicates[0];
}
return $this->add($clause, $predicate, true);
return new CompositeExpression($type, $predicates);
}
/**
......@@ -984,7 +1061,17 @@ class QueryBuilder
*/
public function orderBy(string $sort, ?string $order = null) : self
{
return $this->add('orderBy', $sort . ' ' . (! $order ? 'ASC' : $order), false);
$orderBy = $sort;
if ($order !== null) {
$orderBy .= ' ' . $order;
}
$this->orderBy = [$orderBy];
$this->state = self::STATE_DIRTY;
return $this;
}
/**
......@@ -997,76 +1084,55 @@ class QueryBuilder
*/
public function addOrderBy(string $sort, ?string $order = null) : self
{
return $this->add('orderBy', $sort . ' ' . (! $order ? 'ASC' : $order), true);
}
$orderBy = $sort;
/**
* Gets a query part by its name.
*
* @return mixed
*/
public function getQueryPart(string $queryPartName)
{
return $this->sqlParts[$queryPartName];
if ($order !== null) {
$orderBy .= ' ' . $order;
}
/**
* Gets all query parts.
*
* @return array<string, mixed>
*/
public function getQueryParts() : array
{
return $this->sqlParts;
$this->orderBy[] = $orderBy;
$this->state = self::STATE_DIRTY;
return $this;
}
/**
* Resets SQL parts.
*
* @param array<int, string>|null $queryPartNames
*
* @return $this This QueryBuilder instance.
* @throws QueryException
*/
public function resetQueryParts(?array $queryPartNames = null) : self
private function getSQLForSelect() : string
{
if ($queryPartNames === null) {
$queryPartNames = array_keys($this->sqlParts);
if (count($this->select) === 0) {
throw new QueryException('No SELECT expressions given. Please use select() or addSelect().');
}
foreach ($queryPartNames as $queryPartName) {
$this->resetQueryPart($queryPartName);
}
$query = 'SELECT';
return $this;
if ($this->distinct) {
$query .= ' DISTINCT';
}
/**
* Resets a single SQL part.
*
* @return $this This QueryBuilder instance.
*/
public function resetQueryPart(string $queryPartName) : self
{
$this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName];
$query .= ' ' . implode(', ', $this->select);
$this->state = self::STATE_DIRTY;
if (count($this->from) !== 0) {
$query .= ' FROM ' . implode(', ', $this->getFromClauses());
}
return $this;
if ($this->where !== null) {
$query .= ' WHERE ' . $this->where;
}
/**
* @throws QueryException
*/
private function getSQLForSelect() : string
{
$query = 'SELECT ' . ($this->sqlParts['distinct'] ? 'DISTINCT ' : '') .
implode(', ', $this->sqlParts['select']);
if (count($this->groupBy) !== 0) {
$query .= ' GROUP BY ' . implode(', ', $this->groupBy);
}
if ($this->having !== null) {
$query .= ' HAVING ' . $this->having;
}
$query .= ($this->sqlParts['from'] ? ' FROM ' . implode(', ', $this->getFromClauses()) : '')
. ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '')
. ($this->sqlParts['groupBy'] ? ' GROUP BY ' . implode(', ', $this->sqlParts['groupBy']) : '')
. ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '')
. ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : '');
if (count($this->orderBy) !== 0) {
$query .= ' ORDER BY ' . implode(', ', $this->orderBy);
}
if ($this->isLimitQuery()) {
return $this->connection->getDatabasePlatform()->modifyLimitQuery(
......@@ -1087,9 +1153,7 @@ class QueryBuilder
$fromClauses = [];
$knownAliases = [];
// Loop through all FROM clauses
/** @var From $from */
foreach ($this->sqlParts['from'] as $from) {
foreach ($this->from as $from) {
if ($from->alias === null || $from->alias === $from->table) {
$tableSql = $from->table;
$tableReference = $from->table;
......@@ -1115,7 +1179,7 @@ class QueryBuilder
*/
private function verifyAllAliasesAreKnown(array $knownAliases) : void
{
foreach ($this->sqlParts['join'] as $fromAlias => $joins) {
foreach ($this->join as $fromAlias => $joins) {
if (! isset($knownAliases[$fromAlias])) {
throw UnknownAlias::new($fromAlias, array_keys($knownAliases));
}
......@@ -1132,9 +1196,9 @@ class QueryBuilder
*/
private function getSQLForInsert() : string
{
return 'INSERT INTO ' . $this->sqlParts['table'] .
' (' . implode(', ', array_keys($this->sqlParts['values'])) . ')' .
' VALUES(' . implode(', ', $this->sqlParts['values']) . ')';
return 'INSERT INTO ' . $this->table .
' (' . implode(', ', array_keys($this->values)) . ')' .
' VALUES(' . implode(', ', $this->values) . ')';
}
/**
......@@ -1142,9 +1206,13 @@ class QueryBuilder
*/
private function getSQLForUpdate() : string
{
return 'UPDATE ' . $this->sqlParts['table']
. ' SET ' . implode(', ', $this->sqlParts['set'])
. ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
$query = 'UPDATE ' . $this->table . ' SET ' . implode(', ', $this->set);
if ($this->where !== null) {
$query .= ' WHERE ' . $this->where;
}
return $query;
}
/**
......@@ -1152,7 +1220,13 @@ class QueryBuilder
*/
private function getSQLForDelete() : string
{
return 'DELETE FROM ' . $this->sqlParts['table'] . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '');
$query = 'DELETE FROM ' . $this->table;
if ($this->where !== null) {
$query .= ' WHERE ' . $this->where;
}
return $query;
}
/**
......@@ -1241,9 +1315,11 @@ class QueryBuilder
{
$sql = '';
if (isset($this->sqlParts['join'][$fromAlias])) {
/** @var Join $join */
foreach ($this->sqlParts['join'][$fromAlias] as $join) {
if (! isset($this->join[$fromAlias])) {
return $sql;
}
foreach ($this->join[$fromAlias] as $join) {
if (array_key_exists($join->alias, $knownAliases)) {
throw NonUniqueAlias::new($join->alias, array_keys($knownAliases));
}
......@@ -1253,11 +1329,9 @@ class QueryBuilder
$knownAliases[$join->alias] = true;
}
foreach ($this->sqlParts['join'][$fromAlias] as $join) {
/** @var Join $join */
foreach ($this->join[$fromAlias] as $join) {
$sql .= $this->getSQLForJoins($join->alias, $knownAliases);
}
}
return $sql;
}
......@@ -1267,18 +1341,22 @@ class QueryBuilder
*/
public function __clone()
{
foreach ($this->sqlParts as $part => $elements) {
if (is_array($this->sqlParts[$part])) {
foreach ($this->sqlParts[$part] as $idx => $element) {
if (! is_object($element)) {
continue;
foreach ($this->from as $key => $from) {
$this->from[$key] = clone $from;
}
$this->sqlParts[$part][$idx] = clone $element;
foreach ($this->join as $fromAlias => $joins) {
foreach ($joins as $key => $join) {
$this->join[$fromAlias][$key] = clone $join;
}
} elseif (is_object($elements)) {
$this->sqlParts[$part] = clone $elements;
}
if (is_object($this->where)) {
$this->where = clone $this->where;
}
if (is_object($this->having)) {
$this->having = clone $this->having;
}
foreach ($this->params as $name => $param) {
......
......@@ -293,7 +293,7 @@ class QueryBuilderTest extends DbalTestCase
->from('users', 'u')
->orderBy('u.name');
self::assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name ASC', (string) $qb);
self::assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name', (string) $qb);
}
public function testSelectAddOrderBy() : void
......@@ -305,7 +305,7 @@ class QueryBuilderTest extends DbalTestCase
->orderBy('u.name')
->addOrderBy('u.username', 'DESC');
self::assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name ASC, u.username DESC', (string) $qb);
self::assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name, u.username DESC', (string) $qb);
}
public function testSelectAddAddOrderBy() : void
......@@ -317,7 +317,7 @@ class QueryBuilderTest extends DbalTestCase
->addOrderBy('u.name')
->addOrderBy('u.username', 'DESC');
self::assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name ASC, u.username DESC', (string) $qb);
self::assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name, u.username DESC', (string) $qb);
}
public function testEmptySelect() : void
......@@ -327,6 +327,9 @@ class QueryBuilderTest extends DbalTestCase
self::assertSame($qb, $qb2);
self::assertEquals(QueryBuilder::SELECT, $qb->getType());
$this->expectException(QueryException::class);
$qb->getSQL();
}
public function testSelectAddSelect() : void
......@@ -506,28 +509,6 @@ class QueryBuilderTest extends DbalTestCase
self::assertEquals(10, $qb->getFirstResult());
}
public function testResetQueryPart() : void
{
$qb = new QueryBuilder($this->conn);
$qb->select('u.*')->from('users', 'u')->where('u.name = ?');
self::assertEquals('SELECT u.* FROM users u WHERE u.name = ?', (string) $qb);
$qb->resetQueryPart('where');
self::assertEquals('SELECT u.* FROM users u', (string) $qb);
}
public function testResetQueryParts() : void
{
$qb = new QueryBuilder($this->conn);
$qb->select('u.*')->from('users', 'u')->where('u.name = ?')->orderBy('u.name');
self::assertEquals('SELECT u.* FROM users u WHERE u.name = ? ORDER BY u.name ASC', (string) $qb);
$qb->resetQueryParts(['where', 'orderBy']);
self::assertEquals('SELECT u.* FROM users u', (string) $qb);
}
public function testCreateNamedParameter() : void
{
$qb = new QueryBuilder($this->conn);
......@@ -692,7 +673,6 @@ class QueryBuilderTest extends DbalTestCase
$qb->andWhere('u.id = 1');
self::assertNotSame($qb->getQueryParts(), $qb_clone->getQueryParts());
self::assertNotSame($qb->getParameters(), $qb_clone->getParameters());
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment