Commit f59598bb by Qiang Xue

Added support for building SQLs with sub-queries

parent d8f725bd
Query Builder and Query Query Builder and Query
======================= =======================
Yii provides a basic database access layer as described in the [Database basics](database-basics.md) section. The database access layer provides a low-level way to interact with the database. While useful in some situations, it can be tedious to rely too much upon direct SQL. An alternative approach that Yii provides is the Query Builder. The Query Builder provides an object-oriented vehicle for generating queries to be executed. Yii provides a basic database access layer as described in the [Database basics](database-basics.md) section.
The database access layer provides a low-level way to interact with the database. While useful in some situations,
it can be tedious and error-prone to write raw SQLs. An alternative approach is to use the Query Builder.
The Query Builder provides an object-oriented vehicle for generating queries to be executed.
Here's a basic example: A typical usage of the query builder looks like the following:
```php ```php
$query = new Query; $rows = (new \yii\db\Query)
->select('id, name')
->from('tbl_user')
->limit(10)
->createCommand()
->queryAll();
// which is equivalent to the following code:
// Define the query: $query = new \yii\db\Query;
$query->select('id, name') $query->select('id, name')
->from('tbl_user') ->from('tbl_user')
->limit(10); ->limit(10);
...@@ -21,8 +31,12 @@ $command = $query->createCommand(); ...@@ -21,8 +31,12 @@ $command = $query->createCommand();
$rows = $command->queryAll(); $rows = $command->queryAll();
``` ```
Basic selects In the following, we will explain how to build various clauses in a SQL statement. For simplicity,
------------- we use `$query` to represent a [[yii\db\Query]] object.
`SELECT`
--------
In order to form a basic `SELECT` query, you need to specify what columns to select and from what table: In order to form a basic `SELECT` query, you need to specify what columns to select and from what table:
...@@ -31,39 +45,82 @@ $query->select('id, name') ...@@ -31,39 +45,82 @@ $query->select('id, name')
->from('tbl_user'); ->from('tbl_user');
``` ```
Select options can be specified as a comma-separated string, as in the above, or as an array. The array syntax is especially useful when forming the selection dynamically: Select options can be specified as a comma-separated string, as in the above, or as an array.
The array syntax is especially useful when forming the selection dynamically:
```php ```php
$columns = []; $query->select(['id', 'name'])
$columns[] = 'id';
$columns[] = 'name';
$query->select($columns)
->from('tbl_user'); ->from('tbl_user');
``` ```
> Info: If your `SELECT` contains SQL expressions which use commas (e.g. `CONCAT(first_name, last_name) AS full_name`), > Info: You should always use the array format if your `SELECT` clause contains SQL expressions.
> you should use an array instead of a string to represent the columns being selected. Your SQL expressions may be > This is because a SQL expression like `CONCAT(first_name, last_name) AS full_name` may contain commas.
> split by commas into several parts. > If you list it together with other columns in a string, the expression may be split into several parts
> by commas, which is not what you want to see.
When specifying columns, you may include the table prefixes or column aliases, e.g., `tbl_user.id`, `tbl_user.id AS user_id`.
If you are using array to specify the columns, you may also use the array keys to specify the column aliases,
e.g., `['user_id' => 'tbl_user.id', 'user_name' => 'tbl_user.name']`.
To select distinct rows, you may call `distinct()`, like the following:
```php
$query->select('user_id')->distinct()->from('tbl_post');
```
`FROM`
------
To specify which table(s) to select data from, call `from()`:
```php
$query->select('*')->from('tbl_user');
```
You may specify multiple tables using a comma-separated string or an array.
Table names can contain schema prefixes (e.g. `'public.tbl_user'`) and/or table aliases (e.g. `'tbl_user u'`).
The method will automatically quote the table names unless it contains some parenthesis
(which means the table is given as a sub-query or DB expression). For example,
```php
$query->select('u.*, p.*')->from(['tbl_user u', 'tbl_post p']);
```
Joins When the tables are specified as an array, you may also use the array keys as the table aliases
(if a table does not need alias, do not use a string key). For example,
```php
$query->select('u.*, p.*')->from(['u' => 'tbl_user u', 'p' => 'tbl_post']);
```
You may specify a sub-query using a `Query` object. In this case, the corresponding array key will be used
as the alias for the sub-query.
```php
$subQuery = (new Query)->select('id')->from('tbl_user')->where('status=1');
$query->select('*')->from(['u' => $subQuery]);
```
`JOIN`
----- -----
Joins are generated in the Query Builder by using the applicable join method: The `JOIN` clauses are generated in the Query Builder by using the applicable join method:
- `innerJoin` - `innerJoin()`
- `leftJoin` - `leftJoin()`
- `rightJoin` - `rightJoin()`
This left join selects data from two related tables in one query: This left join selects data from two related tables in one query:
```php ```php
$query->select(['tbl_user.name AS author', 'tbl_post.title as title']) ->from('tbl_user') $query->select(['tbl_user.name AS author', 'tbl_post.title as title'])
->from('tbl_user')
->leftJoin('tbl_post', 'tbl_post.user_id = tbl_user.id'); ->leftJoin('tbl_post', 'tbl_post.user_id = tbl_user.id');
``` ```
In the code, the `leftJion` method's first parameter In the code, the `leftJoin()` method's first parameter
specifies the table to join to. The second paramter defines the join condition. specifies the table to join to. The second parameter defines the join condition.
If your database application supports other join types, you can use those via the generic `join` method: If your database application supports other join types, you can use those via the generic `join` method:
...@@ -73,8 +130,16 @@ $query->join('FULL OUTER JOIN', 'tbl_post', 'tbl_post.user_id = tbl_user.id'); ...@@ -73,8 +130,16 @@ $query->join('FULL OUTER JOIN', 'tbl_post', 'tbl_post.user_id = tbl_user.id');
The first argument is the join type to perform. The second is the table to join to, and the third is the condition. The first argument is the join type to perform. The second is the table to join to, and the third is the condition.
Specifying SELECT conditions Like `FROM`, you may also join with sub-queries. To do so, specify the sub-query as an array
--------------------- which must contain one element. The array value must be a `Query` object representing the sub-query,
while the array key is the alias for the sub-query. For example,
```php
$query->leftJoin(['u' => $subQuery], 'u.id=author_id');
```
`WHERE`
-------
Usually data is selected based upon certain criteria. Query Builder has some useful methods to specify these, the most powerful of which being `where`. It can be used in multiple ways. Usually data is selected based upon certain criteria. Query Builder has some useful methods to specify these, the most powerful of which being `where`. It can be used in multiple ways.
...@@ -185,7 +250,7 @@ In case `$search` isn't empty the following SQL will be generated: ...@@ -185,7 +250,7 @@ In case `$search` isn't empty the following SQL will be generated:
WHERE (`status` = 10) AND (`title` LIKE '%yii%') WHERE (`status` = 10) AND (`title` LIKE '%yii%')
``` ```
Order `ORDER BY`
----- -----
For ordering results `orderBy` and `addOrderBy` could be used: For ordering results `orderBy` and `addOrderBy` could be used:
...@@ -199,13 +264,6 @@ $query->orderBy([ ...@@ -199,13 +264,6 @@ $query->orderBy([
Here we are ordering by `id` ascending and then by `name` descending. Here we are ordering by `id` ascending and then by `name` descending.
Distinct
--------
If you want to get IDs of all users with posts you can use `DISTINCT`. With query builder it will look like the following:
```php
$query->select('user_id')->distinct()->from('tbl_post');
``` ```
Group and Having Group and Having
......
...@@ -165,20 +165,11 @@ class ActiveQuery extends Query implements ActiveQueryInterface ...@@ -165,20 +165,11 @@ class ActiveQuery extends Query implements ActiveQueryInterface
*/ */
public function createCommand($db = null) public function createCommand($db = null)
{ {
/** @var $modelClass ActiveRecord */
$modelClass = $this->modelClass;
$this->setConnection($db); $this->setConnection($db);
$db = $this->getConnection(); $db = $this->getConnection();
$params = $this->params; $params = $this->params;
if ($this->sql === null) { if ($this->sql === null) {
if ($this->from === null) {
$tableName = $modelClass::indexName();
if ($this->select === null && !empty($this->join)) {
$this->select = ["$tableName.*"];
}
$this->from = [$tableName];
}
list ($this->sql, $params) = $db->getQueryBuilder()->build($this); list ($this->sql, $params) = $db->getQueryBuilder()->build($this);
} }
return $db->createCommand($this->sql, $params); return $db->createCommand($this->sql, $params);
......
...@@ -97,7 +97,7 @@ class Query extends Component implements QueryInterface ...@@ -97,7 +97,7 @@ class Query extends Component implements QueryInterface
* @var array list of query parameter values indexed by parameter placeholders. * @var array list of query parameter values indexed by parameter placeholders.
* For example, `[':name' => 'Dan', ':age' => 31]`. * For example, `[':name' => 'Dan', ':age' => 31]`.
*/ */
public $params; public $params = [];
/** /**
* @var callback PHP callback, which should be used to fetch source data for the snippets. * @var callback PHP callback, which should be used to fetch source data for the snippets.
* Such callback will receive array of query result rows as an argument and must return the * Such callback will receive array of query result rows as an argument and must return the
...@@ -559,7 +559,7 @@ class Query extends Component implements QueryInterface ...@@ -559,7 +559,7 @@ class Query extends Component implements QueryInterface
public function addParams($params) public function addParams($params)
{ {
if (!empty($params)) { if (!empty($params)) {
if ($this->params === null) { if (empty($this->params)) {
$this->params = $params; $this->params = $params;
} else { } else {
foreach ($params as $name => $value) { foreach ($params as $name => $value) {
......
...@@ -52,20 +52,33 @@ class QueryBuilder extends Object ...@@ -52,20 +52,33 @@ class QueryBuilder extends Object
/** /**
* Generates a SELECT SQL statement from a [[Query]] object. * Generates a SELECT SQL statement from a [[Query]] object.
* @param Query $query the [[Query]] object from which the SQL statement will be generated * @param Query $query the [[Query]] object from which the SQL statement will be generated
* @param array $params the parameters to be bound to the generated SQL statement. These parameters will
* be included in the result with the additional parameters generated during the query building process.
* @return array the generated SQL statement (the first array element) and the corresponding * @return array the generated SQL statement (the first array element) and the corresponding
* parameters to be bound to the SQL statement (the second array element). * parameters to be bound to the SQL statement (the second array element). The parameters returned
* include those provided in `$params`.
*/ */
public function build($query) public function build($query, $params = [])
{ {
$params = $query->params; $params = empty($params) ? $query->params : array_merge($params, $query->params);
if ($query->match !== null) { if ($query->match !== null) {
$phName = self::PARAM_PREFIX . count($params); $phName = self::PARAM_PREFIX . count($params);
$params[$phName] = (string)$query->match; $params[$phName] = (string)$query->match;
$query->andWhere('MATCH(' . $phName . ')'); $query->andWhere('MATCH(' . $phName . ')');
} }
$from = $query->from;
if ($from === null && $query instanceof ActiveQuery) {
/** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$tableName = $modelClass::indexName();
$from = [$tableName];
}
$clauses = [ $clauses = [
$this->buildSelect($query->select, $query->distinct, $query->selectOption), $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
$this->buildFrom($query->from), $this->buildFrom($from, $Params),
$this->buildWhere($query->from, $query->where, $params), $this->buildWhere($query->from, $query->where, $params),
$this->buildGroupBy($query->groupBy), $this->buildGroupBy($query->groupBy),
$this->buildWithin($query->within), $this->buildWithin($query->within),
...@@ -157,11 +170,11 @@ class QueryBuilder extends Object ...@@ -157,11 +170,11 @@ class QueryBuilder extends Object
* For example, * For example,
* *
* ~~~ * ~~~
* $connection->createCommand()->batchInsert('idx_user', ['id', 'name', 'age'], [ * $sql = $queryBuilder->batchInsert('idx_user', ['id', 'name', 'age'], [
* [1, 'Tom', 30], * [1, 'Tom', 30],
* [2, 'Jane', 20], * [2, 'Jane', 20],
* [3, 'Linda', 25], * [3, 'Linda', 25],
* ])->execute(); * ], $params);
* ~~~ * ~~~
* *
* Note that the values in each row must match the corresponding column names. * Note that the values in each row must match the corresponding column names.
...@@ -183,11 +196,11 @@ class QueryBuilder extends Object ...@@ -183,11 +196,11 @@ class QueryBuilder extends Object
* For example, * For example,
* *
* ~~~ * ~~~
* $connection->createCommand()->batchReplace('idx_user', ['id', 'name', 'age'], [ * $sql = $queryBuilder->batchReplace('idx_user', ['id', 'name', 'age'], [
* [1, 'Tom', 30], * [1, 'Tom', 30],
* [2, 'Jane', 20], * [2, 'Jane', 20],
* [3, 'Linda', 25], * [3, 'Linda', 25],
* ])->execute(); * ], $params);
* ~~~ * ~~~
* *
* Note that the values in each row must match the corresponding column names. * Note that the values in each row must match the corresponding column names.
...@@ -386,11 +399,12 @@ class QueryBuilder extends Object ...@@ -386,11 +399,12 @@ class QueryBuilder extends Object
/** /**
* @param array $columns * @param array $columns
* @param array $params the binding parameters to be populated
* @param boolean $distinct * @param boolean $distinct
* @param string $selectOption * @param string $selectOption
* @return string the SELECT clause built from [[query]]. * @return string the SELECT clause built from [[query]].
*/ */
public function buildSelect($columns, $distinct = false, $selectOption = null) public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
{ {
$select = $distinct ? 'SELECT DISTINCT' : 'SELECT'; $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
if ($selectOption !== null) { if ($selectOption !== null) {
...@@ -402,8 +416,14 @@ class QueryBuilder extends Object ...@@ -402,8 +416,14 @@ class QueryBuilder extends Object
} }
foreach ($columns as $i => $column) { foreach ($columns as $i => $column) {
if (is_object($column)) { if ($column instanceof Expression) {
$columns[$i] = (string)$column; $columns[$i] = $column->expression;
$params = array_merge($params, $column->params);
} elseif (is_string($i)) {
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$columns[$i] = "$column AS " . $this->db->quoteColumnName($i);;
} elseif (strpos($column, '(') === false) { } elseif (strpos($column, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) { if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
$columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]); $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]);
...@@ -413,25 +433,30 @@ class QueryBuilder extends Object ...@@ -413,25 +433,30 @@ class QueryBuilder extends Object
} }
} }
if (is_array($columns)) { return $select . ' ' . implode(', ', $columns);
$columns = implode(', ', $columns);
}
return $select . ' ' . $columns;
} }
/** /**
* @param array $indexes * @param array $indexes
* @param array $params the binding parameters to be populated
* @return string the FROM clause built from [[query]]. * @return string the FROM clause built from [[query]].
*/ */
public function buildFrom($indexes) public function buildFrom($indexes, &$params)
{ {
if (empty($indexes)) { if (empty($indexes)) {
return ''; return '';
} }
foreach ($indexes as $i => $index) { foreach ($indexes as $i => $index) {
if (strpos($index, '(') === false) { if ($index instanceof Query) {
list($sql, $params) = $this->build($index, $params);
$indexes[$i] = "($sql) " . $this->db->quoteIndexName($i);
} elseif (is_string($i)) {
if (strpos($index, '(') === false) {
$index = $this->db->quoteIndexName($index);
}
$indexes[$i] = "$index " . $this->db->quoteIndexName($i);
} elseif (strpos($index, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $index, $matches)) { // with alias if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $index, $matches)) { // with alias
$indexes[$i] = $this->db->quoteIndexName($matches[1]) . ' ' . $this->db->quoteIndexName($matches[2]); $indexes[$i] = $this->db->quoteIndexName($matches[1]) . ' ' . $this->db->quoteIndexName($matches[2]);
} else { } else {
...@@ -491,8 +516,8 @@ class QueryBuilder extends Object ...@@ -491,8 +516,8 @@ class QueryBuilder extends Object
} }
$orders = []; $orders = [];
foreach ($columns as $name => $direction) { foreach ($columns as $name => $direction) {
if (is_object($direction)) { if ($direction instanceof Expression) {
$orders[] = (string)$direction; $orders[] = $direction->expression;
} else { } else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : 'ASC'); $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : 'ASC');
} }
...@@ -537,8 +562,8 @@ class QueryBuilder extends Object ...@@ -537,8 +562,8 @@ class QueryBuilder extends Object
} }
} }
foreach ($columns as $i => $column) { foreach ($columns as $i => $column) {
if (is_object($column)) { if ($column instanceof Expression) {
$columns[$i] = (string)$column; $columns[$i] = $column->expression;
} elseif (strpos($column, '(') === false) { } elseif (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column); $columns[$i] = $this->db->quoteColumnName($column);
} }
...@@ -823,8 +848,8 @@ class QueryBuilder extends Object ...@@ -823,8 +848,8 @@ class QueryBuilder extends Object
} }
$orders = []; $orders = [];
foreach ($columns as $name => $direction) { foreach ($columns as $name => $direction) {
if (is_object($direction)) { if ($direction instanceof Expression) {
$orders[] = (string)$direction; $orders[] = $direction->expression;
} else { } else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
} }
......
...@@ -105,6 +105,10 @@ Yii Framework 2 Change Log ...@@ -105,6 +105,10 @@ Yii Framework 2 Change Log
- Enh #2132: Allow url of CSS and JS files registered in yii\web\View to be url alias (cebe) - Enh #2132: Allow url of CSS and JS files registered in yii\web\View to be url alias (cebe)
- Enh #2144: `Html` helper now supports rendering "data" attributes (qiangxue) - Enh #2144: `Html` helper now supports rendering "data" attributes (qiangxue)
- Enh #2156: `yii migrate` now automatically creates `migrations` directory if it does not exist (samdark) - Enh #2156: `yii migrate` now automatically creates `migrations` directory if it does not exist (samdark)
- Enh:#2211: Added typecast database types into php types (dizews)
- Enh #2240: Improved `yii\web\AssetManager::publish()`, `yii\web\AssetManager::getPublishedPath()` and `yii\web\AssetManager::getPublishedUrl()` to support aliases (vova07)
- Enh #2325: Adding support for the `X-HTTP-Method-Override` header in `yii\web\Request::getMethod()` (pawzar)
- Enh #2364: Take into account current error reporting level in error handler (gureedo)
- Enh: Added support for using arrays as option values for console commands (qiangxue) - Enh: Added support for using arrays as option values for console commands (qiangxue)
- Enh: Added `favicon.ico` and `robots.txt` to default application templates (samdark) - Enh: Added `favicon.ico` and `robots.txt` to default application templates (samdark)
- Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue) - Enh: Added `Widget::autoIdPrefix` to support prefixing automatically generated widget IDs (qiangxue)
...@@ -122,10 +126,7 @@ Yii Framework 2 Change Log ...@@ -122,10 +126,7 @@ Yii Framework 2 Change Log
- Enh: Added `yii\web\View::POS_LOAD` (qiangxue) - Enh: Added `yii\web\View::POS_LOAD` (qiangxue)
- Enh: Added `yii\web\Response::clearOutputBuffers()` (qiangxue) - Enh: Added `yii\web\Response::clearOutputBuffers()` (qiangxue)
- Enh: Improved `QueryBuilder::buildLimit()` to support big numbers (qiangxue) - Enh: Improved `QueryBuilder::buildLimit()` to support big numbers (qiangxue)
- Enh:#2211: Added typecast database types into php types (dizews) - Enh: Added support for building SQLs with sub-queries (qiangxue)
- Enh #2240: Improved `yii\web\AssetManager::publish()`, `yii\web\AssetManager::getPublishedPath()` and `yii\web\AssetManager::getPublishedUrl()` to support aliases (vova07)
- Enh #2325: Adding support for the `X-HTTP-Method-Override` header in `yii\web\Request::getMethod()` (pawzar)
- Enh #2364: Take into account current error reporting level in error handler (gureedo)
- Chg #1519: `yii\web\User::loginRequired()` now returns the `Response` object instead of exiting the application (qiangxue) - Chg #1519: `yii\web\User::loginRequired()` now returns the `Response` object instead of exiting the application (qiangxue)
- Chg #1586: `QueryBuilder::buildLikeCondition()` will now escape special characters and use percentage characters by default (qiangxue) - Chg #1586: `QueryBuilder::buildLikeCondition()` will now escape special characters and use percentage characters by default (qiangxue)
- Chg #1610: `Html::activeCheckboxList()` and `Html::activeRadioList()` will submit an empty string if no checkbox/radio is selected (qiangxue) - Chg #1610: `Html::activeCheckboxList()` and `Html::activeRadioList()` will submit an empty string if no checkbox/radio is selected (qiangxue)
......
...@@ -176,20 +176,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface ...@@ -176,20 +176,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
} }
if ($this->sql === null) { if ($this->sql === null) {
$select = $this->select;
$from = $this->from;
if ($this->from === null) {
$tableName = $modelClass::tableName();
if ($this->select === null && !empty($this->join)) {
$this->select = ["$tableName.*"];
}
$this->from = [$tableName];
}
list ($sql, $params) = $db->getQueryBuilder()->build($this); list ($sql, $params) = $db->getQueryBuilder()->build($this);
$this->select = $select;
$this->from = $from;
} else { } else {
$sql = $this->sql; $sql = $this->sql;
$params = $this->params; $params = $this->params;
......
...@@ -105,7 +105,7 @@ class Query extends Component implements QueryInterface ...@@ -105,7 +105,7 @@ class Query extends Component implements QueryInterface
* @var array list of query parameter values indexed by parameter placeholders. * @var array list of query parameter values indexed by parameter placeholders.
* For example, `[':name' => 'Dan', ':age' => 31]`. * For example, `[':name' => 'Dan', ':age' => 31]`.
*/ */
public $params; public $params = [];
/** /**
...@@ -298,6 +298,9 @@ class Query extends Component implements QueryInterface ...@@ -298,6 +298,9 @@ class Query extends Component implements QueryInterface
* *
* Note that if you are selecting an expression like `CONCAT(first_name, ' ', last_name)`, you should * Note that if you are selecting an expression like `CONCAT(first_name, ' ', last_name)`, you should
* use an array to specify the columns. Otherwise, the expression may be incorrectly split into several parts. * use an array to specify the columns. Otherwise, the expression may be incorrectly split into several parts.
*
* When the columns are specified as an array, you may also use array keys as the column aliases (if a column
* does not need alias, do not use a string key).
* *
* @param string $option additional option that should be appended to the 'SELECT' keyword. For example, * @param string $option additional option that should be appended to the 'SELECT' keyword. For example,
* in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used. * in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used.
...@@ -331,6 +334,13 @@ class Query extends Component implements QueryInterface ...@@ -331,6 +334,13 @@ class Query extends Component implements QueryInterface
* Table names can contain schema prefixes (e.g. `'public.tbl_user'`) and/or table aliases (e.g. `'tbl_user u'`). * Table names can contain schema prefixes (e.g. `'public.tbl_user'`) and/or table aliases (e.g. `'tbl_user u'`).
* The method will automatically quote the table names unless it contains some parenthesis * The method will automatically quote the table names unless it contains some parenthesis
* (which means the table is given as a sub-query or DB expression). * (which means the table is given as a sub-query or DB expression).
*
* When the tables are specified as an array, you may also use the array keys as the table aliases
* (if a table does not need alias, do not use a string key).
*
* Use a Query object to represent a sub-query. In this case, the corresponding array key will be used
* as the alias for the sub-query.
*
* @return static the query object itself * @return static the query object itself
*/ */
public function from($tables) public function from($tables)
...@@ -471,10 +481,17 @@ class Query extends Component implements QueryInterface ...@@ -471,10 +481,17 @@ class Query extends Component implements QueryInterface
* Appends a JOIN part to the query. * Appends a JOIN part to the query.
* The first parameter specifies what type of join it is. * The first parameter specifies what type of join it is.
* @param string $type the type of join, such as INNER JOIN, LEFT JOIN. * @param string $type the type of join, such as INNER JOIN, LEFT JOIN.
* @param string $table the table to be joined. * @param string|array $table the table to be joined.
*
* Use string to represent the name of the table to be joined.
* Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u').
* The method will automatically quote the table name unless it contains some parenthesis * The method will automatically quote the table name unless it contains some parenthesis
* (which means the table is given as a sub-query or DB expression). * (which means the table is given as a sub-query or DB expression).
*
* Use array to represent joining with a sub-query. The array must contain only one element.
* The value must be a Query object representing the sub-query while the corresponding key
* represents the alias for the sub-query.
*
* @param string|array $on the join condition that should appear in the ON part. * @param string|array $on the join condition that should appear in the ON part.
* Please refer to [[where()]] on how to specify this parameter. * Please refer to [[where()]] on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query. * @param array $params the parameters (name => value) to be bound to the query.
...@@ -488,10 +505,17 @@ class Query extends Component implements QueryInterface ...@@ -488,10 +505,17 @@ class Query extends Component implements QueryInterface
/** /**
* Appends an INNER JOIN part to the query. * Appends an INNER JOIN part to the query.
* @param string $table the table to be joined. * @param string|array $table the table to be joined.
*
* Use string to represent the name of the table to be joined.
* Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u').
* The method will automatically quote the table name unless it contains some parenthesis * The method will automatically quote the table name unless it contains some parenthesis
* (which means the table is given as a sub-query or DB expression). * (which means the table is given as a sub-query or DB expression).
*
* Use array to represent joining with a sub-query. The array must contain only one element.
* The value must be a Query object representing the sub-query while the corresponding key
* represents the alias for the sub-query.
*
* @param string|array $on the join condition that should appear in the ON part. * @param string|array $on the join condition that should appear in the ON part.
* Please refer to [[where()]] on how to specify this parameter. * Please refer to [[where()]] on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query. * @param array $params the parameters (name => value) to be bound to the query.
...@@ -505,10 +529,17 @@ class Query extends Component implements QueryInterface ...@@ -505,10 +529,17 @@ class Query extends Component implements QueryInterface
/** /**
* Appends a LEFT OUTER JOIN part to the query. * Appends a LEFT OUTER JOIN part to the query.
* @param string $table the table to be joined. * @param string|array $table the table to be joined.
*
* Use string to represent the name of the table to be joined.
* Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u').
* The method will automatically quote the table name unless it contains some parenthesis * The method will automatically quote the table name unless it contains some parenthesis
* (which means the table is given as a sub-query or DB expression). * (which means the table is given as a sub-query or DB expression).
*
* Use array to represent joining with a sub-query. The array must contain only one element.
* The value must be a Query object representing the sub-query while the corresponding key
* represents the alias for the sub-query.
*
* @param string|array $on the join condition that should appear in the ON part. * @param string|array $on the join condition that should appear in the ON part.
* Please refer to [[where()]] on how to specify this parameter. * Please refer to [[where()]] on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query * @param array $params the parameters (name => value) to be bound to the query
...@@ -522,10 +553,17 @@ class Query extends Component implements QueryInterface ...@@ -522,10 +553,17 @@ class Query extends Component implements QueryInterface
/** /**
* Appends a RIGHT OUTER JOIN part to the query. * Appends a RIGHT OUTER JOIN part to the query.
* @param string $table the table to be joined. * @param string|array $table the table to be joined.
*
* Use string to represent the name of the table to be joined.
* Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u'). * Table name can contain schema prefix (e.g. 'public.tbl_user') and/or table alias (e.g. 'tbl_user u').
* The method will automatically quote the table name unless it contains some parenthesis * The method will automatically quote the table name unless it contains some parenthesis
* (which means the table is given as a sub-query or DB expression). * (which means the table is given as a sub-query or DB expression).
*
* Use array to represent joining with a sub-query. The array must contain only one element.
* The value must be a Query object representing the sub-query while the corresponding key
* represents the alias for the sub-query.
*
* @param string|array $on the join condition that should appear in the ON part. * @param string|array $on the join condition that should appear in the ON part.
* Please refer to [[where()]] on how to specify this parameter. * Please refer to [[where()]] on how to specify this parameter.
* @param array $params the parameters (name => value) to be bound to the query * @param array $params the parameters (name => value) to be bound to the query
...@@ -670,7 +708,7 @@ class Query extends Component implements QueryInterface ...@@ -670,7 +708,7 @@ class Query extends Component implements QueryInterface
public function addParams($params) public function addParams($params)
{ {
if (!empty($params)) { if (!empty($params)) {
if ($this->params === null) { if (empty($this->params)) {
$this->params = $params; $this->params = $params;
} else { } else {
foreach ($params as $name => $value) { foreach ($params as $name => $value) {
......
...@@ -55,16 +55,32 @@ class QueryBuilder extends \yii\base\Object ...@@ -55,16 +55,32 @@ class QueryBuilder extends \yii\base\Object
/** /**
* Generates a SELECT SQL statement from a [[Query]] object. * Generates a SELECT SQL statement from a [[Query]] object.
* @param Query $query the [[Query]] object from which the SQL statement will be generated * @param Query $query the [[Query]] object from which the SQL statement will be generated.
* @param array $params the parameters to be bound to the generated SQL statement. These parameters will
* be included in the result with the additional parameters generated during the query building process.
* @return array the generated SQL statement (the first array element) and the corresponding * @return array the generated SQL statement (the first array element) and the corresponding
* parameters to be bound to the SQL statement (the second array element). * parameters to be bound to the SQL statement (the second array element). The parameters returned
* include those provided in `$params`.
*/ */
public function build($query) public function build($query, $params = [])
{ {
$params = $query->params; $params = empty($params) ? $query->params : array_merge($params, $query->params);
$select = $query->select;
$from = $query->from;
if ($from === null && $query instanceof ActiveQuery) {
/** @var ActiveRecord $modelClass */
$modelClass = $query->modelClass;
$tableName = $modelClass::tableName();
$from = [$tableName];
if ($select === null && !empty($query->join)) {
$select = ["$tableName.*"];
}
}
$clauses = [ $clauses = [
$this->buildSelect($query->select, $query->distinct, $query->selectOption), $this->buildSelect($select, $params, $query->distinct, $query->selectOption),
$this->buildFrom($query->from), $this->buildFrom($from, $params),
$this->buildJoin($query->join, $params), $this->buildJoin($query->join, $params),
$this->buildWhere($query->where, $params), $this->buildWhere($query->where, $params),
$this->buildGroupBy($query->groupBy), $this->buildGroupBy($query->groupBy),
...@@ -128,11 +144,11 @@ class QueryBuilder extends \yii\base\Object ...@@ -128,11 +144,11 @@ class QueryBuilder extends \yii\base\Object
* For example, * For example,
* *
* ~~~ * ~~~
* $connection->createCommand()->batchInsert('tbl_user', ['name', 'age'], [ * $sql = $queryBuilder->batchInsert('tbl_user', ['name', 'age'], [
* ['Tom', 30], * ['Tom', 30],
* ['Jane', 20], * ['Jane', 20],
* ['Linda', 25], * ['Linda', 25],
* ])->execute(); * ]);
* ~~~ * ~~~
* *
* Note that the values in each row must match the corresponding column names. * Note that the values in each row must match the corresponding column names.
...@@ -559,11 +575,12 @@ class QueryBuilder extends \yii\base\Object ...@@ -559,11 +575,12 @@ class QueryBuilder extends \yii\base\Object
/** /**
* @param array $columns * @param array $columns
* @param array $params the binding parameters to be populated
* @param boolean $distinct * @param boolean $distinct
* @param string $selectOption * @param string $selectOption
* @return string the SELECT clause built from [[Query::$select]]. * @return string the SELECT clause built from [[Query::$select]].
*/ */
public function buildSelect($columns, $distinct = false, $selectOption = null) public function buildSelect($columns, &$params, $distinct = false, $selectOption = null)
{ {
$select = $distinct ? 'SELECT DISTINCT' : 'SELECT'; $select = $distinct ? 'SELECT DISTINCT' : 'SELECT';
if ($selectOption !== null) { if ($selectOption !== null) {
...@@ -575,8 +592,14 @@ class QueryBuilder extends \yii\base\Object ...@@ -575,8 +592,14 @@ class QueryBuilder extends \yii\base\Object
} }
foreach ($columns as $i => $column) { foreach ($columns as $i => $column) {
if (is_object($column)) { if ($column instanceof Expression) {
$columns[$i] = (string)$column; $columns[$i] = $column->expression;
$params = array_merge($params, $column->params);
} elseif (is_string($i)) {
if (strpos($column, '(') === false) {
$column = $this->db->quoteColumnName($column);
}
$columns[$i] = "$column AS " . $this->db->quoteColumnName($i);;
} elseif (strpos($column, '(') === false) { } elseif (strpos($column, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) { if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $column, $matches)) {
$columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]); $columns[$i] = $this->db->quoteColumnName($matches[1]) . ' AS ' . $this->db->quoteColumnName($matches[2]);
...@@ -586,25 +609,30 @@ class QueryBuilder extends \yii\base\Object ...@@ -586,25 +609,30 @@ class QueryBuilder extends \yii\base\Object
} }
} }
if (is_array($columns)) { return $select . ' ' . implode(', ', $columns);
$columns = implode(', ', $columns);
}
return $select . ' ' . $columns;
} }
/** /**
* @param array $tables * @param array $tables
* @param array $params the binding parameters to be populated
* @return string the FROM clause built from [[Query::$from]]. * @return string the FROM clause built from [[Query::$from]].
*/ */
public function buildFrom($tables) public function buildFrom($tables, &$params)
{ {
if (empty($tables)) { if (empty($tables)) {
return ''; return '';
} }
foreach ($tables as $i => $table) { foreach ($tables as $i => $table) {
if (strpos($table, '(') === false) { if ($table instanceof Query) {
list($sql, $params) = $this->build($table, $params);
$tables[$i] = "($sql) " . $this->db->quoteTableName($i);
} elseif (is_string($i)) {
if (strpos($table, '(') === false) {
$table = $this->db->quoteTableName($table);
}
$tables[$i] = "$table " . $this->db->quoteTableName($i);
} elseif (strpos($table, '(') === false) {
if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias
$tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); $tables[$i] = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]);
} else { } else {
...@@ -613,11 +641,7 @@ class QueryBuilder extends \yii\base\Object ...@@ -613,11 +641,7 @@ class QueryBuilder extends \yii\base\Object
} }
} }
if (is_array($tables)) { return 'FROM ' . implode(', ', $tables);
$tables = implode(', ', $tables);
}
return 'FROM ' . $tables;
} }
/** /**
...@@ -633,27 +657,32 @@ class QueryBuilder extends \yii\base\Object ...@@ -633,27 +657,32 @@ class QueryBuilder extends \yii\base\Object
} }
foreach ($joins as $i => $join) { foreach ($joins as $i => $join) {
if (is_object($join)) { if (!is_array($join) || !isset($join[0], $join[1])) {
$joins[$i] = (string)$join; throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.');
} elseif (is_array($join) && isset($join[0], $join[1])) { }
// 0:join type, 1:table name, 2:on-condition // 0:join type, 1:join table, 2:on-condition (optional)
$table = $join[1]; list ($joinType, $table) = $join;
if (strpos($table, '(') === false) { if (is_array($table)) {
if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias $query = reset($table);
$table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]); if (!$query instanceof Query) {
} else { throw new Exception('The sub-query for join must be an instance of yii\db\Query.');
$table = $this->db->quoteTableName($table);
}
} }
$joins[$i] = $join[0] . ' ' . $table; $alias = $this->db->quoteTableName(key($table));
if (isset($join[2])) { list ($sql, $params) = $this->build($query, $params);
$condition = $this->buildCondition($join[2], $params); $table = "($sql) $alias";
if ($condition !== '') { } elseif (strpos($table, '(') === false) {
$joins[$i] .= ' ON ' . $condition; if (preg_match('/^(.*?)(?i:\s+as|)\s+([^ ]+)$/', $table, $matches)) { // with alias
} $table = $this->db->quoteTableName($matches[1]) . ' ' . $this->db->quoteTableName($matches[2]);
} else {
$table = $this->db->quoteTableName($table);
}
}
$joins[$i] = "$joinType $table";
if (isset($join[2])) {
$condition = $this->buildCondition($join[2], $params);
if ($condition !== '') {
$joins[$i] .= ' ON ' . $condition;
} }
} else {
throw new Exception('A join clause must be specified as an array of join type, join table, and optionally join condition.');
} }
} }
...@@ -702,8 +731,8 @@ class QueryBuilder extends \yii\base\Object ...@@ -702,8 +731,8 @@ class QueryBuilder extends \yii\base\Object
} }
$orders = []; $orders = [];
foreach ($columns as $name => $direction) { foreach ($columns as $name => $direction) {
if (is_object($direction)) { if ($direction instanceof Expression) {
$orders[] = (string)$direction; $orders[] = $direction->expression;
} else { } else {
$orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : ''); $orders[] = $this->db->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
} }
...@@ -765,14 +794,7 @@ class QueryBuilder extends \yii\base\Object ...@@ -765,14 +794,7 @@ class QueryBuilder extends \yii\base\Object
foreach ($unions as $i => $union) { foreach ($unions as $i => $union) {
$query = $union['query']; $query = $union['query'];
if ($query instanceof Query) { if ($query instanceof Query) {
// save the original parameters so that we can restore them later to prevent from modifying the query object list($unions[$i]['query'], $params) = $this->build($query, $params);
$originalParams = $query->params;
$command = $query->createCommand($this->db);
$unions[$i]['query'] = $command->sql;
foreach ($command->params as $name => $value) {
$params[$name] = $value;
}
$query->params = $originalParams;
} }
$result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) '; $result .= 'UNION ' . ($union['all'] ? 'ALL ' : '') . '( ' . $unions[$i]['query'] . ' ) ';
...@@ -797,8 +819,8 @@ class QueryBuilder extends \yii\base\Object ...@@ -797,8 +819,8 @@ class QueryBuilder extends \yii\base\Object
} }
} }
foreach ($columns as $i => $column) { foreach ($columns as $i => $column) {
if (is_object($column)) { if ($column instanceof Expression) {
$columns[$i] = (string)$column; $columns[$i] = $column->expression;
} elseif (strpos($column, '(') === false) { } elseif (strpos($column, '(') === false) {
$columns[$i] = $this->db->quoteColumnName($column); $columns[$i] = $this->db->quoteColumnName($column);
} }
...@@ -1114,19 +1136,11 @@ class QueryBuilder extends \yii\base\Object ...@@ -1114,19 +1136,11 @@ class QueryBuilder extends \yii\base\Object
*/ */
public function buildExistsCondition($operator, $operands, &$params) public function buildExistsCondition($operator, $operands, &$params)
{ {
$subQuery = $operands[0]; if ($operands[0] instanceof Query) {
if (!$subQuery instanceof Query) { list($sql, $params) = $this->build($operands[0], $params);
return "$operator ($sql)";
} else {
throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.'); throw new InvalidParamException('Subquery for EXISTS operator must be a Query object.');
} }
$command = $subQuery->createCommand($this->db);
$subQuerySql = $command->sql;
$subQueryParams = $command->params;
if (!empty($subQueryParams)) {
foreach ($subQueryParams as $name => $value) {
$params[$name] = $value;
}
}
return "$operator ($subQuerySql)";
} }
} }
...@@ -22,8 +22,8 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -22,8 +22,8 @@ class QueryBuilder extends \yii\db\QueryBuilder
{ {
$params = $query->params; $params = $query->params;
$clauses = [ $clauses = [
$this->buildSelect($query->select, $query->distinct, $query->selectOption), $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
$this->buildFrom($query->from), $this->buildFrom($query->from, $params),
$this->buildJoin($query->join, $params), $this->buildJoin($query->join, $params),
$this->buildWhere($query->where, $params), $this->buildWhere($query->where, $params),
$this->buildGroupBy($query->groupBy), $this->buildGroupBy($query->groupBy),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment