Skip to content

Commit

Permalink
Fixes yiisoft#15357: Added multi statement support for `yii\db\sqlite…
Browse files Browse the repository at this point in the history
…\Command`
  • Loading branch information
sergeymakinen authored and samdark committed Dec 19, 2017
1 parent eac6a5d commit 315855f
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 7 deletions.
1 change: 1 addition & 0 deletions framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Yii Framework 2 Change Log
- Enh #15221: Improved the `help/list-action-options` console command output for command options without a description (brandonkelly)
- Enh #15332: Always check for availability of `openssl_pseudo_random_bytes`, even if LibreSSL is available (sammousa)
- Enh #15335: Added `FileHelper::unlink()` that works well under all OSes (samdark)
- Enh #15357: Added multi statement support for `yii\db\sqlite\Command` (sergeymakinen)
- Enh #15347: Add `Instance` support for object property in DI container (kojit2009)
- Enh #15340: Test CHANGELOG.md for valid format (sammousa)
- Enh #15360: Refactored `BaseConsole::updateProgress()` (developeruz)
Expand Down
44 changes: 37 additions & 7 deletions framework/db/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ class Connection extends Component
public $tablePrefix = '';
/**
* @var array mapping between PDO driver names and [[Schema]] classes.
* The keys of the array are PDO driver names while the values the corresponding
* schema class name or configuration. Please refer to [[Yii::createObject()]] for
* The keys of the array are PDO driver names while the values are either the corresponding
* schema class names or configurations. Please refer to [[Yii::createObject()]] for
* details on how to specify a configuration.
*
* This property is mainly used by [[getSchema()]] when fetching the database schema information.
Expand All @@ -292,10 +292,35 @@ class Connection extends Component
/**
* @var string the class used to create new database [[Command]] objects. If you want to extend the [[Command]] class,
* you may configure this property to use your extended version of the class.
* Since version 2.0.14 [[$commandMap]] is used if this property is set to its default value.
* @see createCommand
* @since 2.0.7
* @deprecated 2.0.14 Use [[$commandMap]] for precise configuration.
*/
public $commandClass = 'yii\db\Command';
/**
* @var array mapping between PDO driver names and [[Command]] classes.
* The keys of the array are PDO driver names while the values are either the corresponding
* command class names or configurations. Please refer to [[Yii::createObject()]] for
* details on how to specify a configuration.
*
* This property is mainly used by [[createCommand()]] to create new database [[Command]] objects.
* You normally do not need to set this property unless you want to use your own
* [[Command]] class or support DBMS that is not supported by Yii.
* @since 2.0.14
*/
public $commandMap = [
'pgsql' => 'yii\db\Command', // PostgreSQL
'mysqli' => 'yii\db\Command', // MySQL
'mysql' => 'yii\db\Command', // MySQL
'sqlite' => 'yii\db\sqlite\Command', // sqlite 3
'sqlite2' => 'yii\db\sqlite\Command', // sqlite 2
'sqlsrv' => 'yii\db\Command', // newer MSSQL driver on MS Windows hosts
'oci' => 'yii\db\Command', // Oracle driver
'mssql' => 'yii\db\Command', // older MSSQL driver on MS Windows hosts
'dblib' => 'yii\db\Command', // dblib drivers on GNU/Linux (and maybe other OSes) hosts
'cubrid' => 'yii\db\Command', // CUBRID
];
/**
* @var bool whether to enable [savepoint](http://en.wikipedia.org/wiki/Savepoint).
* Note that if the underlying DBMS does not support savepoint, setting this property to be true will have no effect.
Expand Down Expand Up @@ -687,12 +712,17 @@ protected function initConnection()
*/
public function createCommand($sql = null, $params = [])
{
$driver = $this->getDriverName();
$config = ['class' => 'yii\db\Command'];
if ($this->commandClass !== $config['class']) {
$config['class'] = $this->commandClass;
} elseif (isset($this->commandMap[$driver])) {
$config = !is_array($this->commandMap[$driver]) ? ['class' => $this->commandMap[$driver]] : $this->commandMap[$driver];
}
$config['db'] = $this;
$config['sql'] = $sql;
/** @var Command $command */
$command = new $this->commandClass([
'db' => $this,
'sql' => $sql,
]);

$command = Yii::createObject($config);
return $command->bindValues($params);
}

Expand Down
116 changes: 116 additions & 0 deletions framework/db/sqlite/Command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/

namespace yii\db\sqlite;

use yii\db\SqlToken;
use yii\helpers\StringHelper;

/**
* Command represents an SQLite's SQL statement to be executed against a database.
*
* {@inheritdoc}
*
* @author Sergey Makinen <[email protected]>
* @since 2.0.14
*/
class Command extends \yii\db\Command
{
/**
* @inheritdoc
*/
public function execute()
{
$sql = $this->getSql();
$params = $this->params;
$statements = $this->splitStatements($sql, $params);
if ($statements === false) {
return parent::execute();
}

$result = null;
foreach ($statements as $statement) {
list($statementSql, $statementParams) = $statement;
$this->setSql($statementSql)->bindValues($statementParams);
$result = parent::execute();
}
$this->setSql($sql)->bindValues($params);
return $result;
}

/**
* @inheritdoc
*/
protected function queryInternal($method, $fetchMode = null)
{
$sql = $this->getSql();
$params = $this->params;
$statements = $this->splitStatements($sql, $params);
if ($statements === false) {
return parent::queryInternal($method, $fetchMode);
}

list($lastStatementSql, $lastStatementParams) = array_pop($statements);
foreach ($statements as $statement) {
list($statementSql, $statementParams) = $statement;
$this->setSql($statementSql)->bindValues($statementParams);
parent::execute();
}
$this->setSql($lastStatementSql)->bindValues($lastStatementParams);
$result = parent::queryInternal($method, $fetchMode);
$this->setSql($sql)->bindValues($params);
return $result;
}

/**
* Splits the specified SQL code into individual SQL statements and returns them
* or `false` if there's a single statement.
* @param string $sql
* @param array $params
* @return string[]|false
*/
private function splitStatements($sql, $params)
{
$semicolonIndex = strpos($sql, ';');
if ($semicolonIndex === false || $semicolonIndex === StringHelper::byteLength($sql) - 1) {
return false;
}

$tokenizer = new SqlTokenizer($sql);
$codeToken = $tokenizer->tokenize();
if (count($codeToken->getChildren()) === 1) {
return false;
}

$statements = [];
foreach ($codeToken->getChildren() as $statement) {
$statements[] = [$statement->getSql(), $this->extractUsedParams($statement, $params)];
}
return $statements;
}

/**
* Returns named bindings used in the specified statement token.
* @param SqlToken $statement
* @param array $params
* @return array
*/
private function extractUsedParams(SqlToken $statement, $params)
{
preg_match_all('/(?P<placeholder>[:][a-zA-Z0-9_]+)/', $statement->getSql(), $matches, PREG_SET_ORDER);
$result = [];
foreach ($matches as $match) {
$phName = ltrim($match['placeholder'], ':');
if (isset($params[$phName])) {
$result[$phName] = $params[$phName];
} elseif (isset($params[':' . $phName])) {
$result[':' . $phName] = $params[':' . $phName];
}
}
return $result;
}
}
43 changes: 43 additions & 0 deletions tests/framework/db/sqlite/CommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,47 @@ public function testAddDropCheck()
{
$this->markTestSkipped('SQLite does not support adding/dropping check constraints.');
}

public function testMultiStatementSupport()
{
$db = $this->getConnection(false);
$sql = <<<'SQL'
DROP TABLE IF EXISTS {{T_multistatement}};
CREATE TABLE {{T_multistatement}} (
[[intcol]] INTEGER,
[[textcol]] TEXT
);
INSERT INTO {{T_multistatement}} VALUES(41, :val1);
INSERT INTO {{T_multistatement}} VALUES(42, :val2);
SQL;
$db->createCommand($sql, [
'val1' => 'foo',
'val2' => 'bar',
])->execute();
$this->assertSame([
[
'intcol' => '41',
'textcol' => 'foo',
],
[
'intcol' => '42',
'textcol' => 'bar',
],
], $db->createCommand('SELECT * FROM {{T_multistatement}}')->queryAll());
$sql = <<<'SQL'
UPDATE {{T_multistatement}} SET [[intcol]] = :newInt WHERE [[textcol]] = :val1;
DELETE FROM {{T_multistatement}} WHERE [[textcol]] = :val2;
SELECT * FROM {{T_multistatement}}
SQL;
$this->assertSame([
[
'intcol' => '410',
'textcol' => 'foo',
],
], $db->createCommand($sql, [
'newInt' => 410,
'val1' => 'foo',
'val2' => 'bar',
])->queryAll());
}
}

0 comments on commit 315855f

Please sign in to comment.