diff --git a/framework/db/Query.php b/framework/db/Query.php index 9f7c4b9b61..a245190254 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -47,7 +47,7 @@ use yii\base\InvalidConfigException; * @author Carsten Brandt * @since 2.0 */ -class Query extends Component implements QueryInterface +class Query extends Component implements QueryInterface, ExpressionInterface { use QueryTrait; diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 4d6f01285c..1585153f38 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -161,6 +161,7 @@ class QueryBuilder extends \yii\base\BaseObject protected function defaultExpressionBuilders() { return [ + 'yii\db\Query' => 'yii\db\QueryExpressionBuilder', 'yii\db\PdoValue' => 'yii\db\PdoValueBuilder', 'yii\db\Expression' => 'yii\db\ExpressionBuilder', '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])) { $this->expressionBuilders[$className] = new $this->expressionBuilders[$className]($this); } diff --git a/framework/db/QueryExpressionBuilder.php b/framework/db/QueryExpressionBuilder.php new file mode 100644 index 0000000000..5c42530f0d --- /dev/null +++ b/framework/db/QueryExpressionBuilder.php @@ -0,0 +1,30 @@ + + * @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)"; + } +} diff --git a/framework/db/conditions/BetweenCondition.php b/framework/db/conditions/BetweenCondition.php index 8bf7d1f82a..50473a8bdc 100644 --- a/framework/db/conditions/BetweenCondition.php +++ b/framework/db/conditions/BetweenCondition.php @@ -15,11 +15,11 @@ class BetweenCondition implements ConditionInterface /** * @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]] */ - protected $column; + private $column; /** * @var mixed beginning of the interval */ diff --git a/framework/db/conditions/ExistsConditionBuilder.php b/framework/db/conditions/ExistsConditionBuilder.php index dabc662141..1846f4cc44 100644 --- a/framework/db/conditions/ExistsConditionBuilder.php +++ b/framework/db/conditions/ExistsConditionBuilder.php @@ -29,8 +29,8 @@ class ExistsConditionBuilder implements ExpressionBuilderInterface $operator = $expression->getOperator(); $query = $expression->getQuery(); - list($sql, $params) = $this->queryBuilder->build($query, $params); + $sql = $this->queryBuilder->buildExpression($query, $params); - return "$operator ($sql)"; + return "$operator $sql"; } } diff --git a/framework/db/conditions/InCondition.php b/framework/db/conditions/InCondition.php index 6df33f3bba..9a66554679 100644 --- a/framework/db/conditions/InCondition.php +++ b/framework/db/conditions/InCondition.php @@ -3,6 +3,7 @@ namespace yii\db\conditions; use yii\base\InvalidArgumentException; +use yii\db\ExpressionInterface; /** * 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`) */ - protected $operator; + private $operator; /** * @var string|string[] the column name. If it is an array, a composite `IN` condition * 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 * [[operator]] is `IN` and empty if operator is `NOT IN`. */ - protected $values; + private $values; /** * SimpleCondition constructor @@ -63,7 +64,7 @@ class InCondition implements ConditionInterface } /** - * @return mixed + * @return ExpressionInterface[]|string[]|int[] */ public function getValues() { diff --git a/framework/db/conditions/InConditionBuilder.php b/framework/db/conditions/InConditionBuilder.php index 37bcd7d480..6554cbd333 100644 --- a/framework/db/conditions/InConditionBuilder.php +++ b/framework/db/conditions/InConditionBuilder.php @@ -109,7 +109,8 @@ class InConditionBuilder implements ExpressionBuilderInterface */ 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)) { foreach ($columns as $i => $col) { 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) { $columns = $this->queryBuilder->db->quoteColumnName($columns); } - return "$columns $operator ($sql)"; + return "$columns $operator $sql"; } /** diff --git a/framework/db/conditions/NotCondition.php b/framework/db/conditions/NotCondition.php index 13697d2a72..8925f39993 100644 --- a/framework/db/conditions/NotCondition.php +++ b/framework/db/conditions/NotCondition.php @@ -15,7 +15,7 @@ class NotCondition implements ConditionInterface /** * @var mixed the condition to be negated */ - protected $condition; + private $condition; /** * NotCondition constructor. diff --git a/framework/db/conditions/SimpleCondition.php b/framework/db/conditions/SimpleCondition.php index f10823a889..fe4632df38 100644 --- a/framework/db/conditions/SimpleCondition.php +++ b/framework/db/conditions/SimpleCondition.php @@ -15,15 +15,15 @@ class SimpleCondition implements ConditionInterface /** * @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]] */ - protected $column; + private $column; /** * @var mixed the value to the right of the [[operator]] */ - protected $value; + private $value; /** * SimpleCondition constructor diff --git a/framework/db/conditions/SimpleConditionBuilder.php b/framework/db/conditions/SimpleConditionBuilder.php index 1f98a4faa1..c916d41d1d 100644 --- a/framework/db/conditions/SimpleConditionBuilder.php +++ b/framework/db/conditions/SimpleConditionBuilder.php @@ -41,10 +41,6 @@ class SimpleConditionBuilder implements ExpressionBuilderInterface if ($value instanceof ExpressionInterface) { 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); return "$column $operator $phName"; diff --git a/tests/framework/db/QueryBuilderTest.php b/tests/framework/db/QueryBuilderTest.php index 6adeaf2146..10b706d412 100644 --- a/tests/framework/db/QueryBuilderTest.php +++ b/tests/framework/db/QueryBuilderTest.php @@ -1092,18 +1092,19 @@ abstract class QueryBuilderTest extends DatabaseTestCase // not [['not', 'name'], 'NOT (name)', []], + [['not', (new Query)->select('exists')->from('some_table')], 'NOT ((SELECT [[exists]] FROM [[some_table]]))', []], // and [['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', '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', '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', new Expression('id=:qp0', [':qp0' => 1])], '(type=1) OR (id=:qp0)', [':qp0' => 1]], - // between [['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]], @@ -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]]', []], // 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]], [['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]],