Merge pull request #15643 from SilverFire/make-query-expression

Implement ExpressionInterface in Query
This commit is contained in:
Dmitry Naumenko
2018-02-12 17:54:46 +02:00
committed by GitHub
11 changed files with 57 additions and 23 deletions

View File

@ -47,7 +47,7 @@ use yii\base\InvalidConfigException;
* @author Carsten Brandt <mail@cebe.cc> * @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
class Query extends Component implements QueryInterface class Query extends Component implements QueryInterface, ExpressionInterface
{ {
use QueryTrait; use QueryTrait;

View File

@ -161,6 +161,7 @@ class QueryBuilder extends \yii\base\BaseObject
protected function defaultExpressionBuilders() protected function defaultExpressionBuilders()
{ {
return [ return [
'yii\db\Query' => 'yii\db\QueryExpressionBuilder',
'yii\db\PdoValue' => 'yii\db\PdoValueBuilder', 'yii\db\PdoValue' => 'yii\db\PdoValueBuilder',
'yii\db\Expression' => 'yii\db\ExpressionBuilder', 'yii\db\Expression' => 'yii\db\ExpressionBuilder',
'yii\db\conditions\ConjunctionCondition' => 'yii\db\conditions\ConjunctionConditionBuilder', 'yii\db\conditions\ConjunctionCondition' => 'yii\db\conditions\ConjunctionConditionBuilder',
@ -287,6 +288,10 @@ class QueryBuilder extends \yii\base\BaseObject
} }
} }
if ($this->expressionBuilders[$className] === __CLASS__) {
return $this;
}
if (!is_object($this->expressionBuilders[$className])) { if (!is_object($this->expressionBuilders[$className])) {
$this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this); $this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this);
} }

View File

@ -0,0 +1,30 @@
<?php
namespace yii\db;
/**
* Class QueryExpressionBuilder is used internally to build [[Query]] object
* using unified [[QueryBuilder]] expression building interface.
*
* @author Dmytro Naumenko <d.naumenko.a@gmail.com>
* @since 2.0.14
*/
class QueryExpressionBuilder implements ExpressionBuilderInterface
{
use ExpressionBuilderTrait;
/**
* Method builds the raw SQL from the $expression that will not be additionally
* escaped or quoted.
*
* @param ExpressionInterface|Query $expression the expression to be built.
* @param array $params the binding parameters.
* @return string the raw SQL that will not be additionally escaped or quoted.
*/
public function build(ExpressionInterface $expression, array &$params = [])
{
list($sql, $params) = $this->queryBuilder->build($expression, $params);
return "($sql)";
}
}

View File

@ -15,11 +15,11 @@ class BetweenCondition implements ConditionInterface
/** /**
* @var string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) * @var string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`)
*/ */
protected $operator; private $operator;
/** /**
* @var mixed the column name to the left of [[operator]] * @var mixed the column name to the left of [[operator]]
*/ */
protected $column; private $column;
/** /**
* @var mixed beginning of the interval * @var mixed beginning of the interval
*/ */

View File

@ -29,8 +29,8 @@ class ExistsConditionBuilder implements ExpressionBuilderInterface
$operator = $expression->getOperator(); $operator = $expression->getOperator();
$query = $expression->getQuery(); $query = $expression->getQuery();
list($sql, $params) = $this->queryBuilder->build($query, $params); $sql = $this->queryBuilder->buildExpression($query, $params);
return "$operator ($sql)"; return "$operator $sql";
} }
} }

View File

