From 87c2fefe092b86e7a632e260d2127100485bae1b Mon Sep 17 00:00:00 2001 From: resurtm Date: Mon, 6 May 2013 16:46:33 +0600 Subject: [PATCH 1/8] Initial MSSQL related commit. --- framework/db/Connection.php | 10 ++-- framework/db/mssql/PDO.php | 68 +++++++++++++++++++++++++++ framework/db/mssql/QueryBuilder.php | 19 ++++++++ framework/db/mssql/Schema.php | 93 +++++++++++++++++++++++++++++++++++++ framework/db/mssql/SqlsrvPDO.php | 33 +++++++++++++ 5 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 framework/db/mssql/PDO.php create mode 100644 framework/db/mssql/QueryBuilder.php create mode 100644 framework/db/mssql/Schema.php create mode 100644 framework/db/mssql/SqlsrvPDO.php diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 797508a..c4bf4b9 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -242,10 +242,10 @@ class Connection extends Component 'mysql' => 'yii\db\mysql\Schema', // MySQL 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2 - 'mssql' => 'yi\db\dao\mssql\Schema', // Mssql driver on windows hosts - 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql + 'sqlsrv' => 'yii\db\mssql\Schema', // newer MSSQL driver on MS Windows hosts 'oci' => 'yii\db\oci\Schema', // Oracle driver - 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts + 'mssql' => 'yii\db\mssql\Schema', // older MSSQL driver on MS Windows hosts + 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on GNU/Linux (and maybe other OSes) hosts ); /** * @var Transaction the currently active transaction @@ -351,7 +351,9 @@ class Connection extends Component $pdoClass = '\PDO'; if (($pos = strpos($this->dsn, ':')) !== false) { $driver = strtolower(substr($this->dsn, 0, $pos)); - if ($driver === 'mssql' || $driver === 'dblib' || $driver === 'sqlsrv') { + if ($driver === 'sqlsrv') { + $pdoClass = 'yii\db\mssql\SqlsrvPDO'; + } elseif ($driver === 'mssql' || $driver === 'dblib') { $pdoClass = 'yii\db\mssql\PDO'; } } diff --git a/framework/db/mssql/PDO.php b/framework/db/mssql/PDO.php new file mode 100644 index 0000000..e045d68 --- /dev/null +++ b/framework/db/mssql/PDO.php @@ -0,0 +1,68 @@ + + * @since 2.0 + */ +class PDO extends \PDO +{ + /** + * Returns last inserted ID value. + * + * @param string|null sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return $this->query('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS bigint)')->fetchColumn(); + } + + /** + * Begin a transaction. + * + * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transactions. + * + * @return boolean + */ + public function beginTransaction() + { + $this->exec('BEGIN TRANSACTION'); + return true; + } + + /** + * Commit a transaction. + * + * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transactions. + * + * @return boolean + */ + public function commit() + { + $this->exec('COMMIT TRANSACTION'); + return true; + } + + /** + * Rollback a transaction. + * + * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transaction. + * + * @return boolean + */ + public function rollBack() + { + $this->exec('ROLLBACK TRANSACTION'); + return true; + } +} diff --git a/framework/db/mssql/QueryBuilder.php b/framework/db/mssql/QueryBuilder.php new file mode 100644 index 0000000..988758b --- /dev/null +++ b/framework/db/mssql/QueryBuilder.php @@ -0,0 +1,19 @@ + + * @since 2.0 + */ +class QueryBuilder extends \yii\db\QueryBuilder +{ + // TODO: mssql driver +} diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php new file mode 100644 index 0000000..8fc8aea --- /dev/null +++ b/framework/db/mssql/Schema.php @@ -0,0 +1,93 @@ + + * @since 2.0 + */ +class Schema extends \yii\db\Schema +{ + /** + * Default schema name to be used. + */ + const DEFAULT_SCHEMA = 'dbo'; + + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + */ + public $typeMap = array( + // TODO: mssql driver + ); + + /** + * @param string $name + * @return TableSchema + */ + public function loadTableSchema($name) + { + return null; + } + + /** + * Quotes a table name for use in a query. + * A simple table name has no 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. + * A simple column name has no prefix. + * @param string $name column name. + * @return string the properly quoted column name. + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '[') !== false || $name === '*' ? $name : '[' . $name . ']'; + } + + /** + * Creates a query builder for the MSSQL database. + * @return QueryBuilder query builder interface. + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * 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. + * @return array all table names in the database. The names have NO the schema name prefix. + */ + protected function findTableNames($schema = '') + { + if ('' === $schema) { + $schema = self::DEFAULT_SCHEMA; + } + $sql = "SELECT TABLE_NAME FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'BASE TABLE'"; + $names = $this->db->createCommand($sql, array(':schema' => $schema))->queryColumn(); + if (self::DEFAULT_SCHEMA !== $schema) { + foreach ($names as $index => $name) { + $names[$index] = $schema . '.' . $name; + } + } + return $names; + } +} diff --git a/framework/db/mssql/SqlsrvPDO.php b/framework/db/mssql/SqlsrvPDO.php new file mode 100644 index 0000000..607d0e4 --- /dev/null +++ b/framework/db/mssql/SqlsrvPDO.php @@ -0,0 +1,33 @@ + + * @since 2.0 + */ +class SqlsrvPDO extends \PDO +{ + /** + * Returns last inserted ID value. + * + * SQLSRV driver supports PDO::lastInsertId() with one peculiarity: when $sequence value is null + * or empty string it returns empty string. But when parameter is not specified it's working + * as expected and returns actual last inserted ID (like the other PDO drivers). + * + * @param string|null $sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return !$sequence ? parent::lastInsertId() : parent::lastInsertId($sequence); + } +} From f2e6d408e3f8b4b029c8694599815047f59d3781 Mon Sep 17 00:00:00 2001 From: resurtm Date: Sat, 11 May 2013 09:40:04 +0600 Subject: [PATCH 2/8] MSSQL driver classes moved to the proper location. --- framework/db/mssql/PDO.php | 68 --------------------------- framework/db/mssql/QueryBuilder.php | 19 -------- framework/db/mssql/Schema.php | 93 ------------------------------------- framework/db/mssql/SqlsrvPDO.php | 33 ------------- 4 files changed, 213 deletions(-) delete mode 100644 framework/db/mssql/PDO.php delete mode 100644 framework/db/mssql/QueryBuilder.php delete mode 100644 framework/db/mssql/Schema.php delete mode 100644 framework/db/mssql/SqlsrvPDO.php diff --git a/framework/db/mssql/PDO.php b/framework/db/mssql/PDO.php deleted file mode 100644 index e045d68..0000000 --- a/framework/db/mssql/PDO.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @since 2.0 - */ -class PDO extends \PDO -{ - /** - * Returns last inserted ID value. - * - * @param string|null sequence the sequence name. Defaults to null. - * @return integer last inserted ID value. - */ - public function lastInsertId($sequence = null) - { - return $this->query('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS bigint)')->fetchColumn(); - } - - /** - * Begin a transaction. - * - * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transactions. - * - * @return boolean - */ - public function beginTransaction() - { - $this->exec('BEGIN TRANSACTION'); - return true; - } - - /** - * Commit a transaction. - * - * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transactions. - * - * @return boolean - */ - public function commit() - { - $this->exec('COMMIT TRANSACTION'); - return true; - } - - /** - * Rollback a transaction. - * - * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transaction. - * - * @return boolean - */ - public function rollBack() - { - $this->exec('ROLLBACK TRANSACTION'); - return true; - } -} diff --git a/framework/db/mssql/QueryBuilder.php b/framework/db/mssql/QueryBuilder.php deleted file mode 100644 index 988758b..0000000 --- a/framework/db/mssql/QueryBuilder.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @since 2.0 - */ -class QueryBuilder extends \yii\db\QueryBuilder -{ - // TODO: mssql driver -} diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php deleted file mode 100644 index 8fc8aea..0000000 --- a/framework/db/mssql/Schema.php +++ /dev/null @@ -1,93 +0,0 @@ - - * @since 2.0 - */ -class Schema extends \yii\db\Schema -{ - /** - * Default schema name to be used. - */ - const DEFAULT_SCHEMA = 'dbo'; - - /** - * @var array mapping from physical column types (keys) to abstract column types (values) - */ - public $typeMap = array( - // TODO: mssql driver - ); - - /** - * @param string $name - * @return TableSchema - */ - public function loadTableSchema($name) - { - return null; - } - - /** - * Quotes a table name for use in a query. - * A simple table name has no 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. - * A simple column name has no prefix. - * @param string $name column name. - * @return string the properly quoted column name. - */ - public function quoteSimpleColumnName($name) - { - return strpos($name, '[') !== false || $name === '*' ? $name : '[' . $name . ']'; - } - - /** - * Creates a query builder for the MSSQL database. - * @return QueryBuilder query builder interface. - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } - - /** - * 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. - * @return array all table names in the database. The names have NO the schema name prefix. - */ - protected function findTableNames($schema = '') - { - if ('' === $schema) { - $schema = self::DEFAULT_SCHEMA; - } - $sql = "SELECT TABLE_NAME FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'BASE TABLE'"; - $names = $this->db->createCommand($sql, array(':schema' => $schema))->queryColumn(); - if (self::DEFAULT_SCHEMA !== $schema) { - foreach ($names as $index => $name) { - $names[$index] = $schema . '.' . $name; - } - } - return $names; - } -} diff --git a/framework/db/mssql/SqlsrvPDO.php b/framework/db/mssql/SqlsrvPDO.php deleted file mode 100644 index 607d0e4..0000000 --- a/framework/db/mssql/SqlsrvPDO.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 2.0 - */ -class SqlsrvPDO extends \PDO -{ - /** - * Returns last inserted ID value. - * - * SQLSRV driver supports PDO::lastInsertId() with one peculiarity: when $sequence value is null - * or empty string it returns empty string. But when parameter is not specified it's working - * as expected and returns actual last inserted ID (like the other PDO drivers). - * - * @param string|null $sequence the sequence name. Defaults to null. - * @return integer last inserted ID value. - */ - public function lastInsertId($sequence = null) - { - return !$sequence ? parent::lastInsertId() : parent::lastInsertId($sequence); - } -} From 1ca7558c1df8e08486cc98600c3fee95dc2d4fb2 Mon Sep 17 00:00:00 2001 From: resurtm Date: Sat, 11 May 2013 11:34:32 +0600 Subject: [PATCH 3/8] MSSQL table metadata parsint (initial version). --- yii/db/mssql/PDO.php | 68 ++++++++++ yii/db/mssql/QueryBuilder.php | 40 ++++++ yii/db/mssql/Schema.php | 292 ++++++++++++++++++++++++++++++++++++++++++ yii/db/mssql/SqlsrvPDO.php | 33 +++++ 4 files changed, 433 insertions(+) create mode 100644 yii/db/mssql/PDO.php create mode 100644 yii/db/mssql/QueryBuilder.php create mode 100644 yii/db/mssql/Schema.php create mode 100644 yii/db/mssql/SqlsrvPDO.php diff --git a/yii/db/mssql/PDO.php b/yii/db/mssql/PDO.php new file mode 100644 index 0000000..e045d68 --- /dev/null +++ b/yii/db/mssql/PDO.php @@ -0,0 +1,68 @@ + + * @since 2.0 + */ +class PDO extends \PDO +{ + /** + * Returns last inserted ID value. + * + * @param string|null sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return $this->query('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS bigint)')->fetchColumn(); + } + + /** + * Begin a transaction. + * + * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transactions. + * + * @return boolean + */ + public function beginTransaction() + { + $this->exec('BEGIN TRANSACTION'); + return true; + } + + /** + * Commit a transaction. + * + * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transactions. + * + * @return boolean + */ + public function commit() + { + $this->exec('COMMIT TRANSACTION'); + return true; + } + + /** + * Rollback a transaction. + * + * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transaction. + * + * @return boolean + */ + public function rollBack() + { + $this->exec('ROLLBACK TRANSACTION'); + return true; + } +} diff --git a/yii/db/mssql/QueryBuilder.php b/yii/db/mssql/QueryBuilder.php new file mode 100644 index 0000000..f4f2088 --- /dev/null +++ b/yii/db/mssql/QueryBuilder.php @@ -0,0 +1,40 @@ + + * @author Christophe Boulain + * @author Timur Ruziev + * @since 2.0 + */ +class QueryBuilder extends \yii\db\QueryBuilder +{ + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = array( + Schema::TYPE_PK => 'int IDENTITY PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_SMALLINT => 'smallint(6)', + Schema::TYPE_INTEGER => 'int(11)', + Schema::TYPE_BIGINT => 'bigint(20)', + Schema::TYPE_FLOAT => 'float', + Schema::TYPE_DECIMAL => 'decimal', + Schema::TYPE_DATETIME => 'datetime', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'binary', + Schema::TYPE_BOOLEAN => 'tinyint(1)', + Schema::TYPE_MONEY => 'decimal(19,4)', + ); +} diff --git a/yii/db/mssql/Schema.php b/yii/db/mssql/Schema.php new file mode 100644 index 0000000..72171bd --- /dev/null +++ b/yii/db/mssql/Schema.php @@ -0,0 +1,292 @@ + + * @author Christophe Boulain + * @author Timur Ruziev + * @since 2.0 + */ +class Schema extends \yii\db\Schema +{ + /** + * Default schema name to be used. + */ + const DEFAULT_SCHEMA = 'dbo'; + + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + */ + public $typeMap = array( + // TODO: mssql driver + ); + + /** + * Quotes a table name for use in a query. + * A simple table name has no 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. + * A simple column name has no prefix. + * @param string $name column name. + * @return string the properly quoted column name. + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '[') !== false || $name === '*' ? $name : '[' . $name . ']'; + } + + /** + * Creates a query builder for the MSSQL database. + * @return QueryBuilder query builder interface. + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema driver dependent table metadata. Null if the table does not exist. + */ + public function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); + $this->findPrimaryKeys($table); + + if ($this->findColumns($table)) { + $this->findConstraints($table); + return $table; + } else { + return null; + } + } + + /** + * Collects the metadata of table columns. + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS'; + $whereSql = "t1.TABLE_NAME = '" . $table->name . "'"; + if ($table->catalogName !== null) { + $columnsTableName = $table->catalogName . '.' . $columnsTableName; + $whereSql .= " AND t1.TABLE_CATALOG = '" . $table->catalogName . "'"; + } + if ($table->schemaName !== null) { + $whereSql .= " AND t1.TABLE_SCHEMA = '" . $table->schemaName . "'"; + } + $columnsTableName = $this->quoteTableName($columnsTableName); + + $sql = <<db->createCommand($sql)->queryAll(); + } catch (\Exception $e) { + return false; + } + foreach ($columns as $column) { + $column = $this->loadColumnSchema($column); + if (is_array($table->primaryKey)) { + $column->isPrimaryKey = count(preg_grep('/' . preg_quote($column->name) . '/i', $table->primaryKey)) > 0; + } else { + $column->isPrimaryKey = strcasecmp($column->name, $table->primaryKey) === 0; + } + $table->columns[$column->name] = $column; + if ($column->isPrimaryKey && $column->autoIncrement) { + $table->sequenceName = ''; + } + } + return true; + } + + /** + * Collects the primary key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findPrimaryKeys($table) + { + $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE'; + $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS'; + if ($table->catalogName !== null) { + $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; + $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName; + } + $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); + $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName); + + $sql = <<primaryKey = $this->db + ->createCommand($sql, array(':tableName' => $table->name, ':schemaName' => $table->schemaName)) + ->queryColumn(); + if (count($table->primaryKey) == 0) { + // table does not have primary key + $table->primaryKey = null; + } elseif (count($table->primaryKey) == 1) { + // table have one primary key + $table->primaryKey = $table->primaryKey[0]; + } + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); + $column->name = $info['COLUMN_NAME']; + $column->comment = $info['Comment'] === null ? '' : $column['Comment']; + + $column->dbType = $info['DATA_TYPE']; + $column->unsigned = stripos($column->dbType, 'unsigned') !== false; + $column->allowNull = $info['IS_NULLABLE'] == 'YES'; + + $column->isPrimaryKey = null; // primary key is determined in findColumns() method + $column->autoIncrement = $info['IsIdentity'] == 1; + + $column->type = self::TYPE_STRING; + // TODO: better type infer + + $column->phpType = $this->getColumnPhpType($column); + return $column; + } + + /** + * Collects the foreign key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findConstraints($table) + { + $referentialConstraintsTableName = 'INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS'; + $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE'; + if ($table->catalogName !== null) { + $referentialConstraintsTableName = $table->catalogName . '.' . $referentialConstraintsTableName; + $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; + } + $referentialConstraintsTableName = $this->quoteTableName($referentialConstraintsTableName); + $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); + + // please refer to the following page for more details: + // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx + $sql = <<db->createCommand($sql, array(':tableName' => $table->name))->queryAll(); + $table->foreignKeys = array(); + foreach ($rows as $row) { + $table->foreignKeys[] = array($row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']); + } + } + + /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace(array('[', ']'), '', $name)); + $partCount = count($parts); + if ($partCount == 3) { + // catalog name, schema name and table name provided + $table->catalogName = $parts[0]; + $table->schemaName = $parts[1]; + $table->name = $parts[2]; + } elseif ($partCount == 2) { + // only schema name and table name provided + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + } else { + // only schema name provided + $table->schemaName = static::DEFAULT_SCHEMA; + $table->name = $parts[0]; + } + } + + /** + * 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. + * @return array all table names in the database. The names have NO the schema name prefix. + */ + protected function findTableNames($schema = '') + { + if ('' === $schema) { + $schema = self::DEFAULT_SCHEMA; + } + $sql = "SELECT TABLE_NAME FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'BASE TABLE'"; + $names = $this->db->createCommand($sql, array(':schema' => $schema))->queryColumn(); + if (self::DEFAULT_SCHEMA !== $schema) { + foreach ($names as $index => $name) { + $names[$index] = $schema . '.' . $name; + } + } + return $names; + } +} diff --git a/yii/db/mssql/SqlsrvPDO.php b/yii/db/mssql/SqlsrvPDO.php new file mode 100644 index 0000000..607d0e4 --- /dev/null +++ b/yii/db/mssql/SqlsrvPDO.php @@ -0,0 +1,33 @@ + + * @since 2.0 + */ +class SqlsrvPDO extends \PDO +{ + /** + * Returns last inserted ID value. + * + * SQLSRV driver supports PDO::lastInsertId() with one peculiarity: when $sequence value is null + * or empty string it returns empty string. But when parameter is not specified it's working + * as expected and returns actual last inserted ID (like the other PDO drivers). + * + * @param string|null $sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return !$sequence ? parent::lastInsertId() : parent::lastInsertId($sequence); + } +} From 58f3c323c00c161691bbf8e7fd3435b497e5d851 Mon Sep 17 00:00:00 2001 From: resurtm Date: Sat, 11 May 2013 12:22:14 +0600 Subject: [PATCH 4/8] MSSQL Schema: methods order changed, initial data type infer. --- yii/db/mssql/Schema.php | 176 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 118 insertions(+), 58 deletions(-) diff --git a/yii/db/mssql/Schema.php b/yii/db/mssql/Schema.php index 72171bd..a737070 100644 --- a/yii/db/mssql/Schema.php +++ b/yii/db/mssql/Schema.php @@ -9,7 +9,6 @@ namespace yii\db\mssql; use yii\db\TableSchema; use yii\db\ColumnSchema; -use yii\helpers\ArrayHelper; /** * Schema is the class for retrieving metadata from a MS SQL database (version 2008 and above). @@ -30,7 +29,31 @@ class Schema extends \yii\db\Schema * @var array mapping from physical column types (keys) to abstract column types (values) */ public $typeMap = array( - // TODO: mssql driver + 'tinyint' => self::TYPE_SMALLINT, + 'bit' => self::TYPE_SMALLINT, + 'smallint' => self::TYPE_SMALLINT, + 'mediumint' => self::TYPE_INTEGER, + 'int' => self::TYPE_INTEGER, + 'integer' => self::TYPE_INTEGER, + 'bigint' => self::TYPE_BIGINT, + 'float' => self::TYPE_FLOAT, + 'double' => self::TYPE_FLOAT, + 'real' => self::TYPE_FLOAT, + 'decimal' => self::TYPE_DECIMAL, + 'numeric' => self::TYPE_DECIMAL, + 'tinytext' => self::TYPE_TEXT, + 'mediumtext' => self::TYPE_TEXT, + 'longtext' => self::TYPE_TEXT, + 'text' => self::TYPE_TEXT, + 'varchar' => self::TYPE_STRING, + 'string' => self::TYPE_STRING, + 'char' => self::TYPE_STRING, + 'datetime' => self::TYPE_DATETIME, + 'year' => self::TYPE_DATE, + 'date' => self::TYPE_DATE, + 'time' => self::TYPE_TIME, + 'timestamp' => self::TYPE_TIMESTAMP, + 'enum' => self::TYPE_STRING, ); /** @@ -76,7 +99,7 @@ class Schema extends \yii\db\Schema $this->findPrimaryKeys($table); if ($this->findColumns($table)) { - $this->findConstraints($table); + $this->findForeignKeys($table); return $table; } else { return null; @@ -84,6 +107,84 @@ class Schema extends \yii\db\Schema } /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace(array('[', ']'), '', $name)); + $partCount = count($parts); + if ($partCount == 3) { + // catalog name, schema name and table name provided + $table->catalogName = $parts[0]; + $table->schemaName = $parts[1]; + $table->name = $parts[2]; + } elseif ($partCount == 2) { + // only schema name and table name provided + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + } else { + // only schema name provided + $table->schemaName = static::DEFAULT_SCHEMA; + $table->name = $parts[0]; + } + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); + + $column->name = $info['COLUMN_NAME']; + $column->comment = $info['Comment'] === null ? '' : $column['Comment']; + + $column->dbType = $info['DATA_TYPE']; + $column->unsigned = stripos($column->dbType, 'unsigned') !== false; + $column->allowNull = $info['IS_NULLABLE'] == 'YES'; + + $column->isPrimaryKey = null; // primary key is determined in findColumns() method + $column->autoIncrement = $info['IsIdentity'] == 1; + + $column->type = self::TYPE_STRING; + $matches = array(); + if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { + $type = $matches[1]; + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } + if (!empty($matches[2])) { + $values = explode(',', $matches[2]); + $column->size = $column->precision = (int)$values[0]; + if (isset($values[1])) { + $column->scale = (int)$values[1]; + } + if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { + $column->type = 'boolean'; + } elseif ($type === 'bit') { + if ($column->size > 32) { + $column->type = 'bigint'; + } elseif ($column->size === 32) { + $column->type = 'integer'; + } + } + } + } + + $column->phpType = $this->getColumnPhpType($column); + + if ($info['COLUMN_DEFAULT'] == '(NULL)') { + $column->defaultValue = null; + } + + return $column; + } + + /** * Collects the metadata of table columns. * @param TableSchema $table the table metadata * @return boolean whether the table exists in the database @@ -177,35 +278,10 @@ SQL; } /** - * Loads the column information into a [[ColumnSchema]] object. - * @param array $info column information - * @return ColumnSchema the column schema object - */ - protected function loadColumnSchema($info) - { - $column = new ColumnSchema(); - $column->name = $info['COLUMN_NAME']; - $column->comment = $info['Comment'] === null ? '' : $column['Comment']; - - $column->dbType = $info['DATA_TYPE']; - $column->unsigned = stripos($column->dbType, 'unsigned') !== false; - $column->allowNull = $info['IS_NULLABLE'] == 'YES'; - - $column->isPrimaryKey = null; // primary key is determined in findColumns() method - $column->autoIncrement = $info['IsIdentity'] == 1; - - $column->type = self::TYPE_STRING; - // TODO: better type infer - - $column->phpType = $this->getColumnPhpType($column); - return $column; - } - - /** * Collects the foreign key column details for the given table. * @param TableSchema $table the table metadata */ - protected function findConstraints($table) + protected function findForeignKeys($table) { $referentialConstraintsTableName = 'INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS'; $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE'; @@ -244,31 +320,6 @@ SQL; } /** - * Resolves the table name and schema name (if any). - * @param TableSchema $table the table metadata object - * @param string $name the table name - */ - protected function resolveTableNames($table, $name) - { - $parts = explode('.', str_replace(array('[', ']'), '', $name)); - $partCount = count($parts); - if ($partCount == 3) { - // catalog name, schema name and table name provided - $table->catalogName = $parts[0]; - $table->schemaName = $parts[1]; - $table->name = $parts[2]; - } elseif ($partCount == 2) { - // only schema name and table name provided - $table->schemaName = $parts[0]; - $table->name = $parts[1]; - } else { - // only schema name provided - $table->schemaName = static::DEFAULT_SCHEMA; - $table->name = $parts[0]; - } - } - - /** * 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. @@ -280,11 +331,20 @@ SQL; if ('' === $schema) { $schema = self::DEFAULT_SCHEMA; } - $sql = "SELECT TABLE_NAME FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = :schema AND TABLE_TYPE = 'BASE TABLE'"; + + $sql = <<db->createCommand($sql, array(':schema' => $schema))->queryColumn(); - if (self::DEFAULT_SCHEMA !== $schema) { - foreach ($names as $index => $name) { - $names[$index] = $schema . '.' . $name; + if (static::DEFAULT_SCHEMA !== $schema) { + foreach ($names as $i => $name) { + $names[$i] = $schema . '.' . $name; } } return $names; From 5c101ae3141bc266e5d32b4529d0a8644c29b72a Mon Sep 17 00:00:00 2001 From: resurtm Date: Sat, 11 May 2013 14:58:35 +0600 Subject: [PATCH 5/8] Initial MSSQL tests. --- tests/unit/data/config.php | 6 + tests/unit/data/mssql.sql | 374 +++++---------------- .../framework/db/mssql/MssqlActiveRecordTest.php | 12 + tests/unit/framework/db/mssql/MssqlCommandTest.php | 31 ++ .../framework/db/mssql/MssqlConnectionTest.php | 43 +++ tests/unit/framework/db/mssql/MssqlQueryTest.php | 12 + 6 files changed, 185 insertions(+), 293 deletions(-) create mode 100644 tests/unit/framework/db/mssql/MssqlActiveRecordTest.php create mode 100644 tests/unit/framework/db/mssql/MssqlCommandTest.php create mode 100644 tests/unit/framework/db/mssql/MssqlConnectionTest.php create mode 100644 tests/unit/framework/db/mssql/MssqlQueryTest.php diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index c980c57..04db5f4 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -12,5 +12,11 @@ return array( 'dsn' => 'sqlite::memory:', 'fixture' => __DIR__ . '/sqlite.sql', ), + 'sqlsrv' => array( + 'dsn' => 'sqlsrv:Server=localhost;Database=test', + 'username' => '', + 'password' => '', + 'fixture' => __DIR__ . '/mssql.sql', + ), ) ); diff --git a/tests/unit/data/mssql.sql b/tests/unit/data/mssql.sql index 38967b2..d3bc8d2 100644 --- a/tests/unit/data/mssql.sql +++ b/tests/unit/data/mssql.sql @@ -1,306 +1,94 @@ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[categories]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[categories]( +IF OBJECT_ID('[dbo].[tbl_order_item]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_order_item]; +IF OBJECT_ID('[dbo].[tbl_item]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_item]; +IF OBJECT_ID('[dbo].[tbl_order]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_order]; +IF OBJECT_ID('[dbo].[tbl_category]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_category]; +IF OBJECT_ID('[dbo].[tbl_customer]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_customer]; +IF OBJECT_ID('[dbo].[tbl_type]', 'U') IS NOT NULL DROP TABLE [dbo].[tbl_type]; + +CREATE TABLE [dbo].[tbl_customer] ( [id] [int] IDENTITY(1,1) NOT NULL, + [email] [varchar](128) NOT NULL, [name] [varchar](128) NOT NULL, - [parent_id] [int] NULL, - CONSTRAINT [PK_categories] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[orders]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[orders]( - [key1] [int] NOT NULL, - [key2] [int] NOT NULL, + [address] [text], + [status] [int] DEFAULT 0, + CONSTRAINT [PK_customer] PRIMARY KEY CLUSTERED ( + [id] ASC + ) ON [PRIMARY] +); + +CREATE TABLE [dbo].[tbl_category] ( + [id] [int] IDENTITY(1,1) NOT NULL, [name] [varchar](128) NOT NULL, - CONSTRAINT [PK_orders] PRIMARY KEY CLUSTERED -( - [key1] ASC, - [key2] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[types]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[types]( - [int_col] [int] NOT NULL, - [int_col2] [int] NULL CONSTRAINT [DF_types_int_col2] DEFAULT (1), - [char_col] [char](100) NOT NULL, - [char_col2] [varchar](100) NULL CONSTRAINT [DF_types_char_col2] DEFAULT ('something'), - [char_col3] [text] NULL, - [float_col] [real] NOT NULL, - [float_col2] [float] NULL CONSTRAINT [DF_types_float_col2] DEFAULT (1.23), - [blob_col] [image] NULL, - [numeric_col] [numeric](5, 2) NULL CONSTRAINT [DF_types_numeric_col] DEFAULT (33.22), - [time] [datetime] NULL CONSTRAINT [DF_types_time] DEFAULT ('2002-01-01 00:00:00'), - [bool_col] [bit] NOT NULL, - [bool_col2] [bit] NOT NULL CONSTRAINT [DF_types_bool_col2] DEFAULT (1) -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[users]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[users]( + CONSTRAINT [PK_category] PRIMARY KEY CLUSTERED ( + [id] ASC + ) ON [PRIMARY] +); + +CREATE TABLE [dbo].[tbl_item] ( [id] [int] IDENTITY(1,1) NOT NULL, - [username] [varchar](128) NOT NULL, - [password] [varchar](128) NOT NULL, - [email] [varchar](128) NOT NULL, - CONSTRAINT [PK_users] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[post_category]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[post_category]( + [name] [varchar](128) NOT NULL, [category_id] [int] NOT NULL, - [post_id] [int] NOT NULL, - CONSTRAINT [PK_post_category] PRIMARY KEY CLUSTERED -( - [category_id] ASC, - [post_id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[items]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[items]( - [id] [int] IDENTITY(1,1) NOT NULL, - [name] [varchar](128) NULL, - [col1] [int] NOT NULL, - [col2] [int] NOT NULL, - CONSTRAINT [PK_items] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[comments]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[comments]( - [id] [int] IDENTITY(1,1) NOT NULL, - [content] [text] NOT NULL, - [post_id] [int] NOT NULL, - [author_id] [int] NOT NULL, - CONSTRAINT [PK_comments] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[posts]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[posts]( - [id] [int] IDENTITY(1,1) NOT NULL, - [title] [varchar](128) NOT NULL, - [create_time] [datetime] NOT NULL, - [author_id] [int] NOT NULL, - [content] [text] NULL, - CONSTRAINT [PK_posts] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -END -GO -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[profiles]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) -BEGIN -CREATE TABLE [dbo].[profiles]( - [id] [int] IDENTITY(1,1) NOT NULL, - [first_name] [varchar](128) NOT NULL, - [last_name] [varchar](128) NOT NULL, - [user_id] [int] NOT NULL, - CONSTRAINT [PK_profiles] PRIMARY KEY CLUSTERED -( - [id] ASC -) ON [PRIMARY] -) ON [PRIMARY] -END -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_categories_categories]') AND type = 'F') -ALTER TABLE [dbo].[categories] WITH CHECK ADD CONSTRAINT [FK_categories_categories] FOREIGN KEY([parent_id]) -REFERENCES [dbo].[categories] ([id]) -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_post_category_categories]') AND type = 'F') -ALTER TABLE [dbo].[post_category] WITH CHECK ADD CONSTRAINT [FK_post_category_categories] FOREIGN KEY([category_id]) -REFERENCES [dbo].[categories] ([id]) -ON DELETE CASCADE -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_post_category_posts]') AND type = 'F') -ALTER TABLE [dbo].[post_category] WITH NOCHECK ADD CONSTRAINT [FK_post_category_posts] FOREIGN KEY([post_id]) -REFERENCES [dbo].[posts] ([id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[post_category] CHECK CONSTRAINT [FK_post_category_posts] -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_items_orders]') AND type = 'F') -ALTER TABLE [dbo].[items] WITH CHECK ADD CONSTRAINT [FK_items_orders] FOREIGN KEY([col1], [col2]) -REFERENCES [dbo].[orders] ([key1], [key2]) -ON DELETE CASCADE -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_comments_users]') AND type = 'F') -ALTER TABLE [dbo].[comments] WITH NOCHECK ADD CONSTRAINT [FK_comments_users] FOREIGN KEY([author_id]) -REFERENCES [dbo].[users] ([id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[comments] CHECK CONSTRAINT [FK_comments_users] -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_post_comment]') AND type = 'F') -ALTER TABLE [dbo].[comments] WITH NOCHECK ADD CONSTRAINT [FK_post_comment] FOREIGN KEY([post_id]) -REFERENCES [dbo].[posts] ([id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[comments] CHECK CONSTRAINT [FK_post_comment] -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_posts_users]') AND type = 'F') -ALTER TABLE [dbo].[posts] WITH NOCHECK ADD CONSTRAINT [FK_posts_users] FOREIGN KEY([author_id]) -REFERENCES [dbo].[users] ([id]) -GO -ALTER TABLE [dbo].[posts] CHECK CONSTRAINT [FK_posts_users] -GO -IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[dbo].[FK_profile_user]') AND type = 'F') -ALTER TABLE [dbo].[profiles] WITH NOCHECK ADD CONSTRAINT [FK_profile_user] FOREIGN KEY([user_id]) -REFERENCES [dbo].[users] ([id]) -ON DELETE CASCADE -GO -ALTER TABLE [dbo].[profiles] CHECK CONSTRAINT [FK_profile_user] - -INSERT INTO users (username, password, email) VALUES ('user1','pass1','email1') -GO -INSERT INTO users (username, password, email) VALUES ('user2','pass2','email2') -GO -INSERT INTO users (username, password, email) VALUES ('user3','pass3','email3') -GO + CONSTRAINT [PK_item] PRIMARY KEY CLUSTERED ( + [id] ASC + ) ON [PRIMARY] +); -INSERT INTO profiles (first_name, last_name, user_id) VALUES ('first 1','last 1',1) -GO -INSERT INTO profiles (first_name, last_name, user_id) VALUES ('first 2','last 2',2) -GO - -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 1','2000-01-01',1,'content 1') -GO -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 2','2000-01-02',2,'content 2') -GO -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 3','2000-01-03',2,'content 3') -GO -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 4','2000-01-04',2,'content 4') -GO -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 5','2000-01-05',3,'content 5') -GO +CREATE TABLE [dbo].[tbl_order] ( + [id] [int] IDENTITY(1,1) NOT NULL, + [customer_id] [int] NOT NULL, + [create_time] [int] NOT NULL, + [total] [decimal](10,0) NOT NULL, + CONSTRAINT [PK_order] PRIMARY KEY CLUSTERED ( + [id] ASC + ) ON [PRIMARY] +); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 1',1, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 2',1, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 3',1, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 4',2, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 5',2, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 6',3, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 7',3, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 8',3, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 9',3, 2) -GO -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 10',5, 3) -GO +CREATE TABLE [dbo].[tbl_order_item] ( + [order_id] [int] NOT NULL, + [item_id] [int] NOT NULL, + [quantity] [int] NOT NULL, + [subtotal] [decimal](10,0) NOT NULL, + CONSTRAINT [PK_order_item] PRIMARY KEY CLUSTERED ( + [order_id] ASC, + [item_id] ASC + ) ON [PRIMARY] +); -INSERT INTO categories (name, parent_id) VALUES ('cat 1',NULL) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 2',NULL) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 3',NULL) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 4',1) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 5',1) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 6',5) -GO -INSERT INTO categories (name, parent_id) VALUES ('cat 7',5) -GO +CREATE TABLE [dbo].[tbl_type] ( + [int_col] [int] NOT NULL, + [int_col2] [int] DEFAULT '1', + [char_col] [char](100) NOT NULL, + [char_col2] [varchar](100) DEFAULT 'something', + [char_col3] [text], + [float_col] [decimal](4,3) NOT NULL, + [float_col2] [float] DEFAULT '1.23', + [blob_col] [binary], + [numeric_col] [decimal](5,2) DEFAULT '33.22', + [time] [datetime] NOT NULL DEFAULT '2002-01-01 00:00:00', + [bool_col] [tinyint] NOT NULL, + [bool_col2] [tinyint] DEFAULT '1' +); -INSERT INTO post_category (category_id, post_id) VALUES (1,1) -GO -INSERT INTO post_category (category_id, post_id) VALUES (2,1) -GO -INSERT INTO post_category (category_id, post_id) VALUES (3,1) -GO -INSERT INTO post_category (category_id, post_id) VALUES (4,2) -GO -INSERT INTO post_category (category_id, post_id) VALUES (1,2) -GO -INSERT INTO post_category (category_id, post_id) VALUES (1,3) -GO +INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status]) VALUES ('user1@example.com', 'user1', 'address1', 1); +INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status]) VALUES ('user2@example.com', 'user2', 'address2', 1); +INSERT INTO [dbo].[tbl_customer] ([email], [name], [address], [status]) VALUES ('user3@example.com', 'user3', 'address3', 2); +INSERT INTO [dbo].[tbl_category] ([name]) VALUES ('Books'); +INSERT INTO [dbo].[tbl_category] ([name]) VALUES ('Movies'); -INSERT INTO orders (key1,key2,name) VALUES (1,2,'order 12') -GO -INSERT INTO orders (key1,key2,name) VALUES (1,3,'order 13') -GO -INSERT INTO orders (key1,key2,name) VALUES (2,1,'order 21') -GO -INSERT INTO orders (key1,key2,name) VALUES (2,2,'order 22') -GO +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Yii 1.1 Application Development Cookbook', 1); +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Ice Age', 2); +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Toy Story', 2); +INSERT INTO [dbo].[tbl_item] ([name], [category_id]) VALUES ('Cars', 2); +INSERT INTO [dbo].[tbl_order] ([customer_id], [create_time], [total]) VALUES (1, 1325282384, 110.0); +INSERT INTO [dbo].[tbl_order] ([customer_id], [create_time], [total]) VALUES (2, 1325334482, 33.0); +INSERT INTO [dbo].[tbl_order] ([customer_id], [create_time], [total]) VALUES (2, 1325502201, 40.0); -INSERT INTO items (name,col1,col2) VALUES ('item 1',1,2) -GO -INSERT INTO items (name,col1,col2) VALUES ('item 2',1,2) -GO -INSERT INTO items (name,col1,col2) VALUES ('item 3',1,3) -GO -INSERT INTO items (name,col1,col2) VALUES ('item 4',2,2) -GO -INSERT INTO items (name,col1,col2) VALUES ('item 5',2,2) -GO +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (1, 1, 1, 30.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (1, 2, 2, 40.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (2, 4, 1, 10.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (2, 5, 1, 15.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (2, 3, 1, 8.0); +INSERT INTO [dbo].[tbl_order_item] ([order_id], [item_id], [quantity], [subtotal]) VALUES (3, 2, 1, 40.0); diff --git a/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php b/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php new file mode 100644 index 0000000..4a41663 --- /dev/null +++ b/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php @@ -0,0 +1,12 @@ +driverName = 'sqlsrv'; + parent::setUp(); + } +} diff --git a/tests/unit/framework/db/mssql/MssqlCommandTest.php b/tests/unit/framework/db/mssql/MssqlCommandTest.php new file mode 100644 index 0000000..422a00c --- /dev/null +++ b/tests/unit/framework/db/mssql/MssqlCommandTest.php @@ -0,0 +1,31 @@ +driverName = 'sqlsrv'; + parent::setUp(); + } + + 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() + { + $this->markTestIncomplete(); + } + + function testBindParamValue() + { + $this->markTestIncomplete(); + } +} diff --git a/tests/unit/framework/db/mssql/MssqlConnectionTest.php b/tests/unit/framework/db/mssql/MssqlConnectionTest.php new file mode 100644 index 0000000..870af0a --- /dev/null +++ b/tests/unit/framework/db/mssql/MssqlConnectionTest.php @@ -0,0 +1,43 @@ +driverName = 'sqlsrv'; + parent::setUp(); + } + + function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } + + function testQuoteTableName() + { + $connection = $this->getConnection(false); + $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('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } + + function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $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('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } +} diff --git a/tests/unit/framework/db/mssql/MssqlQueryTest.php b/tests/unit/framework/db/mssql/MssqlQueryTest.php new file mode 100644 index 0000000..17d6681 --- /dev/null +++ b/tests/unit/framework/db/mssql/MssqlQueryTest.php @@ -0,0 +1,12 @@ +driverName = 'sqlsrv'; + parent::setUp(); + } +} From 90395b5df2cf02d5d20f72581f5b5f6d8d236907 Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 22 May 2013 21:34:41 +0600 Subject: [PATCH 6/8] Refinements to MSSQL driver classes. --- framework/yii/db/mssql/PDO.php | 61 ++++ framework/yii/db/mssql/QueryBuilder.php | 38 +++ framework/yii/db/mssql/Schema.php | 371 +++++++++++++++++++++ framework/yii/db/mssql/SqlsrvPDO.php | 33 ++ .../framework/db/mssql/MssqlActiveRecordTest.php | 12 - yii/db/mssql/PDO.php | 68 ---- yii/db/mssql/QueryBuilder.php | 40 --- yii/db/mssql/Schema.php | 352 ------------------- yii/db/mssql/SqlsrvPDO.php | 33 -- 9 files changed, 503 insertions(+), 505 deletions(-) create mode 100644 framework/yii/db/mssql/PDO.php create mode 100644 framework/yii/db/mssql/QueryBuilder.php create mode 100644 framework/yii/db/mssql/Schema.php create mode 100644 framework/yii/db/mssql/SqlsrvPDO.php delete mode 100644 tests/unit/framework/db/mssql/MssqlActiveRecordTest.php delete mode 100644 yii/db/mssql/PDO.php delete mode 100644 yii/db/mssql/QueryBuilder.php delete mode 100644 yii/db/mssql/Schema.php delete mode 100644 yii/db/mssql/SqlsrvPDO.php diff --git a/framework/yii/db/mssql/PDO.php b/framework/yii/db/mssql/PDO.php new file mode 100644 index 0000000..c29cd4c --- /dev/null +++ b/framework/yii/db/mssql/PDO.php @@ -0,0 +1,61 @@ + + * @since 2.0 + */ +class PDO extends \PDO +{ + /** + * Returns value of the last inserted ID. + * @param string|null $sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return $this->query('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS bigint)')->fetchColumn(); + } + + /** + * Starts a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not + * natively support transactions. + * @return boolean the result of a transaction start. + */ + public function beginTransaction() + { + $this->exec('BEGIN TRANSACTION'); + return true; + } + + /** + * Commits a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not + * natively support transactions. + * @return boolean the result of a transaction commit. + */ + public function commit() + { + $this->exec('COMMIT TRANSACTION'); + return true; + } + + /** + * Rollbacks a transaction. It is necessary to override PDO's method as MSSQL PDO driver does not + * natively support transactions. + * @return boolean the result of a transaction rollback. + */ + public function rollBack() + { + $this->exec('ROLLBACK TRANSACTION'); + return true; + } +} diff --git a/framework/yii/db/mssql/QueryBuilder.php b/framework/yii/db/mssql/QueryBuilder.php new file mode 100644 index 0000000..9ba27b2 --- /dev/null +++ b/framework/yii/db/mssql/QueryBuilder.php @@ -0,0 +1,38 @@ + + * @since 2.0 + */ +class QueryBuilder extends \yii\db\QueryBuilder +{ + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = array( + Schema::TYPE_PK => 'int IDENTITY PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'text', + Schema::TYPE_SMALLINT => 'smallint(6)', + Schema::TYPE_INTEGER => 'int(11)', + Schema::TYPE_BIGINT => 'bigint(20)', + Schema::TYPE_FLOAT => 'float', + Schema::TYPE_DECIMAL => 'decimal', + Schema::TYPE_DATETIME => 'datetime', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'binary', + Schema::TYPE_BOOLEAN => 'tinyint(1)', + Schema::TYPE_MONEY => 'decimal(19,4)', + ); +} diff --git a/framework/yii/db/mssql/Schema.php b/framework/yii/db/mssql/Schema.php new file mode 100644 index 0000000..f37ae8f --- /dev/null +++ b/framework/yii/db/mssql/Schema.php @@ -0,0 +1,371 @@ + + * @since 2.0 + */ +class Schema extends \yii\db\Schema +{ + /** + * Default schema name to be used. + */ + const DEFAULT_SCHEMA = 'dbo'; + + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + */ + public $typeMap = array( + // exact numerics + 'bigint' => self::TYPE_BIGINT, + 'numeric' => self::TYPE_DECIMAL, + 'bit' => self::TYPE_SMALLINT, + 'smallint' => self::TYPE_SMALLINT, + 'decimal' => self::TYPE_DECIMAL, + 'smallmoney' => self::TYPE_MONEY, + 'int' => self::TYPE_INTEGER, + 'tinyint' => self::TYPE_SMALLINT, + 'money' => self::TYPE_MONEY, + + // approximate numerics + 'float' => self::TYPE_FLOAT, + 'real' => self::TYPE_FLOAT, + + // date and time + 'date' => self::TYPE_DATE, + 'datetimeoffset' => self::TYPE_DATETIME, + 'datetime2' => self::TYPE_DATETIME, + 'smalldatetime' => self::TYPE_DATETIME, + 'datetime' => self::TYPE_DATETIME, + 'time' => self::TYPE_TIME, + + // character strings + 'char' => self::TYPE_STRING, + 'varchar' => self::TYPE_STRING, + 'text' => self::TYPE_TEXT, + + // unicode character strings + 'nchar' => self::TYPE_STRING, + 'nvarchar' => self::TYPE_STRING, + 'ntext' => self::TYPE_TEXT, + + // binary strings + 'binary' => self::TYPE_BINARY, + 'varbinary' => self::TYPE_BINARY, + 'image' => self::TYPE_BINARY, + + // other data types + // 'cursor' type cannot be used with tables + 'timestamp' => self::TYPE_TIMESTAMP, + 'hierarchyid' => self::TYPE_STRING, + 'uniqueidentifier' => self::TYPE_STRING, + 'sql_variant' => self::TYPE_STRING, + 'xml' => self::TYPE_STRING, + 'table' => self::TYPE_STRING, + ); + + /** + * Quotes a table name for use in a query. + * A simple table name has no 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. + * A simple column name has no prefix. + * @param string $name column name. + * @return string the properly quoted column name. + */ + public function quoteSimpleColumnName($name) + { + return strpos($name, '[') === false && $name !== '*' ? "[{$name}]" : $name; + } + + /** + * Creates a query builder for the MSSQL database. + * @return QueryBuilder query builder interface. + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + /** + * Loads the metadata for the specified table. + * @param string $name table name + * @return TableSchema|null driver dependent table metadata. Null if the table does not exist. + */ + public function loadTableSchema($name) + { + $table = new TableSchema(); + $this->resolveTableNames($table, $name); + $this->findPrimaryKeys($table); + if ($this->findColumns($table)) { + $this->findForeignKeys($table); + return $table; + } + } + + /** + * Resolves the table name and schema name (if any). + * @param TableSchema $table the table metadata object + * @param string $name the table name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace(array('[', ']'), '', $name)); + $partCount = count($parts); + if ($partCount == 3) { + // catalog name, schema name and table name passed + $table->catalogName = $parts[0]; + $table->schemaName = $parts[1]; + $table->name = $parts[2]; + } elseif ($partCount == 2) { + // only schema name and table name passed + $table->schemaName = $parts[0]; + $table->name = $parts[1]; + } else { + // only schema name passed + $table->schemaName = static::DEFAULT_SCHEMA; + $table->name = $parts[0]; + } + } + + /** + * Loads the column information into a [[ColumnSchema]] object. + * @param array $info column information + * @return ColumnSchema the column schema object + */ + protected function loadColumnSchema($info) + { + $column = new ColumnSchema(); + + $column->name = $info['COLUMN_NAME']; + $column->allowNull = $info['IS_NULLABLE'] == 'YES'; + $column->dbType = $info['DATA_TYPE']; + $column->enumValues = array(); // mssql has only vague equivalents to enum + $column->isPrimaryKey = null; // primary key will be determined in findColumns() method + $column->autoIncrement = $info['IsIdentity'] == 1; + $column->unsigned = stripos($column->dbType, 'unsigned') !== false; + $column->comment = $info['Comment'] === null ? '' : $column['Comment']; + + $column->type = self::TYPE_STRING; + if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { + $type = $matches[1]; + if (isset($this->typeMap[$type])) { + $column->type = $this->typeMap[$type]; + } + if (!empty($matches[2])) { + $values = explode(',', $matches[2]); + $column->size = $column->precision = (int)$values[0]; + if (isset($values[1])) { + $column->scale = (int)$values[1]; + } + if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { + $column->type = 'boolean'; + } elseif ($type === 'bit') { + if ($column->size > 32) { + $column->type = 'bigint'; + } elseif ($column->size === 32) { + $column->type = 'integer'; + } + } + } + } + + $column->phpType = $this->getColumnPhpType($column); + + if ($info['COLUMN_DEFAULT'] == '(NULL)') { + $info['COLUMN_DEFAULT'] = null; + } + if ($column->type !== 'timestamp' || $info['COLUMN_DEFAULT'] !== 'CURRENT_TIMESTAMP') { + $column->defaultValue = $column->typecast($info['COLUMN_DEFAULT']); + } + + return $column; + } + + /** + * Collects the metadata of table columns. + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + $columnsTableName = 'information_schema.columns'; + $whereSql = "[t1].[table_name] = '{$table->name}'"; + if ($table->catalogName !== null) { + $columnsTableName = "{$table->catalogName}.{$columnsTableName}"; + $whereSql .= " AND [t1].[table_catalog] = '{$table->catalogName}'"; + } + if ($table->schemaName !== null) { + $whereSql .= " AND [t1].[table_schema] = '{$table->schemaName}'"; + } + $columnsTableName = $this->quoteTableName($columnsTableName); + + $sql = <<db->createCommand($sql)->queryAll(); + } catch (\Exception $e) { + return false; + } + foreach ($columns as $column) { + $column = $this->loadColumnSchema($column); + if (is_array($table->primaryKey)) { + foreach ($table->primaryKey as $primaryKeyColumn) { + if (strcasecmp($column->name, $primaryKeyColumn) === 0) { + $column->isPrimaryKey = true; + break; + } + } + } else { + $column->isPrimaryKey = strcasecmp($column->name, $table->primaryKey) === 0; + } + if ($column->isPrimaryKey && $column->autoIncrement) { + $table->sequenceName = ''; + } + $table->columns[$column->name] = $column; + } + return true; + } + + /** + * Collects the primary key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findPrimaryKeys($table) + { + $keyColumnUsageTableName = 'information_schema.key_column_usage'; + $tableConstraintsTableName = 'information_schema.table_constraints'; + if ($table->catalogName !== null) { + $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; + $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName; + } + $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); + $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName); + + $sql = <<primaryKey = $this->db + ->createCommand($sql, array(':tableName' => $table->name, ':schemaName' => $table->schemaName)) + ->queryColumn(); + if (count($table->primaryKey) == 0) { + // table does not have primary key + $table->primaryKey = null; + } elseif (count($table->primaryKey) == 1) { + // table has one primary key + $table->primaryKey = $table->primaryKey[0]; + } + } + + /** + * Collects the foreign key column details for the given table. + * @param TableSchema $table the table metadata + */ + protected function findForeignKeys($table) + { + $referentialConstraintsTableName = 'information_schema.referential_constraints'; + $keyColumnUsageTableName = 'information_schema.key_column_usage'; + if ($table->catalogName !== null) { + $referentialConstraintsTableName = $table->catalogName . '.' . $referentialConstraintsTableName; + $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; + } + $referentialConstraintsTableName = $this->quoteTableName($referentialConstraintsTableName); + $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); + + // please refer to the following page for more details: + // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx + $sql = <<db->createCommand($sql, array(':tableName' => $table->name))->queryAll(); + $table->foreignKeys = array(); + foreach ($rows as $row) { + $table->foreignKeys[] = array($row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']); + } + } + + /** + * 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. + * @return array all table names in the database. The names have NO the schema name prefix. + */ + protected function findTableNames($schema = '') + { + if ($schema === '') { + $schema = static::DEFAULT_SCHEMA; + } + + $sql = <<db->createCommand($sql, array(':schema' => $schema))->queryColumn(); + if ($schema !== static::DEFAULT_SCHEMA) { + foreach ($names as $index => $name) { + $names[$index] = $schema . '.' . $name; + } + } + return $names; + } +} diff --git a/framework/yii/db/mssql/SqlsrvPDO.php b/framework/yii/db/mssql/SqlsrvPDO.php new file mode 100644 index 0000000..29444c5 --- /dev/null +++ b/framework/yii/db/mssql/SqlsrvPDO.php @@ -0,0 +1,33 @@ + + * @since 2.0 + */ +class SqlsrvPDO extends \PDO +{ + /** + * Returns value of the last inserted ID. + * + * SQLSRV driver implements [[PDO::lastInsertId()]] method but with a single peculiarity: + * when `$sequence` value is a null or an empty string it returns an empty string. + * But when parameter is not specified it works as expected and returns actual + * last inserted ID (like the other PDO drivers). + * @param string|null $sequence the sequence name. Defaults to null. + * @return integer last inserted ID value. + */ + public function lastInsertId($sequence = null) + { + return !$sequence ? parent::lastInsertId() : parent::lastInsertId($sequence); + } +} diff --git a/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php b/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php deleted file mode 100644 index 4a41663..0000000 --- a/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php +++ /dev/null @@ -1,12 +0,0 @@ -driverName = 'sqlsrv'; - parent::setUp(); - } -} diff --git a/yii/db/mssql/PDO.php b/yii/db/mssql/PDO.php deleted file mode 100644 index e045d68..0000000 --- a/yii/db/mssql/PDO.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @since 2.0 - */ -class PDO extends \PDO -{ - /** - * Returns last inserted ID value. - * - * @param string|null sequence the sequence name. Defaults to null. - * @return integer last inserted ID value. - */ - public function lastInsertId($sequence = null) - { - return $this->query('SELECT CAST(COALESCE(SCOPE_IDENTITY(), @@IDENTITY) AS bigint)')->fetchColumn(); - } - - /** - * Begin a transaction. - * - * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transactions. - * - * @return boolean - */ - public function beginTransaction() - { - $this->exec('BEGIN TRANSACTION'); - return true; - } - - /** - * Commit a transaction. - * - * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transactions. - * - * @return boolean - */ - public function commit() - { - $this->exec('COMMIT TRANSACTION'); - return true; - } - - /** - * Rollback a transaction. - * - * Is is necessary to override PDO's method as MSSQL PDO drivers does not support transaction. - * - * @return boolean - */ - public function rollBack() - { - $this->exec('ROLLBACK TRANSACTION'); - return true; - } -} diff --git a/yii/db/mssql/QueryBuilder.php b/yii/db/mssql/QueryBuilder.php deleted file mode 100644 index f4f2088..0000000 --- a/yii/db/mssql/QueryBuilder.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @author Christophe Boulain - * @author Timur Ruziev - * @since 2.0 - */ -class QueryBuilder extends \yii\db\QueryBuilder -{ - /** - * @var array mapping from abstract column types (keys) to physical column types (values). - */ - public $typeMap = array( - Schema::TYPE_PK => 'int IDENTITY PRIMARY KEY', - Schema::TYPE_STRING => 'varchar(255)', - Schema::TYPE_TEXT => 'text', - Schema::TYPE_SMALLINT => 'smallint(6)', - Schema::TYPE_INTEGER => 'int(11)', - Schema::TYPE_BIGINT => 'bigint(20)', - Schema::TYPE_FLOAT => 'float', - Schema::TYPE_DECIMAL => 'decimal', - Schema::TYPE_DATETIME => 'datetime', - Schema::TYPE_TIMESTAMP => 'timestamp', - Schema::TYPE_TIME => 'time', - Schema::TYPE_DATE => 'date', - Schema::TYPE_BINARY => 'binary', - Schema::TYPE_BOOLEAN => 'tinyint(1)', - Schema::TYPE_MONEY => 'decimal(19,4)', - ); -} diff --git a/yii/db/mssql/Schema.php b/yii/db/mssql/Schema.php deleted file mode 100644 index a737070..0000000 --- a/yii/db/mssql/Schema.php +++ /dev/null @@ -1,352 +0,0 @@ - - * @author Christophe Boulain - * @author Timur Ruziev - * @since 2.0 - */ -class Schema extends \yii\db\Schema -{ - /** - * Default schema name to be used. - */ - const DEFAULT_SCHEMA = 'dbo'; - - /** - * @var array mapping from physical column types (keys) to abstract column types (values) - */ - public $typeMap = array( - 'tinyint' => self::TYPE_SMALLINT, - 'bit' => self::TYPE_SMALLINT, - 'smallint' => self::TYPE_SMALLINT, - 'mediumint' => self::TYPE_INTEGER, - 'int' => self::TYPE_INTEGER, - 'integer' => self::TYPE_INTEGER, - 'bigint' => self::TYPE_BIGINT, - 'float' => self::TYPE_FLOAT, - 'double' => self::TYPE_FLOAT, - 'real' => self::TYPE_FLOAT, - 'decimal' => self::TYPE_DECIMAL, - 'numeric' => self::TYPE_DECIMAL, - 'tinytext' => self::TYPE_TEXT, - 'mediumtext' => self::TYPE_TEXT, - 'longtext' => self::TYPE_TEXT, - 'text' => self::TYPE_TEXT, - 'varchar' => self::TYPE_STRING, - 'string' => self::TYPE_STRING, - 'char' => self::TYPE_STRING, - 'datetime' => self::TYPE_DATETIME, - 'year' => self::TYPE_DATE, - 'date' => self::TYPE_DATE, - 'time' => self::TYPE_TIME, - 'timestamp' => self::TYPE_TIMESTAMP, - 'enum' => self::TYPE_STRING, - ); - - /** - * Quotes a table name for use in a query. - * A simple table name has no 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. - * A simple column name has no prefix. - * @param string $name column name. - * @return string the properly quoted column name. - */ - public function quoteSimpleColumnName($name) - { - return strpos($name, '[') !== false || $name === '*' ? $name : '[' . $name . ']'; - } - - /** - * Creates a query builder for the MSSQL database. - * @return QueryBuilder query builder interface. - */ - public function createQueryBuilder() - { - return new QueryBuilder($this->db); - } - - /** - * Loads the metadata for the specified table. - * @param string $name table name - * @return TableSchema driver dependent table metadata. Null if the table does not exist. - */ - public function loadTableSchema($name) - { - $table = new TableSchema(); - $this->resolveTableNames($table, $name); - $this->findPrimaryKeys($table); - - if ($this->findColumns($table)) { - $this->findForeignKeys($table); - return $table; - } else { - return null; - } - } - - /** - * Resolves the table name and schema name (if any). - * @param TableSchema $table the table metadata object - * @param string $name the table name - */ - protected function resolveTableNames($table, $name) - { - $parts = explode('.', str_replace(array('[', ']'), '', $name)); - $partCount = count($parts); - if ($partCount == 3) { - // catalog name, schema name and table name provided - $table->catalogName = $parts[0]; - $table->schemaName = $parts[1]; - $table->name = $parts[2]; - } elseif ($partCount == 2) { - // only schema name and table name provided - $table->schemaName = $parts[0]; - $table->name = $parts[1]; - } else { - // only schema name provided - $table->schemaName = static::DEFAULT_SCHEMA; - $table->name = $parts[0]; - } - } - - /** - * Loads the column information into a [[ColumnSchema]] object. - * @param array $info column information - * @return ColumnSchema the column schema object - */ - protected function loadColumnSchema($info) - { - $column = new ColumnSchema(); - - $column->name = $info['COLUMN_NAME']; - $column->comment = $info['Comment'] === null ? '' : $column['Comment']; - - $column->dbType = $info['DATA_TYPE']; - $column->unsigned = stripos($column->dbType, 'unsigned') !== false; - $column->allowNull = $info['IS_NULLABLE'] == 'YES'; - - $column->isPrimaryKey = null; // primary key is determined in findColumns() method - $column->autoIncrement = $info['IsIdentity'] == 1; - - $column->type = self::TYPE_STRING; - $matches = array(); - if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { - $type = $matches[1]; - if (isset($this->typeMap[$type])) { - $column->type = $this->typeMap[$type]; - } - if (!empty($matches[2])) { - $values = explode(',', $matches[2]); - $column->size = $column->precision = (int)$values[0]; - if (isset($values[1])) { - $column->scale = (int)$values[1]; - } - if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { - $column->type = 'boolean'; - } elseif ($type === 'bit') { - if ($column->size > 32) { - $column->type = 'bigint'; - } elseif ($column->size === 32) { - $column->type = 'integer'; - } - } - } - } - - $column->phpType = $this->getColumnPhpType($column); - - if ($info['COLUMN_DEFAULT'] == '(NULL)') { - $column->defaultValue = null; - } - - return $column; - } - - /** - * Collects the metadata of table columns. - * @param TableSchema $table the table metadata - * @return boolean whether the table exists in the database - */ - protected function findColumns($table) - { - $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS'; - $whereSql = "t1.TABLE_NAME = '" . $table->name . "'"; - if ($table->catalogName !== null) { - $columnsTableName = $table->catalogName . '.' . $columnsTableName; - $whereSql .= " AND t1.TABLE_CATALOG = '" . $table->catalogName . "'"; - } - if ($table->schemaName !== null) { - $whereSql .= " AND t1.TABLE_SCHEMA = '" . $table->schemaName . "'"; - } - $columnsTableName = $this->quoteTableName($columnsTableName); - - $sql = <<db->createCommand($sql)->queryAll(); - } catch (\Exception $e) { - return false; - } - foreach ($columns as $column) { - $column = $this->loadColumnSchema($column); - if (is_array($table->primaryKey)) { - $column->isPrimaryKey = count(preg_grep('/' . preg_quote($column->name) . '/i', $table->primaryKey)) > 0; - } else { - $column->isPrimaryKey = strcasecmp($column->name, $table->primaryKey) === 0; - } - $table->columns[$column->name] = $column; - if ($column->isPrimaryKey && $column->autoIncrement) { - $table->sequenceName = ''; - } - } - return true; - } - - /** - * Collects the primary key column details for the given table. - * @param TableSchema $table the table metadata - */ - protected function findPrimaryKeys($table) - { - $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE'; - $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS'; - if ($table->catalogName !== null) { - $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; - $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName; - } - $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); - $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName); - - $sql = <<primaryKey = $this->db - ->createCommand($sql, array(':tableName' => $table->name, ':schemaName' => $table->schemaName)) - ->queryColumn(); - if (count($table->primaryKey) == 0) { - // table does not have primary key - $table->primaryKey = null; - } elseif (count($table->primaryKey) == 1) { - // table have one primary key - $table->primaryKey = $table->primaryKey[0]; - } - } - - /** - * Collects the foreign key column details for the given table. - * @param TableSchema $table the table metadata - */ - protected function findForeignKeys($table) - { - $referentialConstraintsTableName = 'INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS'; - $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE'; - if ($table->catalogName !== null) { - $referentialConstraintsTableName = $table->catalogName . '.' . $referentialConstraintsTableName; - $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; - } - $referentialConstraintsTableName = $this->quoteTableName($referentialConstraintsTableName); - $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); - - // please refer to the following page for more details: - // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx - $sql = <<db->createCommand($sql, array(':tableName' => $table->name))->queryAll(); - $table->foreignKeys = array(); - foreach ($rows as $row) { - $table->foreignKeys[] = array($row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']); - } - } - - /** - * 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. - * @return array all table names in the database. The names have NO the schema name prefix. - */ - protected function findTableNames($schema = '') - { - if ('' === $schema) { - $schema = self::DEFAULT_SCHEMA; - } - - $sql = <<db->createCommand($sql, array(':schema' => $schema))->queryColumn(); - if (static::DEFAULT_SCHEMA !== $schema) { - foreach ($names as $i => $name) { - $names[$i] = $schema . '.' . $name; - } - } - return $names; - } -} diff --git a/yii/db/mssql/SqlsrvPDO.php b/yii/db/mssql/SqlsrvPDO.php deleted file mode 100644 index 607d0e4..0000000 --- a/yii/db/mssql/SqlsrvPDO.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 2.0 - */ -class SqlsrvPDO extends \PDO -{ - /** - * Returns last inserted ID value. - * - * SQLSRV driver supports PDO::lastInsertId() with one peculiarity: when $sequence value is null - * or empty string it returns empty string. But when parameter is not specified it's working - * as expected and returns actual last inserted ID (like the other PDO drivers). - * - * @param string|null $sequence the sequence name. Defaults to null. - * @return integer last inserted ID value. - */ - public function lastInsertId($sequence = null) - { - return !$sequence ? parent::lastInsertId() : parent::lastInsertId($sequence); - } -} From 1f9a46850cef5975171ba051c85703ca23a78202 Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 22 May 2013 22:25:49 +0600 Subject: [PATCH 7/8] MSSQL initial query builder. --- framework/yii/db/QueryBuilder.php | 3 ++- framework/yii/db/mssql/QueryBuilder.php | 43 ++++++++++++++++++++++++++++++++ framework/yii/db/mysql/QueryBuilder.php | 7 +++--- framework/yii/db/sqlite/QueryBuilder.php | 5 ++-- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php index 9e7749a..c0b4223 100644 --- a/framework/yii/db/QueryBuilder.php +++ b/framework/yii/db/QueryBuilder.php @@ -429,10 +429,11 @@ class QueryBuilder extends \yii\base\Object * Builds a SQL statement for enabling or disabling integrity check. * @param boolean $check whether to turn on or off the integrity check. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $table the table name. Defaults to empty string, meaning that no table will be changed. * @return string the SQL statement for checking integrity * @throws NotSupportedException if this is not supported by the underlying DBMS */ - public function checkIntegrity($check = true, $schema = '') + public function checkIntegrity($check = true, $schema = '', $table = '') { throw new NotSupportedException($this->db->getDriverName() . ' does not support enabling/disabling integrity check.'); } diff --git a/framework/yii/db/mssql/QueryBuilder.php b/framework/yii/db/mssql/QueryBuilder.php index 9ba27b2..45a7507 100644 --- a/framework/yii/db/mssql/QueryBuilder.php +++ b/framework/yii/db/mssql/QueryBuilder.php @@ -7,6 +7,8 @@ namespace yii\db\mssql; +use yii\base\InvalidParamException; + /** * QueryBuilder is the query builder for MS SQL Server databases (version 2008 and above). * @@ -35,4 +37,45 @@ class QueryBuilder extends \yii\db\QueryBuilder Schema::TYPE_BOOLEAN => 'tinyint(1)', Schema::TYPE_MONEY => 'decimal(19,4)', ); + +// public function update($table, $columns, $condition, &$params) +// { +// return ''; +// } + +// public function delete($table, $condition, &$params) +// { +// return ''; +// } + +// public function buildLimit($limit, $offset) +// { +// return ''; +// } + +// public function resetSequence($table, $value = null) +// { +// return ''; +// } + + /** + * Builds a SQL statement for enabling or disabling integrity check. + * @param boolean $check whether to turn on or off the integrity check. + * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $table the table name. Defaults to empty string, meaning that no table will be changed. + * @return string the SQL statement for checking integrity + * @throws InvalidParamException if the table does not exist or there is no sequence associated with the table. + */ + public function checkIntegrity($check = true, $schema = '', $table = '') + { + if ($schema !== '') { + $table = "{$schema}.{$table}"; + } + $table = $this->db->quoteTableName($table); + if ($this->db->getTableSchema($table) === null) { + throw new InvalidParamException("Table not found: $table"); + } + $enable = $check ? 'CHECK' : 'NOCHECK'; + return "ALTER TABLE {$table} {$enable} CONSTRAINT ALL"; + } } diff --git a/framework/yii/db/mysql/QueryBuilder.php b/framework/yii/db/mysql/QueryBuilder.php index a078b9a..7bf293b 100644 --- a/framework/yii/db/mysql/QueryBuilder.php +++ b/framework/yii/db/mysql/QueryBuilder.php @@ -120,12 +120,13 @@ class QueryBuilder extends \yii\db\QueryBuilder /** * Builds a SQL statement for enabling or disabling integrity check. * @param boolean $check whether to turn on or off the integrity check. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $table the table name. Meaningless for MySQL. + * @param string $schema the schema of the tables. Meaningless for MySQL. * @return string the SQL statement for checking integrity */ - public function checkIntegrity($check = true, $schema = '') + public function checkIntegrity($check = true, $schema = '', $table = '') { - return 'SET FOREIGN_KEY_CHECKS=' . ($check ? 1 : 0); + return 'SET FOREIGN_KEY_CHECKS = ' . ($check ? 1 : 0); } /** diff --git a/framework/yii/db/sqlite/QueryBuilder.php b/framework/yii/db/sqlite/QueryBuilder.php index 3aa89e7..72d48f4 100644 --- a/framework/yii/db/sqlite/QueryBuilder.php +++ b/framework/yii/db/sqlite/QueryBuilder.php @@ -77,10 +77,11 @@ class QueryBuilder extends \yii\db\QueryBuilder /** * Enables or disables integrity check. * @param boolean $check whether to turn on or off the integrity check. - * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema. + * @param string $schema the schema of the tables. Meaningless for SQLite. + * @param string $table the table name. Meaningless for SQLite. * @throws NotSupportedException this is not supported by SQLite */ - public function checkIntegrity($check = true, $schema = '') + public function checkIntegrity($check = true, $schema = '', $table = '') { throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); } From 2430ebe95d7a2a639e4f7dd1cefaa62a4e13ef12 Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 22 May 2013 22:37:09 +0600 Subject: [PATCH 8/8] MSSQL AR tests. --- framework/yii/db/mssql/Schema.php | 7 ------- tests/unit/framework/db/mssql/MssqlActiveRecordTest.php | 12 ++++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 tests/unit/framework/db/mssql/MssqlActiveRecordTest.php diff --git a/framework/yii/db/mssql/Schema.php b/framework/yii/db/mssql/Schema.php index f37ae8f..c52dfc3 100644 --- a/framework/yii/db/mssql/Schema.php +++ b/framework/yii/db/mssql/Schema.php @@ -290,13 +290,6 @@ SQL; $table->primaryKey = $this->db ->createCommand($sql, array(':tableName' => $table->name, ':schemaName' => $table->schemaName)) ->queryColumn(); - if (count($table->primaryKey) == 0) { - // table does not have primary key - $table->primaryKey = null; - } elseif (count($table->primaryKey) == 1) { - // table has one primary key - $table->primaryKey = $table->primaryKey[0]; - } } /** diff --git a/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php b/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php new file mode 100644 index 0000000..2cf0b01 --- /dev/null +++ b/tests/unit/framework/db/mssql/MssqlActiveRecordTest.php @@ -0,0 +1,12 @@ +driverName = 'sqlsrv'; + parent::setUp(); + } +}