diff --git a/docs/guide/db-query-builder.md b/docs/guide/db-query-builder.md index 544c6d7..1086f44 100644 --- a/docs/guide/db-query-builder.md +++ b/docs/guide/db-query-builder.md @@ -255,6 +255,9 @@ the operator can be one of the following: - `between`: operand 1 should be the column name, and operand 2 and 3 should be the starting and ending values of the range that the column is in. For example, `['between', 'id', 1, 10]` will generate `id BETWEEN 1 AND 10`. + In case you need to build a condition where value is between two columns (like `11 BETWEEN min_id AND max_id`), + you should use [[yii\db\conditions\BetweenColumnsCondition|BetweenColumnsCondition]]. + See [Conditions – Object Format](#object-format) chapter to learn more about object definition of conditions. - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` in the generated condition. @@ -796,7 +799,7 @@ $unbufferedDb->close(); ### Adding custom Conditions and Expressions -As it was mentioned in [Conditions – Object Fromat](#object-format) chapter, is is possible to create custom condition +As it was mentioned in [Conditions – Object Format](#object-format) chapter, is is possible to create custom condition classes. For example, let's create a condition that will check that specific columns are less than some value. Using the operator format, it would look like the following: diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 54250f9..0d800d7 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -101,6 +101,7 @@ Yii Framework 2 Change Log - Enh #15476: Added `\yii\widgets\ActiveForm::$validationStateOn` to be able to specify where to add class for invalid fields (samdark) - Enh #15496: CSRF token is now regenerated on changing identity (samdark, rhertogh) - Enh #15595: `yii\data\DataFilter` can now handle `lt`,`gt`,`lte` and `gte` on `yii\validators\DateValidator` (mikk150) +- Enh #11611: Added `BetweenColumnsCondition` to build SQL condition like `value BETWEEN col1 and col2` (silverfire) - Enh: Added check to `yii\base\Model::formName()` to prevent source path disclosure when form is represented by an anonymous class (silverfire) - Chg #15420: Handle OPTIONS request in `yii\filter\Cors` so the preflight check isn't passed trough authentication filters (michaelarnauts, leandrogehlen) - Chg #15625: `yii\grid\DataColumn` boolean filter dropdown list values are now in reversed order (bizley) diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index f4d9810..4d6f012 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -173,6 +173,7 @@ class QueryBuilder extends \yii\base\BaseObject 'yii\db\conditions\ExistsCondition' => 'yii\db\conditions\ExistsConditionBuilder', 'yii\db\conditions\SimpleCondition' => 'yii\db\conditions\SimpleConditionBuilder', 'yii\db\conditions\HashCondition' => 'yii\db\conditions\HashConditionBuilder', + 'yii\db\conditions\BetweenColumnsCondition' => 'yii\db\conditions\BetweenColumnsConditionBuilder', ]; } diff --git a/framework/db/conditions/BetweenColumnsCondition.php b/framework/db/conditions/BetweenColumnsCondition.php new file mode 100644 index 0000000..a662045 --- /dev/null +++ b/framework/db/conditions/BetweenColumnsCondition.php @@ -0,0 +1,115 @@ +select('time')->from('log')->orderBy('id ASC')->limit(1), + * 'update_time' + * ); + * + * // Will be built to: + * // NOW() BETWEEN (SELECT time FROM log ORDER BY id ASC LIMIT 1) AND update_time + * ``` + * + * @author Dmytro Naumenko + * @since 2.0.14 + */ +class BetweenColumnsCondition implements ConditionInterface +{ + /** + * @var string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) + */ + protected $operator; + /** + * @var mixed the value to compare against + */ + protected $value; + /** + * @var string|ExpressionInterface|Query the column name or expression that is a beginning of the interval + */ + protected $intervalStartColumn; + /** + * @var string|ExpressionInterface|Query the column name or expression that is an end of the interval + */ + protected $intervalEndColumn; + + /** + * Creates a condition with the `BETWEEN` operator. + * + * @param mixed the value to compare against + * @param string $operator the operator to use (e.g. `BETWEEN` or `NOT BETWEEN`) + * @param string|ExpressionInterface $intervalStartColumn the column name or expression that is a beginning of the interval + * @param string|ExpressionInterface $intervalEndColumn the column name or expression that is an end of the interval + */ + public function __construct($value, $operator, $intervalStartColumn, $intervalEndColumn) + { + $this->value = $value; + $this->operator = $operator; + $this->intervalStartColumn = $intervalStartColumn; + $this->intervalEndColumn = $intervalEndColumn; + } + + /** + * @return string + */ + public function getOperator() + { + return $this->operator; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * @return string|ExpressionInterface|Query + */ + public function getIntervalStartColumn() + { + return $this->intervalStartColumn; + } + + /** + * @return string|ExpressionInterface|Query + */ + public function getIntervalEndColumn() + { + return $this->intervalEndColumn; + } + + /** + * {@inheritdoc} + * @throws InvalidArgumentException if wrong number of operands have been given. + */ + public static function fromArrayDefinition($operator, $operands) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new InvalidArgumentException("Operator '$operator' requires three operands."); + } + + return new static($operands[0], $operator, $operands[1], $operands[2]); + } +} diff --git a/framework/db/conditions/BetweenColumnsConditionBuilder.php b/framework/db/conditions/BetweenColumnsConditionBuilder.php new file mode 100644 index 0000000..1dc17d7 --- /dev/null +++ b/framework/db/conditions/BetweenColumnsConditionBuilder.php @@ -0,0 +1,75 @@ + + * @since 2.0.14 + */ +class BetweenColumnsConditionBuilder implements ExpressionBuilderInterface +{ + use ExpressionBuilderTrait; + + /** + * Method builds the raw SQL from the $expression that will not be additionally + * escaped or quoted. + * + * @param ExpressionInterface|BetweenColumnsCondition $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 = []) + { + $operator = $expression->getOperator(); + + $startColumn = $this->escapeColumnName($expression->getIntervalStartColumn(), $params); + $endColumn = $this->escapeColumnName($expression->getIntervalEndColumn(), $params); + $value = $this->createPlaceholder($expression->getValue(), $params); + + return "$value $operator $startColumn AND $endColumn"; + } + + /** + * Prepares column name to be used in SQL statement. + * + * @param Query|ExpressionInterface|string $columnName + * @param array $params the binding parameters. + * @return string + */ + protected function escapeColumnName($columnName, &$params = []) + { + if ($columnName instanceof Query) { + list($sql, $params) = $this->queryBuilder->build($columnName, $params); + return "($sql)"; + } elseif ($columnName instanceof ExpressionInterface) { + return $this->queryBuilder->buildExpression($columnName, $params); + } elseif (strpos($columnName, '(') === false) { + return $this->queryBuilder->db->quoteColumnName($columnName); + } + + return $columnName; + } + + /** + * Attaches $value to $params array and returns placeholder. + * + * @param mixed $value + * @param array $params passed by reference + * @return string + */ + protected function createPlaceholder($value, &$params) + { + if ($value instanceof ExpressionInterface) { + return $this->queryBuilder->buildExpression($value, $params); + } + + return $this->queryBuilder->bindParam($value, $params); + } +} diff --git a/tests/framework/db/QueryBuilderTest.php b/tests/framework/db/QueryBuilderTest.php index 10ba9ba..86ebd15 100644 --- a/tests/framework/db/QueryBuilderTest.php +++ b/tests/framework/db/QueryBuilderTest.php @@ -7,6 +7,7 @@ namespace yiiunit\framework\db; +use yii\db\conditions\BetweenColumnsCondition; use yii\db\cubrid\QueryBuilder as CubridQueryBuilder; use yii\db\Expression; use yii\db\mssql\QueryBuilder as MssqlQueryBuilder; @@ -1076,6 +1077,11 @@ abstract class QueryBuilderTest extends DatabaseTestCase [['between', 'date', new Expression('(NOW() - INTERVAL 1 MONTH)'), 123], '[[date]] BETWEEN (NOW() - INTERVAL 1 MONTH) AND :qp0', [':qp0' => 123]], [['not between', 'date', new Expression('(NOW() - INTERVAL 1 MONTH)'), new Expression('NOW()')], '[[date]] NOT BETWEEN (NOW() - INTERVAL 1 MONTH) AND NOW()', []], [['not between', 'date', new Expression('(NOW() - INTERVAL 1 MONTH)'), 123], '[[date]] NOT BETWEEN (NOW() - INTERVAL 1 MONTH) AND :qp0', [':qp0' => 123]], + [new BetweenColumnsCondition('2018-02-11', 'BETWEEN', 'create_time', 'update_time'), ':qp0 BETWEEN [[create_time]] AND [[update_time]]', [':qp0' => '2018-02-11']], + [new BetweenColumnsCondition('2018-02-11', 'NOT BETWEEN', 'NOW()', 'update_time'), ':qp0 NOT BETWEEN NOW() AND [[update_time]]', [':qp0' => '2018-02-11']], + [new BetweenColumnsCondition(new Expression('NOW()'), 'BETWEEN', 'create_time', 'update_time'), 'NOW() BETWEEN [[create_time]] AND [[update_time]]', []], + [new BetweenColumnsCondition(new Expression('NOW()'), 'NOT BETWEEN', 'create_time', 'update_time'), 'NOW() NOT BETWEEN [[create_time]] AND [[update_time]]', []], + [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]],