From 7d08a09620bea117b5fa5635f035ec71f32626c8 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 11 Jan 2014 08:35:43 -0500 Subject: [PATCH] Fixes #1809: Added support for building "EXISTS" and "NOT EXISTS" query conditions --- framework/CHANGELOG.md | 1 + framework/db/QueryBuilder.php | 19 +++++ tests/unit/framework/db/QueryBuilderTest.php | 83 ++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 8dbaea7eee..fd54fcd02f 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -55,6 +55,7 @@ Yii Framework 2 Change Log - Enh #1681: Added support for automatically adjusting the "for" attribute of label generated by `ActiveField::label()` (qiangxue) - Enh #1706: Added support for registering a single JS/CSS file with dependency (qiangxue) - Enh #1773: keyPrefix property of Cache is not restricted to alnum characters anymore, however it is still recommended (cebe) +- Enh #1809: Added support for building "EXISTS" and "NOT EXISTS" query conditions (abdrasulov) - Enh #1852: ActiveRecord::tableName() now returns table name using DbConnection::tablePrefix (creocoder) - Enh: Added `favicon.ico` and `robots.txt` to default application templates (samdark) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index f8193626e0..5248d84706 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -799,6 +799,8 @@ class QueryBuilder extends \yii\base\Object 'NOT LIKE' => 'buildLikeCondition', 'OR LIKE' => 'buildLikeCondition', 'OR NOT LIKE' => 'buildLikeCondition', + 'EXISTS' => 'buildExistsCondition', + 'NOT EXISTS' => 'buildExistsCondition', ]; if (!is_array($condition)) { @@ -1071,4 +1073,21 @@ class QueryBuilder extends \yii\base\Object return implode($andor, $parts); } + + /** + * Creates an SQL expressions with the `EXISTS` operator. + * @param string $operator the operator to use (e.g. `EXISTS` or `NOT EXISTS`) + * @param array $operands contains only the one element. It is sub-query + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + */ + public function buildExistsCondition($operator, $operands, &$params) + { + $subQuery = $operands[0]; + list($subQuerySql, $subQueryParams) = $this->build($subQuery); + foreach ($subQueryParams as $name => $value) { + $params[$name] = $value; + } + return "$operator ($subQuerySql)"; + } } diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php index b8eefe0b2e..93e7b8cc5c 100644 --- a/tests/unit/framework/db/QueryBuilderTest.php +++ b/tests/unit/framework/db/QueryBuilderTest.php @@ -2,6 +2,7 @@ namespace yiiunit\framework\db; +use yii\db\Query; use yii\db\QueryBuilder; use yii\db\Schema; use yii\db\mysql\QueryBuilder as MysqlQueryBuilder; @@ -130,4 +131,86 @@ class QueryBuilderTest extends DatabaseTestCase $tableSchema = $qb->db->getSchema()->getTableSchema($tableName); $this->assertEquals(0, count($tableSchema->primaryKey)); } + + public function testBuildWhereExists() + { + $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE EXISTS (SELECT `1` FROM `Website` `w`)"; + $expectedQueryParams = null; + + $subQuery = new Query(); + $subQuery->select('1') + ->from('Website w'); + + $query = new Query(); + $query->select('id') + ->from('TotalExample t') + ->where(['exists', $subQuery]); + + list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $actualQueryParams); + } + + public function testBuildWhereNotExists() + { + $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE NOT EXISTS (SELECT `1` FROM `Website` `w`)"; + $expectedQueryParams = null; + + $subQuery = new Query(); + $subQuery->select('1') + ->from('Website w'); + + $query = new Query(); + $query->select('id') + ->from('TotalExample t') + ->where(['not exists', $subQuery]); + + list($actualQuerySql, $actualQueryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $actualQueryParams); + } + + public function testBuildWhereExistsWithParameters() + { + $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE (EXISTS (SELECT `1` FROM `Website` `w` WHERE (w.id = t.website_id) AND (w.merchant_id = :merchant_id))) AND (t.some_column = :some_value)"; + $expectedQueryParams = [':some_value' => "asd", ':merchant_id' => 6]; + + $subQuery = new Query(); + $subQuery->select('1') + ->from('Website w') + ->where('w.id = t.website_id') + ->andWhere('w.merchant_id = :merchant_id', [':merchant_id' => 6]); + + $query = new Query(); + $query->select('id') + ->from('TotalExample t') + ->where(['exists', $subQuery]) + ->andWhere('t.some_column = :some_value', [':some_value' => "asd"]); + + list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $queryParams); + } + + public function testBuildWhereExistsWithArrayParameters() + { + $expectedQuerySql = "SELECT `id` FROM `TotalExample` `t` WHERE (EXISTS (SELECT `1` FROM `Website` `w` WHERE (w.id = t.website_id) AND ((`w`.`merchant_id`=:qp0) AND (`w`.`user_id`=:qp1)))) AND (`t`.`some_column`=:qp2)"; + $expectedQueryParams = [':qp0' => 6, ':qp1' => 210, ':qp2' => 'asd']; + + $subQuery = new Query(); + $subQuery->select('1') + ->from('Website w') + ->where('w.id = t.website_id') + ->andWhere(['w.merchant_id' => 6, 'w.user_id' => '210']); + + $query = new Query(); + $query->select('id') + ->from('TotalExample t') + ->where(['exists', $subQuery]) + ->andWhere(['t.some_column' => "asd"]); + + list($actualQuerySql, $queryParams) = $this->getQueryBuilder()->build($query); + $this->assertEquals($expectedQuerySql, $actualQuerySql); + $this->assertEquals($expectedQueryParams, $queryParams); + } }