diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 125263fb88..422d3a3fdb 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -107,6 +107,7 @@ Yii Framework 2 Change Log - Enh #13266: Added `yii\validators\EachValidator::$stopOnFirstError` allowing addition of more than one error (klimov-paul) - Enh #13268: Added logging of memory usage (bashkarev) - Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe) +- Enh #8293: `yii\db\Query` can be passed to `insert` method in `yii\db\QueryBuilder` (voroks) - Enh #13134: Added logging URL rules (bashkarev) - Enh: Refactored `yii\web\ErrorAction` to make it reusable (silverfire) - Enh: Added support for field `yii\console\controllers\BaseMigrateController::$migrationNamespaces` setup from CLI (schmunk42) diff --git a/framework/db/Command.php b/framework/db/Command.php index 3b6fc2f3bb..14d8978dc2 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -421,7 +421,9 @@ class Command extends Component * Note that the created command is not executed until [[execute()]] is called. * * @param string $table the table that new rows will be inserted into. - * @param array $columns the column data (name => value) to be inserted into the table. + * @param array|\yii\db\Query $columns the column data (name => value) to be inserted into the table or instance + * of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement. + * Passing of [[yii\db\Query|Query]] is available since version 2.0.11. * @return $this the command object itself */ public function insert($table, $columns) diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 85f9666e6b..8d5b8d6988 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -142,7 +142,9 @@ class QueryBuilder extends \yii\base\Object * The method will properly escape the table and column names. * * @param string $table the table that new rows will be inserted into. - * @param array $columns the column data (name => value) to be inserted into the table. + * @param array|\yii\db\Query $columns the column data (name => value) to be inserted into the table or instance + * of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement. + * Passing of [[yii\db\Query|Query]] is available since version 2.0.11. * @param array $params the binding parameters that will be generated by this method. * They should be bound to the DB command later. * @return string the INSERT SQL @@ -157,23 +159,58 @@ class QueryBuilder extends \yii\base\Object } $names = []; $placeholders = []; - foreach ($columns as $name => $value) { - $names[] = $schema->quoteColumnName($name); - if ($value instanceof Expression) { - $placeholders[] = $value->expression; - foreach ($value->params as $n => $v) { - $params[$n] = $v; + $values = ' DEFAULT VALUES'; + if ($columns instanceof \yii\db\Query) { + list($names, $values) = $this->prepareInsertSelectSubQuery($columns, $schema); + } else { + foreach ($columns as $name => $value) { + $names[] = $schema->quoteColumnName($name); + if ($value instanceof Expression) { + $placeholders[] = $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $placeholders[] = $phName; + $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value; } - } else { - $phName = self::PARAM_PREFIX . count($params); - $placeholders[] = $phName; - $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value; } } return 'INSERT INTO ' . $schema->quoteTableName($table) . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '') - . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : ' DEFAULT VALUES'); + . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values); + } + + /** + * Prepare select-subquery and field names for INSERT INTO ... SELECT SQL statement. + * + * @param \yii\db\Query $columns Object, which represents select query + * @param \yii\db\Schema $schema Schema object to qoute column name + * @return array + * @since 2.0.11 + */ + protected function prepareInsertSelectSubQuery($columns, $schema) + { + if (!is_array($columns->select) || empty($columns->select) || in_array('*', $columns->select)) { + throw new InvalidParamException('Expected select query object with enumerated (named) parameters'); + } + + list ($values, ) = $this->build($columns); + $names = []; + $values = ' ' . $values; + foreach ($columns->select as $title => $field) { + if (is_string($title)) { + $names[] = $title; + } else if (preg_match('/^(.*?)(?i:\s+as\s+|\s+)([\w\-_\.]+)$/', $field, $matches)) { + $names[] = $schema->quoteColumnName($matches[2]); + } else { + $names[] = $schema->quoteColumnName($field); + } + } + + return [$names, $values]; } /** diff --git a/framework/db/mysql/QueryBuilder.php b/framework/db/mysql/QueryBuilder.php index dd52adc96d..2c8479ffdf 100644 --- a/framework/db/mysql/QueryBuilder.php +++ b/framework/db/mysql/QueryBuilder.php @@ -195,30 +195,35 @@ class QueryBuilder extends \yii\db\QueryBuilder } $names = []; $placeholders = []; - foreach ($columns as $name => $value) { - $names[] = $schema->quoteColumnName($name); - if ($value instanceof Expression) { - $placeholders[] = $value->expression; - foreach ($value->params as $n => $v) { - $params[$n] = $v; - } - } else { - $phName = self::PARAM_PREFIX . count($params); - $placeholders[] = $phName; - $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value; - } - } - if (empty($names) && $tableSchema !== null) { - $columns = !empty($tableSchema->primaryKey) ? $tableSchema->primaryKey : [reset($tableSchema->columns)->name]; - foreach ($columns as $name) { + $values = ' DEFAULT VALUES'; + if ($columns instanceof \yii\db\Query) { + list($names, $values) = $this->prepareInsertSelectSubQuery($columns, $schema); + } else { + foreach ($columns as $name => $value) { $names[] = $schema->quoteColumnName($name); - $placeholders[] = 'DEFAULT'; + if ($value instanceof Expression) { + $placeholders[] = $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $placeholders[] = $phName; + $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value; + } + } + if (empty($names) && $tableSchema !== null) { + $columns = !empty($tableSchema->primaryKey) ? $tableSchema->primaryKey : [reset($tableSchema->columns)->name]; + foreach ($columns as $name) { + $names[] = $schema->quoteColumnName($name); + $placeholders[] = 'DEFAULT'; + } } } return 'INSERT INTO ' . $schema->quoteTableName($table) . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '') - . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : ' DEFAULT VALUES'); + . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values); } /** diff --git a/framework/db/oci/QueryBuilder.php b/framework/db/oci/QueryBuilder.php index 072f3f0915..827e6f8613 100644 --- a/framework/db/oci/QueryBuilder.php +++ b/framework/db/oci/QueryBuilder.php @@ -178,30 +178,35 @@ EOD; } $names = []; $placeholders = []; - foreach ($columns as $name => $value) { - $names[] = $schema->quoteColumnName($name); - if ($value instanceof Expression) { - $placeholders[] = $value->expression; - foreach ($value->params as $n => $v) { - $params[$n] = $v; - } - } else { - $phName = self::PARAM_PREFIX . count($params); - $placeholders[] = $phName; - $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value; - } - } - if (empty($names) && $tableSchema !== null) { - $columns = !empty($tableSchema->primaryKey) ? $tableSchema->primaryKey : reset($tableSchema->columns)->name; - foreach ($columns as $name) { + $values = ' DEFAULT VALUES'; + if ($columns instanceof \yii\db\Query) { + list($names, $values) = $this->prepareInsertSelectSubQuery($columns, $schema); + } else { + foreach ($columns as $name => $value) { $names[] = $schema->quoteColumnName($name); - $placeholders[] = 'DEFAULT'; + if ($value instanceof Expression) { + $placeholders[] = $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $placeholders[] = $phName; + $params[$phName] = !is_array($value) && isset($columnSchemas[$name]) ? $columnSchemas[$name]->dbTypecast($value) : $value; + } + } + if (empty($names) && $tableSchema !== null) { + $columns = !empty($tableSchema->primaryKey) ? $tableSchema->primaryKey : [reset($tableSchema->columns)->name]; + foreach ($columns as $name) { + $names[] = $schema->quoteColumnName($name); + $placeholders[] = 'DEFAULT'; + } } } return 'INSERT INTO ' . $schema->quoteTableName($table) . (!empty($names) ? ' (' . implode(', ', $names) . ')' : '') - . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : ' DEFAULT VALUES'); + . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values); } /** diff --git a/framework/db/pgsql/QueryBuilder.php b/framework/db/pgsql/QueryBuilder.php index 1ede10a1a9..b43fc5ace1 100644 --- a/framework/db/pgsql/QueryBuilder.php +++ b/framework/db/pgsql/QueryBuilder.php @@ -245,12 +245,18 @@ class QueryBuilder extends \yii\db\QueryBuilder /** * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary. * @param string $table the table that data will be saved into. - * @param array $columns the column data (name => value) to be saved into the table. + * @param array|\yii\db\Query $columns the column data (name => value) to be saved into the table or instance + * of [[yii\db\Query|Query]] to perform INSERT INTO ... SELECT SQL statement. + * Passing of [[yii\db\Query|Query]] is available since version 2.0.11. * @return array normalized columns * @since 2.0.9 */ private function normalizeTableRowData($table, $columns) { + if ($columns instanceof \yii\db\Query) { + return $columns; + } + if (($tableSchema = $this->db->getSchema()->getTableSchema($table)) !== null) { $columnSchemas = $tableSchema->columns; foreach ($columns as $name => $value) { @@ -259,6 +265,7 @@ class QueryBuilder extends \yii\db\QueryBuilder } } } + return $columns; } diff --git a/tests/framework/db/CommandTest.php b/tests/framework/db/CommandTest.php index cbf7af7a5b..ffdee8a8c1 100644 --- a/tests/framework/db/CommandTest.php +++ b/tests/framework/db/CommandTest.php @@ -321,6 +321,136 @@ SQL; ], $record); } + /** + * Test INSERT INTO ... SELECT SQL statement + */ + public function testInsertSelect() + { + $db = $this->getConnection(); + $db->createCommand('DELETE FROM {{customer}};')->execute(); + + $command = $db->createCommand(); + $command->insert( + '{{customer}}', + [ + 'email' => 't1@example.com', + 'name' => 'test', + 'address' => 'test address', + ] + )->execute(); + + $query = new \yii\db\Query(); + $query->select([ + '{{customer}}.email as name', + 'name as email', + 'address', + ] + )->from('{{customer}}'); + + $command = $db->createCommand(); + $command->insert( + '{{customer}}', + $query + )->execute(); + + $this->assertEquals(2, $db->createCommand('SELECT COUNT(*) FROM {{customer}};')->queryScalar()); + $record = $db->createCommand('SELECT email, name, address FROM {{customer}};')->queryAll(); + $this->assertEquals([ + [ + 'email' => 't1@example.com', + 'name' => 'test', + 'address' => 'test address', + ], + [ + 'email' => 'test', + 'name' => 't1@example.com', + 'address' => 'test address', + ], + ], $record); + } + + /** + * Test INSERT INTO ... SELECT SQL statement with alias syntax + */ + public function testInsertSelectAlias() + { + $db = $this->getConnection(); + $db->createCommand('DELETE FROM {{customer}};')->execute(); + + $command = $db->createCommand(); + $command->insert( + '{{customer}}', + [ + 'email' => 't1@example.com', + 'name' => 'test', + 'address' => 'test address', + ] + )->execute(); + + $query = new \yii\db\Query(); + $query->select([ + 'email' => '{{customer}}.email', + 'address' => 'name', + 'name' => 'address', + ] + )->from('{{customer}}'); + + $command = $db->createCommand(); + $command->insert( + '{{customer}}', + $query + )->execute(); + + $this->assertEquals(2, $db->createCommand('SELECT COUNT(*) FROM {{customer}};')->queryScalar()); + $record = $db->createCommand('SELECT email, name, address FROM {{customer}};')->queryAll(); + $this->assertEquals([ + [ + 'email' => 't1@example.com', + 'name' => 'test', + 'address' => 'test address', + ], + [ + 'email' => 't1@example.com', + 'name' => 'test address', + 'address' => 'test', + ], + ], $record); + } + + /** + * Data provider for testInsertSelectFailed + * @return array + */ + public function invalidSelectColumns() { + return [ + [[]], + ['*'], + [['*']], + ]; + } + + /** + * Test INSERT INTO ... SELECT SQL statement with wrong query object + * + * @dataProvider invalidSelectColumns + * @expectedException \yii\base\InvalidParamException + * @expectedExceptionMessage Expected select query object with enumerated (named) parameters + */ + public function testInsertSelectFailed($invalidSelectCulumns) + { + $this->setExpectedException('\yii\base\InvalidParamException'); + + $query = new \yii\db\Query(); + $query->select(['*'])->from('{{customer}}'); + + $db = $this->getConnection(); + $command = $db->createCommand(); + $command->insert( + '{{customer}}', + $query + )->execute(); + } + public function testInsertExpression() { $db = $this->getConnection();