From 1ca7558c1df8e08486cc98600c3fee95dc2d4fb2 Mon Sep 17 00:00:00 2001 From: resurtm Date: Sat, 11 May 2013 11:34:32 +0600 Subject: [PATCH] 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); + } +}