diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 28e6ccefda..8c2f8aab50 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -31,6 +31,7 @@ Yii Framework 2 Change Log - Bug #6107: `yii message` was emptying existing translations in .po in case of multiple categories (samdark) - Bug #6112: `yii message` was incorrectly writing not yet translated strings in .po in case of multiple categories (samdark) - Bug #6172: `yii\rbac\DbManager` should properly quote table and column names (qiangxue) +- Bug #6164: Added missing support for `yii\db\Exression` QueryBuilder `BETWEEN` and `LIKE` conditions (cebe) - Bug: Gii console command help information does not contain global options (qiangxue) - Bug: `yii\web\UrlRule` was unable to create URLs for rules containing unicode characters (samdark) - Enh #4181: Added `yii\bootstrap\Modal::$headerOptions` and `yii\bootstrap\Modal::$footerOptions` (tuxoff, samdark) diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 66f834e82f..561ffc56e5 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -1020,10 +1020,24 @@ class QueryBuilder extends \yii\base\Object if (strpos($column, '(') === false) { $column = $this->db->quoteColumnName($column); } - $phName1 = self::PARAM_PREFIX . count($params); - $params[$phName1] = $value1; - $phName2 = self::PARAM_PREFIX . count($params); - $params[$phName2] = $value2; + if ($value1 instanceof Expression) { + foreach ($value1->params as $n => $v) { + $params[$n] = $v; + } + $phName1 = $value1->expression; + } else { + $phName1 = self::PARAM_PREFIX . count($params); + $params[$phName1] = $value1; + } + if ($value2 instanceof Expression) { + foreach ($value2->params as $n => $v) { + $params[$n] = $v; + } + $phName2 = $value2->expression; + } else { + $phName2 = self::PARAM_PREFIX . count($params); + $params[$phName2] = $value2; + } return "$column $operator $phName1 AND $phName2"; } @@ -1181,7 +1195,9 @@ class QueryBuilder extends \yii\base\Object list($column, $values) = $operands; - $values = (array) $values; + if (!is_array($values)) { + $values = [$values]; + } if (empty($values)) { return $not ? '' : '0=1'; @@ -1193,8 +1209,15 @@ class QueryBuilder extends \yii\base\Object $parts = []; foreach ($values as $value) { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'); + if ($value instanceof Expression) { + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + $phName = $value->expression; + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = empty($escape) ? $value : ('%' . strtr($value, $escape) . '%'); + } $parts[] = "$column $operator $phName"; } diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php index 85d00b602c..86a879ec54 100644 --- a/tests/unit/framework/db/QueryBuilderTest.php +++ b/tests/unit/framework/db/QueryBuilderTest.php @@ -166,6 +166,16 @@ class QueryBuilderTest extends DatabaseTestCase [ ['or like', 'name', ['heyho', 'abc']], '`name` LIKE :qp0 OR `name` LIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%'] ], [ ['or not like', 'name', ['heyho', 'abc']], '`name` NOT LIKE :qp0 OR `name` NOT LIKE :qp1', [':qp0' => '%heyho%', ':qp1' => '%abc%'] ], + // like with Expression + [ ['like', 'name', new Expression('CONCAT("test", colname, "%")')], '`name` LIKE CONCAT("test", colname, "%")', [] ], + [ ['not like', 'name', new Expression('CONCAT("test", colname, "%")')], '`name` NOT LIKE CONCAT("test", colname, "%")', [] ], + [ ['or like', 'name', new Expression('CONCAT("test", colname, "%")')], '`name` LIKE CONCAT("test", colname, "%")', [] ], + [ ['or not like', 'name', new Expression('CONCAT("test", colname, "%")')], '`name` NOT LIKE CONCAT("test", colname, "%")', [] ], + [ ['like', 'name', [new Expression('CONCAT("test", colname, "%")'), 'abc']], '`name` LIKE CONCAT("test", colname, "%") AND `name` LIKE :qp0', [':qp0' => '%abc%'] ], + [ ['not like', 'name', [new Expression('CONCAT("test", colname, "%")'), 'abc']], '`name` NOT LIKE CONCAT("test", colname, "%") AND `name` NOT LIKE :qp0', [':qp0' => '%abc%'] ], + [ ['or like', 'name', [new Expression('CONCAT("test", colname, "%")'), 'abc']], '`name` LIKE CONCAT("test", colname, "%") OR `name` LIKE :qp0', [':qp0' => '%abc%'] ], + [ ['or not like', 'name', [new Expression('CONCAT("test", colname, "%")'), 'abc']], '`name` NOT LIKE CONCAT("test", colname, "%") OR `name` NOT LIKE :qp0', [':qp0' => '%abc%'] ], + // not [ ['not', 'name'], 'NOT (name)', [] ], @@ -177,10 +187,13 @@ class QueryBuilderTest extends DatabaseTestCase [ ['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))', [] ], - // 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] ], + [ ['between', 'date', new Expression('(NOW() - INTERVAL 1 MONTH)'), new Expression('NOW()')], '`date` BETWEEN (NOW() - INTERVAL 1 MONTH) AND NOW()', [] ], + [ ['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] ], // in [ ['in', 'id', [1, 2, 3]], '`id` IN (:qp0, :qp1, :qp2)', [':qp0' => 1, ':qp1' => 2, ':qp2' => 3] ], @@ -194,7 +207,6 @@ class QueryBuilderTest extends DatabaseTestCase [ ['in', ['id', 'name'], (new Query())->select(['id', 'name'])->from('users')->where(['active' => 1])], '(`id`, `name`) IN (SELECT `id`, `name` FROM `users` WHERE `active`=:qp0)', [':qp0' => 1] ], [ ['not in', ['id', 'name'], (new Query())->select(['id', 'name'])->from('users')->where(['active' => 1])], '(`id`, `name`) NOT IN (SELECT `id`, `name` FROM `users` WHERE `active`=:qp0)', [':qp0' => 1] ], - // exists [ ['exists', (new Query())->select('id')->from('users')->where(['active' => 1])], 'EXISTS (SELECT `id` FROM `users` WHERE `active`=:qp0)', [':qp0' => 1] ], [ ['not exists', (new Query())->select('id')->from('users')->where(['active' => 1])], 'NOT EXISTS (SELECT `id` FROM `users` WHERE `active`=:qp0)', [':qp0' => 1] ], @@ -209,6 +221,11 @@ class QueryBuilderTest extends DatabaseTestCase [ ['!=', 'a', 'b'], '`a` != :qp0', [':qp0' => 'b'] ], [ ['>=', 'date', new Expression('DATE_SUB(NOW(), INTERVAL 1 MONTH)')], '`date` >= DATE_SUB(NOW(), INTERVAL 1 MONTH)', [] ], [ ['>=', 'date', new Expression('DATE_SUB(NOW(), INTERVAL :month MONTH)', [':month' => 2])], '`date` >= DATE_SUB(NOW(), INTERVAL :month MONTH)', [':month' => 2] ], + + // hash condition + [ ['a' => 1, 'b' => 2], '(`a`=:qp0) AND (`b`=:qp1)', [':qp0' => 1, ':qp1' => 2] ], + [ ['a' => new Expression('CONCAT(col1, col2)'), 'b' => 2], '(`a`=CONCAT(col1, col2)) AND (`b`=:qp0)', [':qp0' => 2] ], + ]; // adjust dbms specific escaping