@ -3,6 +3,7 @@
namespace yii\db\conditions; namespace yii\db\conditions;
use yii\base\InvalidArgumentException; use yii\base\InvalidArgumentException;
use yii\db\ExpressionInterface;
/** /**
* Class InCondition represents `IN` condition. * Class InCondition represents `IN` condition.
@ -15,20 +16,20 @@ class InCondition implements ConditionInterface
/** /**
* @var string $operator the operator to use (e.g. `IN` or `NOT IN`) * @var string $operator the operator to use (e.g. `IN` or `NOT IN`)
*/ */
protected $operator; private $operator;
/** /**
* @var string|string[] the column name. If it is an array, a composite `IN` condition * @var string|string[] the column name. If it is an array, a composite `IN` condition
* will be generated. * will be generated.
*/ */
protected $column; private $column;
/** /**
* @var array an array of values that [[column]] value should be among. * @var ExpressionInterface[]|string[]|int[] an array of values that [[column]] value should be among.
* If it is an empty array the generated expression will be a `false` value if * If it is an empty array the generated expression will be a `false` value if
* [[operator]] is `IN` and empty if operator is `NOT IN`. * [[operator]] is `IN` and empty if operator is `NOT IN`.
*/ */
protected $values; private $values;
/** /**
* SimpleCondition constructor * SimpleCondition constructor
@ -63,7 +64,7 @@ class InCondition implements ConditionInterface
} }
/** /**
* @return mixed * @return ExpressionInterface[]|string[]|int[]
*/ */
public function getValues() public function getValues()
{ {

View File

@ -109,7 +109,8 @@ class InConditionBuilder implements ExpressionBuilderInterface
*/ */
protected function buildSubqueryInCondition($operator, $columns, $values, &$params) protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
{ {
list($sql, $params) = $this->queryBuilder->build($values, $params); $sql = $this->queryBuilder->buildExpression($values, $params);
if (is_array($columns)) { if (is_array($columns)) {
foreach ($columns as $i => $col) { foreach ($columns as $i => $col) {
if (strpos($col, '(') === false) { if (strpos($col, '(') === false) {
@ -117,14 +118,14 @@ class InConditionBuilder implements ExpressionBuilderInterface
} }
} }
return '(' . implode(', ', $columns) . ") $operator ($sql)"; return '(' . implode(', ', $columns) . ") $operator $sql";
} }
if (strpos($columns, '(') === false) { if (strpos($columns, '(') === false) {
$columns = $this->queryBuilder->db->quoteColumnName($columns); $columns = $this->queryBuilder->db->quoteColumnName($columns);
} }
return "$columns $operator ($sql)"; return "$columns $operator $sql";
} }
/** /**

View File

@ -15,7 +15,7 @@ class NotCondition implements ConditionInterface
/** /**
* @var mixed the condition to be negated * @var mixed the condition to be negated
*/ */
protected $condition; private $condition;
/** /**
* NotCondition constructor. * NotCondition constructor.

View File

@ -15,15 +15,15 @@ class SimpleCondition implements ConditionInterface
/** /**
* @var string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc. * @var string $operator the operator to use. Anything could be used e.g. `>`, `<=`, etc.
*/ */
protected $operator; private $operator;
/** /**
* @var mixed the column name to the left of [[operator]] * @var mixed the column name to the left of [[operator]]
*/ */
protected $column; private $column;
/** /**
* @var mixed the value to the right of the [[operator]] * @var mixed the value to the right of the [[operator]]
*/ */
protected $value; private $value;
/** /**
* SimpleCondition constructor * SimpleCondition constructor

View File

@ -41,10 +41,6 @@ class SimpleConditionBuilder implements ExpressionBuilderInterface
if ($value instanceof ExpressionInterface) { if ($value instanceof ExpressionInterface) {
return "$column $operator {$this->queryBuilder->buildExpression($value, $params)}"; return "$column $operator {$this->queryBuilder->buildExpression($value, $params)}";
} }
if ($value instanceof Query) {
list($sql, $params) = $this->queryBuilder->build($value, $params);
return "$column $operator ($sql)";
}
$phName = $this->queryBuilder->bindParam($value, $params); $phName = $this->queryBuilder->bindParam($value, $params);
return "$column $operator $phName"; return "$column $operator $phName";

View File

@ -1092,18 +1092,19 @@ abstract class QueryBuilderTest extends DatabaseTestCase
// not // not
[['not', 'name'], 'NOT (name)', []], [['not', 'name'], 'NOT (name)', []],
[['not', (new Query)->select('exists')->from('some_table')], 'NOT ((SELECT [[exists]] FROM [[some_table]]))', []],
// and // and
[['and', 'id=1', 'id=2'], '(id=1) AND (id=2)', []], [['and', 'id=1', 'id=2'], '(id=1) AND (id=2)', []],
[['and', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) AND ((id=1) OR (id=2))', []], [['and', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) AND ((id=1) OR (id=2))', []],
[['and', 'id=1', new Expression('id=:qp0', [':qp0' => 2])], '(id=1) AND (id=:qp0)', [':qp0' => 2]], [['and', 'id=1', new Expression('id=:qp0', [':qp0' => 2])], '(id=1) AND (id=:qp0)', [':qp0' => 2]],
[['and', ['expired' => false], (new Query)->select('count(*) > 1')->from('queue')], '([[expired]]=:qp0) AND ((SELECT count(*) > 1 FROM [[queue]]))', [':qp0' => false]],
// or // or
[['or', 'id=1', 'id=2'], '(id=1) OR (id=2)', []], [['or', 'id=1', 'id=2'], '(id=1) OR (id=2)', []],
[['or', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) OR ((id=1) OR (id=2))', []], [['or', 'type=1', ['or', 'id=1', 'id=2']], '(type=1) OR ((id=1) OR (id=2))', []],
[['or', 'type=1', new Expression('id=:qp0', [':qp0' => 1])], '(type=1) OR (id=:qp0)', [':qp0' => 1]], [['or', 'type=1', new Expression('id=:qp0', [':qp0' => 1])], '(type=1) OR (id=:qp0)', [':qp0' => 1]],
// between // between
[['between', 'id', 1, 10], '[[id]] BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10]], [['between', 'id', 1, 10], '[[id]] BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10]],
[['not between', 'id', 1, 10], '[[id]] NOT BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10]], [['not between', 'id', 1, 10], '[[id]] NOT BETWEEN :qp0 AND :qp1', [':qp0' => 1, ':qp1' => 10]],
@ -1118,7 +1119,7 @@ abstract class QueryBuilderTest extends DatabaseTestCase
[new BetweenColumnsCondition(new Expression('NOW()'), 'NOT BETWEEN', (new Query)->select('min_date')->from('some_table'), 'max_date'), 'NOW() NOT BETWEEN (SELECT [[min_date]] FROM [[some_table]]) AND [[max_date]]', []], [new BetweenColumnsCondition(new Expression('NOW()'), 'NOT BETWEEN', (new Query)->select('min_date')->from('some_table'), 'max_date'), 'NOW() NOT BETWEEN (SELECT [[min_date]] FROM [[some_table]]) AND [[max_date]]', []],
// in // in
[['in', 'id', [1, 2, 3]], '[[id]] IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3]], [['in', 'id', [1, 2, (new Query())->select('three')->from('digits')]], '[[id]] IN (:qp0, :qp1, (SELECT [[three]] FROM [[digits]]))', [':qp0' => 1, ':qp1' => 2]],
[['not in', 'id', [1, 2, 3]], '[[id]] NOT IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3]], [['not in', 'id', [1, 2, 3]], '[[id]] NOT IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3]],
[['in', 'id', (new Query())->select('id')->from('users')->where(['active' => 1])], '[[id]] IN (SELECT [[id]] FROM [[users]] WHERE [[active]]=:qp0)', [':qp0' => 1]], [['in', 'id', (new Query())->select('id')->from('users')->where(['active' => 1])], '[[id]] IN (SELECT [[id]] FROM [[users]] WHERE [[active]]=:qp0)', [':qp0' => 1]],
[['not in', 'id', (new Query())->select('id')->from('users')->where(['active' => 1])], '[[id]] NOT IN (SELECT [[id]] FROM [[users]] WHERE [[active]]=:qp0)', [':qp0' => 1]], [['not in', 'id', (new Query())->select('id')->from('users')->where(['active' => 1])], '[[id]] NOT IN (SELECT [[id]] FROM [[users]] WHERE [[active]]=:qp0)', [':qp0' => 1]],