diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index a56d4e04d5..a7612097f4 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -10,6 +10,7 @@ Yii Framework 2 Change Log - Enh #17396: Added 'invoked by controller' to the debug log message when `\yii\base\Action` is used (alexkart) - Bug #17325: Fixed "Cannot drop view" for MySQL while `migrate/fresh` (alexkart) - Bug #17384: Fixed SQL error when passing `DISTINCT ON` queries (brandonkelly) +- Bug #17057: Fixed issues with table names that contain special characters or keywords in MSSQL (alexkart) 2.0.21 June 18, 2019 diff --git a/framework/db/Schema.php b/framework/db/Schema.php index d27166cb69..e89aabe1d8 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -15,7 +15,6 @@ use yii\base\NotSupportedException; use yii\caching\Cache; use yii\caching\CacheInterface; use yii\caching\TagDependency; -use yii\helpers\StringHelper; /** * Schema is the base class for concrete DBMS-specific schema classes. @@ -485,7 +484,7 @@ abstract class Schema extends BaseObject if (strpos($name, '.') === false) { return $this->quoteSimpleTableName($name); } - $parts = explode('.', $name); + $parts = $this->getTableNameParts($name); foreach ($parts as $i => $part) { $parts[$i] = $this->quoteSimpleTableName($part); } @@ -493,6 +492,17 @@ abstract class Schema extends BaseObject return implode('.', $parts); } + /** + * Splits full table name into parts + * @param string $name + * @return array + * @since 2.0.22 + */ + protected function getTableNameParts($name) + { + return explode('.', $name); + } + /** * Quotes a column name for use in a query. * If the column name contains prefix, the prefix will also be properly quoted. @@ -661,7 +671,7 @@ abstract class Schema extends BaseObject } $message = $e->getMessage() . "\nThe SQL being executed was: $rawSql"; $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - return new $exceptionClass($message, $errorInfo, (int) $e->getCode(), $e); + return new $exceptionClass($message, $errorInfo, (int)$e->getCode(), $e); } /** diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php index 0c3323f7f9..541f02bc10 100644 --- a/framework/db/mssql/Schema.php +++ b/framework/db/mssql/Schema.php @@ -98,7 +98,7 @@ class Schema extends \yii\db\Schema implements ConstraintFinderInterface protected function resolveTableName($name) { $resolvedName = new TableSchema(); - $parts = explode('.', str_replace(['[', ']'], '', $name)); + $parts = $this->getTableNameParts($name); $partCount = count($parts); if ($partCount === 4) { // server name, catalog name, schema name and table name passed @@ -126,6 +126,25 @@ class Schema extends \yii\db\Schema implements ConstraintFinderInterface return $resolvedName; } + /** + * {@inheritDoc} + * @param string $name + * @return array + * @since 2.0.22 + */ + protected function getTableNameParts($name) + { + $parts = [$name]; + preg_match_all('/([^.\[\]]+)|\[([^\[\]]+)\]/', $name, $matches); + if (isset($matches[0]) && is_array($matches[0]) && !empty($matches[0])) { + $parts = $matches[0]; + } + + $parts = str_replace(['[', ']'], '', $parts); + + return $parts; + } + /** * {@inheritdoc} * @see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql @@ -223,8 +242,8 @@ SQL; $result = []; foreach ($indexes as $name => $index) { $result[] = new IndexConstraint([ - 'isPrimary' => (bool) $index[0]['index_is_primary'], - 'isUnique' => (bool) $index[0]['index_is_unique'], + 'isPrimary' => (bool)$index[0]['index_is_primary'], + 'isUnique' => (bool)$index[0]['index_is_unique'], 'name' => $name, 'columnNames' => ArrayHelper::getColumn($index, 'column_name'), ]); @@ -297,7 +316,7 @@ SQL; */ protected function resolveTableNames($table, $name) { - $parts = explode('.', str_replace(['[', ']'], '', $name)); + $parts = $this->getTableNameParts($name); $partCount = count($parts); if ($partCount === 4) { // server name, catalog name, schema name and table name passed @@ -349,9 +368,9 @@ SQL; } if (!empty($matches[2])) { $values = explode(',', $matches[2]); - $column->size = $column->precision = (int) $values[0]; + $column->size = $column->precision = (int)$values[0]; if (isset($values[1])) { - $column->scale = (int) $values[1]; + $column->scale = (int)$values[1]; } if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { $column->type = 'boolean'; diff --git a/tests/data/mssql.sql b/tests/data/mssql.sql index 7b759a9a20..576f19bc62 100644 --- a/tests/data/mssql.sql +++ b/tests/data/mssql.sql @@ -21,6 +21,7 @@ IF OBJECT_ID('[T_constraints_3]', 'U') IS NOT NULL DROP TABLE [T_constraints_3]; IF OBJECT_ID('[T_constraints_2]', 'U') IS NOT NULL DROP TABLE [T_constraints_2]; IF OBJECT_ID('[T_constraints_1]', 'U') IS NOT NULL DROP TABLE [T_constraints_1]; IF OBJECT_ID('[T_upsert]', 'U') IS NOT NULL DROP TABLE [T_upsert]; +IF OBJECT_ID('[table.with.special.characters]', 'U') IS NOT NULL DROP TABLE [table.with.special.characters]; CREATE TABLE [dbo].[profile] ( [id] [int] IDENTITY NOT NULL, @@ -89,7 +90,9 @@ CREATE TABLE [dbo].[order_item] ( [item_id] ASC ) ON [PRIMARY] -);CREATE TABLE [dbo].[order_item_with_null_fk] ( +); + +CREATE TABLE [dbo].[order_item_with_null_fk] ( [order_id] [int], [item_id] [int], [quantity] [int] NOT NULL, @@ -311,3 +314,7 @@ CREATE TABLE [T_upsert] [profile_id] INT NULL, UNIQUE ([email], [recovery_email]) ); + +CREATE TABLE [dbo].[table.with.special.characters] ( + [id] [int] +); diff --git a/tests/framework/db/mssql/SchemaTest.php b/tests/framework/db/mssql/SchemaTest.php index 492c873176..41b3b7b6ca 100644 --- a/tests/framework/db/mssql/SchemaTest.php +++ b/tests/framework/db/mssql/SchemaTest.php @@ -83,4 +83,54 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest } } } + + /** + * @dataProvider quoteTableNameDataProvider + * @param $name + * @param $expectedName + * @throws \yii\base\NotSupportedException + */ + public function testQuoteTableName($name, $expectedName) + { + $schema = $this->getConnection()->getSchema(); + $quotedName = $schema->quoteTableName($name); + $this->assertEquals($expectedName, $quotedName); + } + + public function quoteTableNameDataProvider() + { + return [ + ['test', '[test]'], + ['test.test', '[test].[test]'], + ['test.test.test', '[test].[test].[test]'], + ['[test]', '[test]'], + ['[test].[test]', '[test].[test]'], + ['test.[test.test]', '[test].[test.test]'], + ['test.test.[test.test]', '[test].[test].[test.test]'], + ['[test].[test.test]', '[test].[test.test]'], + ]; + } + + /** + * @dataProvider getTableSchemaDataProvider + * @param $name + * @param $expectedName + * @throws \yii\base\NotSupportedException + */ + public function testGetTableSchema($name, $expectedName) + { + $schema = $this->getConnection()->getSchema(); + $tableSchema = $schema->getTableSchema($name); + $this->assertEquals($expectedName, $tableSchema->name); + } + + public function getTableSchemaDataProvider() + { + return [ + ['[dbo].[profile]', 'profile'], + ['dbo.profile', 'profile'], + ['profile', 'profile'], + ['dbo.[table.with.special.characters]', 'table.with.special.characters'], + ]; + } } diff --git a/tests/framework/db/sqlite/SchemaTest.php b/tests/framework/db/sqlite/SchemaTest.php index e4ecff0ebd..06d3c14207 100644 --- a/tests/framework/db/sqlite/SchemaTest.php +++ b/tests/framework/db/sqlite/SchemaTest.php @@ -82,4 +82,29 @@ class SchemaTest extends \yiiunit\framework\db\SchemaTest return $result; } + + /** + * @dataProvider quoteTableNameDataProvider + * @param $name + * @param $expectedName + * @throws \yii\base\NotSupportedException + */ + public function testQuoteTableName($name, $expectedName) + { + $schema = $this->getConnection()->getSchema(); + $quotedName = $schema->quoteTableName($name); + $this->assertEquals($expectedName, $quotedName); + } + + public function quoteTableNameDataProvider() + { + return [ + ['test', '`test`'], + ['test.test', '`test`.`test`'], + ['test.test.test', '`test`.`test`.`test`'], + ['`test`', '`test`'], + ['`test`.`test`', '`test`.`test`'], + ['test.`test`.test', '`test`.`test`.`test`'], + ]; + } }