Commit fe90d4dd by Qiang Xue

implemented auto-quoting for DB commands.

parent d15378ef
...@@ -62,21 +62,6 @@ class Command extends \yii\base\Component ...@@ -62,21 +62,6 @@ class Command extends \yii\base\Component
private $_params = array(); private $_params = array();
/** /**
* Constructor.
* @param Connection $connection the database connection
* @param string $sql the SQL statement to be executed
* @param array $params the parameters to be bound to the SQL statement
* @param array $config name-value pairs that will be used to initialize the object properties
*/
public function __construct($connection, $sql = null, $params = array(), $config = array())
{
$this->connection = $connection;
$this->_sql = $sql;
$this->bindValues($params);
parent::__construct($config);
}
/**
* Returns the SQL statement for this command. * Returns the SQL statement for this command.
* @return string the SQL statement to be executed * @return string the SQL statement to be executed
*/ */
...@@ -88,14 +73,26 @@ class Command extends \yii\base\Component ...@@ -88,14 +73,26 @@ class Command extends \yii\base\Component
/** /**
* Specifies the SQL statement to be executed. * Specifies the SQL statement to be executed.
* Any previous execution will be terminated or cancelled. * Any previous execution will be terminated or cancelled.
* @param string $value the SQL statement to be set. * @param string $sql the SQL statement to be set.
* @return Command this command instance * @return Command this command instance
*/ */
public function setSql($value) public function setSql($sql)
{ {
$this->_sql = $value; if ($sql !== $this->_sql) {
$this->_params = array(); if ($this->connection->enableAutoQuoting && $sql != '') {
$this->cancel(); $sql = preg_replace_callback('/(\\{\\{(.*?)\\}\\}|\\[\\[(.*?)\\]\\])/', function($matches) {
if (isset($matches[3])) {
return $this->connection->quoteColumnName($matches[3]);
} else {
$name = str_replace('%', $this->connection->tablePrefix, $matches[2]);
return $this->connection->quoteTableName($name);
}
}, $sql);
}
$this->_sql = $sql;
$this->_params = array();
$this->cancel();
}
return $this; return $this;
} }
...@@ -110,7 +107,7 @@ class Command extends \yii\base\Component ...@@ -110,7 +107,7 @@ class Command extends \yii\base\Component
public function prepare() public function prepare()
{ {
if ($this->pdoStatement == null) { if ($this->pdoStatement == null) {
$sql = $this->connection->expandTablePrefix($this->getSql()); $sql = $this->getSql();
try { try {
$this->pdoStatement = $this->connection->pdo->prepare($sql); $this->pdoStatement = $this->connection->pdo->prepare($sql);
} catch (\Exception $e) { } catch (\Exception $e) {
...@@ -223,7 +220,7 @@ class Command extends \yii\base\Component ...@@ -223,7 +220,7 @@ class Command extends \yii\base\Component
*/ */
public function execute($params = array()) public function execute($params = array())
{ {
$sql = $this->connection->expandTablePrefix($this->getSql()); $sql = $this->getSql();
$this->_params = array_merge($this->_params, $params); $this->_params = array_merge($this->_params, $params);
if ($this->_params === array()) { if ($this->_params === array()) {
$paramLog = ''; $paramLog = '';
...@@ -356,7 +353,7 @@ class Command extends \yii\base\Component ...@@ -356,7 +353,7 @@ class Command extends \yii\base\Component
private function queryInternal($method, $params, $fetchMode = null) private function queryInternal($method, $params, $fetchMode = null)
{ {
$db = $this->connection; $db = $this->connection;
$sql = $db->expandTablePrefix($this->getSql()); $sql = $this->getSql();
$this->_params = array_merge($this->_params, $params); $this->_params = array_merge($this->_params, $params);
if ($this->_params === array()) { if ($this->_params === array()) {
$paramLog = ''; $paramLog = '';
......
...@@ -124,7 +124,7 @@ class Connection extends \yii\base\ApplicationComponent ...@@ -124,7 +124,7 @@ class Connection extends \yii\base\ApplicationComponent
public $attributes; public $attributes;
/** /**
* @var \PDO the PHP PDO instance associated with this DB connection. * @var \PDO the PHP PDO instance associated with this DB connection.
* This property is mainly managed by [[open]] and [[close]] methods. * This property is mainly managed by [[open()]] and [[close()]] methods.
* When a DB connection is active, this property will represent a PDO instance; * When a DB connection is active, this property will represent a PDO instance;
* otherwise, it will be null. * otherwise, it will be null.
*/ */
...@@ -132,7 +132,7 @@ class Connection extends \yii\base\ApplicationComponent ...@@ -132,7 +132,7 @@ class Connection extends \yii\base\ApplicationComponent
/** /**
* @var boolean whether to enable schema caching. * @var boolean whether to enable schema caching.
* Note that in order to enable truly schema caching, a valid cache component as specified * Note that in order to enable truly schema caching, a valid cache component as specified
* by [[schemaCacheID]] must be enabled and [[schemaCacheEnabled]] must be set true. * by [[schemaCacheID]] must be enabled and [[enableSchemaCache]] must be set true.
* @see schemaCacheDuration * @see schemaCacheDuration
* @see schemaCacheExclude * @see schemaCacheExclude
* @see schemaCacheID * @see schemaCacheID
...@@ -159,7 +159,7 @@ class Connection extends \yii\base\ApplicationComponent ...@@ -159,7 +159,7 @@ class Connection extends \yii\base\ApplicationComponent
/** /**
* @var boolean whether to enable query caching. * @var boolean whether to enable query caching.
* Note that in order to enable query caching, a valid cache component as specified * Note that in order to enable query caching, a valid cache component as specified
* by [[queryCacheID]] must be enabled and [[queryCacheEnabled]] must be set true. * by [[queryCacheID]] must be enabled and [[enableQueryCache]] must be set true.
* *
* Methods [[beginCache()]] and [[endCache()]] can be used as shortcuts to turn on * Methods [[beginCache()]] and [[endCache()]] can be used as shortcuts to turn on
* and off query caching on the fly. * and off query caching on the fly.
...@@ -215,16 +215,24 @@ class Connection extends \yii\base\ApplicationComponent ...@@ -215,16 +215,24 @@ class Connection extends \yii\base\ApplicationComponent
*/ */
public $enableProfiling = false; public $enableProfiling = false;
/** /**
* @var string the default prefix for table names. Defaults to null, meaning not using table prefix. * @var string the common prefix or suffix for table names. If a table name is given
* By setting this property, any token like '{{TableName}}' in [[Command::sql]] will * as `{{%TableName}}`, then the percentage character `%` will be replaced with this
* be replaced with 'prefixTableName', where 'prefix' refers to this property value. * property value. For example, `{{%post}}` becomes `{{tbl_post}}` if this property is
* For example, '{{post}}' becomes 'tbl_post', if 'tbl_' is set as the table prefix. * set as `"tbl_"`. Note that this property is only effective when [[enableAutoQuoting]]
* * is true.
* Note that if you set this property to be an empty string, then '{{post}}' will be replaced * @see enableAutoQuoting
* with 'post'.
*/ */
public $tablePrefix; public $tablePrefix;
/** /**
* @var boolean whether to enable automatic quoting of table names and column names.
* Defaults to true. When this property is true, any token enclosed within double curly brackets
* (e.g. `{{post}}`) in a SQL statement will be treated as a table name and will be quoted
* accordingly when the SQL statement is executed; and any token enclosed within double square
* brackets (e.g. `[[name]]`) will be treated as a column name and quoted accordingly.
* @see tablePrefix
*/
public $enableAutoQuoting = true;
/**
* @var array a list of SQL statements that should be executed right after the DB connection is established. * @var array a list of SQL statements that should be executed right after the DB connection is established.
*/ */
public $initSQLs; public $initSQLs;
...@@ -411,7 +419,12 @@ class Connection extends \yii\base\ApplicationComponent ...@@ -411,7 +419,12 @@ class Connection extends \yii\base\ApplicationComponent
public function createCommand($sql = null, $params = array()) public function createCommand($sql = null, $params = array())
{ {
$this->open(); $this->open();
return new Command($this, $sql, $params); $command = new Command(array(
'connection' => $this,
'sql' => $sql,
));
$command->bindValues($params);
return $command;
} }
/** /**
...@@ -513,43 +526,27 @@ class Connection extends \yii\base\ApplicationComponent ...@@ -513,43 +526,27 @@ class Connection extends \yii\base\ApplicationComponent
/** /**
* Quotes a table name for use in a query. * Quotes a table name for use in a query.
* If the table name contains schema prefix, the prefix will also be properly quoted. * If the table name contains schema prefix, the prefix will also be properly quoted.
* If the table name is already quoted or contains special characters including '(', '[[' and '{{',
* then this method will do nothing.
* @param string $name table name * @param string $name table name
* @param boolean $simple if this is true, then the method will assume $name is a table name without schema prefix.
* @return string the properly quoted table name * @return string the properly quoted table name
*/ */
public function quoteTableName($name, $simple = false) public function quoteTableName($name)
{ {
return $simple ? $this->getDriver()->quoteSimpleTableName($name) : $this->getDriver()->quoteTableName($name); return $this->getDriver()->quoteTableName($name);
} }
/** /**
* Quotes a column name for use in a query. * Quotes a column name for use in a query.
* If the column name contains table prefix, the prefix will also be properly quoted. * If the column name contains prefix, the prefix will also be properly quoted.
* If the column name is already quoted or contains special characters including '(', '[[' and '{{',
* then this method will do nothing.
* @param string $name column name * @param string $name column name
* @param boolean $simple if this is true, then the method will assume $name is a column name without table prefix.
* @return string the properly quoted column name * @return string the properly quoted column name
*/ */
public function quoteColumnName($name, $simple = false) public function quoteColumnName($name)
{
return $simple ? $this->getDriver()->quoteSimpleColumnName($name) : $this->getDriver()->quoteColumnName($name);
}
/**
* Prefixes table names in a SQL statement with [[tablePrefix]].
* By calling this method, tokens like '{{TableName}}' in the given SQL statement will
* be replaced with 'prefixTableName', where 'prefix' refers to [[tablePrefix]].
* Note that if [[tablePrefix]] is null, this method will do nothing.
* @param string $sql the SQL statement whose table names need to be prefixed with [[tablePrefix]].
* @return string the expanded SQL statement
* @see tablePrefix
*/
public function expandTablePrefix($sql)
{ {
if ($this->tablePrefix !== null && strpos($sql, '{{') !== false) { return $this->getDriver()->quoteColumnName($name);
return preg_replace('/{{(.*?)}}/', $this->tablePrefix . '\1', $sql);
} else {
return $sql;
}
} }
/** /**
......
...@@ -92,8 +92,7 @@ abstract class Driver extends \yii\base\Object ...@@ -92,8 +92,7 @@ abstract class Driver extends \yii\base\Object
} }
$db = $this->connection; $db = $this->connection;
$realName = $this->getRealTableName($name);
$realName = $db->expandTablePrefix($name);
/** @var $cache \yii\caching\Cache */ /** @var $cache \yii\caching\Cache */
if ($db->enableSchemaCache && ($cache = \Yii::$application->getComponent($db->schemaCacheID)) !== null && !in_array($name, $db->schemaCacheExclude, true)) { if ($db->enableSchemaCache && ($cache = \Yii::$application->getComponent($db->schemaCacheID)) !== null && !in_array($name, $db->schemaCacheExclude, true)) {
...@@ -168,37 +167,57 @@ abstract class Driver extends \yii\base\Object ...@@ -168,37 +167,57 @@ abstract class Driver extends \yii\base\Object
/** /**
* Refreshes the schema. * Refreshes the schema.
* This method cleans up the cached table schema and names * This method cleans up all cached table schemas so that they can be re-created later
* so that they can be recreated to reflect the database schema change. * to reflect the database schema change.
* @param string $tableName the name of the table that needs to be refreshed.
* If null, all currently loaded tables will be refreshed.
*/ */
public function refresh($tableName = null) public function refresh()
{ {
$db = $this->connection;
/** @var $cache \yii\caching\Cache */ /** @var $cache \yii\caching\Cache */
if ($db->enableSchemaCache && ($cache = \Yii::$application->getComponent($db->schemaCacheID)) !== null) { if ($this->connection->enableSchemaCache && ($cache = \Yii::$application->getComponent($this->connection->schemaCacheID)) !== null) {
if ($tableName === null) { foreach ($this->_tables as $name => $table) {
foreach ($this->_tables as $name => $table) { $cache->delete($this->getCacheKey($name));
$cache->delete($this->getCacheKey($name));
}
$this->_tables = array();
} else {
$cache->delete($this->getCacheKey($tableName));
unset($this->_tables[$tableName]);
} }
} }
$this->_tables = array();
}
/**
* Creates a query builder for the database.
* This method may be overridden by child classes to create a DBMS-specific query builder.
* @return QueryBuilder query builder instance
*/
public function createQueryBuilder()
{
return new QueryBuilder($this->connection);
}
/**
* Returns all table names in the database.
* This method should be overridden by child classes in order to support this feature
* because the default implementation simply throws an exception.
* @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
* If not empty, the returned table names will be prefixed with the schema name.
* @return array all table names in the database.
*/
protected function findTableNames($schema = '')
{
throw new Exception(get_class($this) . ' does not support fetching all table names.');
} }
/** /**
* Quotes a table name for use in a query. * Quotes a table name for use in a query.
* If the table name contains schema prefix, the prefix will also be properly quoted. * If the table name contains schema prefix, the prefix will also be properly quoted.
* If the table name is already quoted or contains special characters including '(', '[[' and '{{',
* then this method will do nothing.
* @param string $name table name * @param string $name table name
* @return string the properly quoted table name * @return string the properly quoted table name
* @see quoteSimpleTableName * @see quoteSimpleTableName
*/ */
public function quoteTableName($name) public function quoteTableName($name)
{ {
if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) {
return $name;
}
if (strpos($name, '.') === false) { if (strpos($name, '.') === false) {
return $this->quoteSimpleTableName($name); return $this->quoteSimpleTableName($name);
} }
...@@ -211,25 +230,19 @@ abstract class Driver extends \yii\base\Object ...@@ -211,25 +230,19 @@ abstract class Driver extends \yii\base\Object
} }
/** /**
* Quotes a simple table name for use in a query.
* A simple table name does not schema prefix.
* @param string $name table name
* @return string the properly quoted table name
*/
public function quoteSimpleTableName($name)
{
return strpos($name, "'") !== false ? $name : "'" . $name . "'";
}
/**
* Quotes a column name for use in a query. * Quotes a column name for use in a query.
* If the column name contains prefix, the prefix will also be properly quoted. * If the column name contains prefix, the prefix will also be properly quoted.
* If the column name is already quoted or contains special characters including '(', '[[' and '{{',
* then this method will do nothing.
* @param string $name column name * @param string $name column name
* @return string the properly quoted column name * @return string the properly quoted column name
* @see quoteSimpleColumnName * @see quoteSimpleColumnName
*/ */
public function quoteColumnName($name) public function quoteColumnName($name)
{ {
if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) {
return $name;
}
if (($pos = strrpos($name, '.')) !== false) { if (($pos = strrpos($name, '.')) !== false) {
$prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.'; $prefix = $this->quoteTableName(substr($name, 0, $pos)) . '.';
$name = substr($name, $pos + 1); $name = substr($name, $pos + 1);
...@@ -240,36 +253,43 @@ abstract class Driver extends \yii\base\Object ...@@ -240,36 +253,43 @@ abstract class Driver extends \yii\base\Object
} }
/** /**
* Quotes a simple column name for use in a query. * Quotes a simple table name for use in a query.
* A simple column name does not contain prefix. * A simple table name should contain the table name only without any schema prefix.
* @param string $name column name * If the table name is already quoted, this method will do nothing.
* @return string the properly quoted column name * @param string $name table name
* @return string the properly quoted table name
*/ */
public function quoteSimpleColumnName($name) public function quoteSimpleTableName($name)
{ {
return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"'; return strpos($name, "'") !== false ? $name : "'" . $name . "'";
} }
/** /**
* Creates a query builder for the database. * Quotes a simple column name for use in a query.
* This method may be overridden by child classes to create a DBMS-specific query builder. * A simple column name should contain the column name only without any prefix.
* @return QueryBuilder query builder instance * If the column name is already quoted or is the asterisk character '*', this method will do nothing.
* @param string $name column name
* @return string the properly quoted column name
*/ */
public function createQueryBuilder() public function quoteSimpleColumnName($name)
{ {
return new QueryBuilder($this->connection); return strpos($name, '"') !== false || $name === '*' ? $name : '"' . $name . '"';
} }
/** /**
* Returns all table names in the database. * Returns the real name of a table name.
* This method should be overridden by child classes in order to support this feature * This method will strip off curly brackets from the given table name
* because the default implementation simply throws an exception. * and replace the percentage character in the name with [[Connection::tablePrefix]].
* @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. * @param string $name the table name to be converted
* If not empty, the returned table names will be prefixed with the schema name. * @return string the real name of the given table name
* @return array all table names in the database.
*/ */
protected function findTableNames($schema = '') public function getRealTableName($name)
{ {
throw new Exception(get_class($this) . 'does not support fetching all table names.'); if ($this->connection->enableAutoQuoting && strpos($name, '{{') !== false) {
$name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name);
return str_replace('%', $this->connection->tablePrefix, $name);
} else {
return $name;
}
} }
} }
...@@ -46,10 +46,11 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -46,10 +46,11 @@ class QueryBuilder extends \yii\db\QueryBuilder
* @param string $oldName the old name of the column. The name will be properly quoted by the method. * @param string $oldName the old name of the column. The name will be properly quoted by the method.
* @param string $newName the new name of the column. The name will be properly quoted by the method. * @param string $newName the new name of the column. The name will be properly quoted by the method.
* @return string the SQL statement for renaming a DB column. * @return string the SQL statement for renaming a DB column.
* @throws Exception
*/ */
public function renameColumn($table, $oldName, $newName) public function renameColumn($table, $oldName, $newName)
{ {
$quotedTable = $this->quoteTableName($table); $quotedTable = $this->connection->quoteTableName($table);
$row = $this->connection->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryRow(); $row = $this->connection->createCommand('SHOW CREATE TABLE ' . $quotedTable)->queryRow();
if ($row === false) { if ($row === false) {
throw new Exception("Unable to find '$oldName' in table '$table'."); throw new Exception("Unable to find '$oldName' in table '$table'.");
...@@ -64,16 +65,16 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -64,16 +65,16 @@ class QueryBuilder extends \yii\db\QueryBuilder
foreach ($matches[1] as $i => $c) { foreach ($matches[1] as $i => $c) {
if ($c === $oldName) { if ($c === $oldName) {
return "ALTER TABLE $quotedTable CHANGE " return "ALTER TABLE $quotedTable CHANGE "
. $this->quoteColumnName($oldName, true) . ' ' . $this->connection->quoteColumnName($oldName) . ' '
. $this->quoteColumnName($newName, true) . ' ' . $this->connection->quoteColumnName($newName) . ' '
. $matches[2][$i]; . $matches[2][$i];
} }
} }
} }
// try to give back a SQL anyway // try to give back a SQL anyway
return "ALTER TABLE $quotedTable CHANGE " return "ALTER TABLE $quotedTable CHANGE "
. $this->quoteColumnName($oldName, true) . ' ' . $this->connection->quoteColumnName($oldName) . ' '
. $this->quoteColumnName($newName, true); . $this->connection->quoteColumnName($newName);
} }
/** /**
...@@ -84,7 +85,7 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -84,7 +85,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
*/ */
public function dropForeignKey($name, $table) public function dropForeignKey($name, $table)
{ {
return 'ALTER TABLE ' . $this->quoteTableName($table) return 'ALTER TABLE ' . $this->connection->quoteTableName($table)
. ' DROP FOREIGN KEY ' . $this->quoteColumnName($name); . ' DROP FOREIGN KEY ' . $this->connection->quoteColumnName($name);
} }
} }
...@@ -36,6 +36,15 @@ class CommandTest extends \yiiunit\MysqlTestCase ...@@ -36,6 +36,15 @@ class CommandTest extends \yiiunit\MysqlTestCase
$this->assertEquals($sql2, $command->sql); $this->assertEquals($sql2, $command->sql);
} }
function testAutoQuoting()
{
$db = $this->getConnection(false);
$sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t';
$command = $db->createCommand($sql);
$this->assertEquals("SELECT `id`, `t`.`name` FROM `tbl_customer` t", $command->sql);
}
function testPrepareCancel() function testPrepareCancel()
{ {
$db = $this->getConnection(false); $db = $this->getConnection(false);
......
...@@ -49,7 +49,7 @@ class ConnectionTest extends \yiiunit\MysqlTestCase ...@@ -49,7 +49,7 @@ class ConnectionTest extends \yiiunit\MysqlTestCase
$connection = $this->getConnection(false); $connection = $this->getConnection(false);
$this->assertEquals(123, $connection->quoteValue(123)); $this->assertEquals(123, $connection->quoteValue(123));
$this->assertEquals("'string'", $connection->quoteValue('string')); $this->assertEquals("'string'", $connection->quoteValue('string'));
$this->assertEquals("'It\'s interesting'", $connection->quoteValue("It's interesting")); $this->assertEquals("'It\\'s interesting'", $connection->quoteValue("It's interesting"));
} }
function testQuoteTableName() function testQuoteTableName()
...@@ -58,7 +58,10 @@ class ConnectionTest extends \yiiunit\MysqlTestCase ...@@ -58,7 +58,10 @@ class ConnectionTest extends \yiiunit\MysqlTestCase
$this->assertEquals('`table`', $connection->quoteTableName('table')); $this->assertEquals('`table`', $connection->quoteTableName('table'));
$this->assertEquals('`table`', $connection->quoteTableName('`table`')); $this->assertEquals('`table`', $connection->quoteTableName('`table`'));
$this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.table')); $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.table'));
$this->assertEquals('`schema.table`', $connection->quoteTableName('schema.table', true)); $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.`table`'));
$this->assertEquals('[[table]]', $connection->quoteTableName('[[table]]'));
$this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}'));
$this->assertEquals('(table)', $connection->quoteTableName('(table)'));
} }
function testQuoteColumnName() function testQuoteColumnName()
...@@ -67,7 +70,10 @@ class ConnectionTest extends \yiiunit\MysqlTestCase ...@@ -67,7 +70,10 @@ class ConnectionTest extends \yiiunit\MysqlTestCase
$this->assertEquals('`column`', $connection->quoteColumnName('column')); $this->assertEquals('`column`', $connection->quoteColumnName('column'));
$this->assertEquals('`column`', $connection->quoteColumnName('`column`')); $this->assertEquals('`column`', $connection->quoteColumnName('`column`'));
$this->assertEquals('`table`.`column`', $connection->quoteColumnName('table.column')); $this->assertEquals('`table`.`column`', $connection->quoteColumnName('table.column'));
$this->assertEquals('`table.column`', $connection->quoteColumnName('table.column', true)); $this->assertEquals('`table`.`column`', $connection->quoteColumnName('table.`column`'));
$this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]'));
$this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}'));
$this->assertEquals('(column)', $connection->quoteColumnName('(column)'));
} }
function testGetPdoType() function testGetPdoType()
......
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