* @since 2.0 */ class Schema extends \yii\db\Schema { /** * @var string the default schema used for the current session. */ public $defaultSchema = 'dbo'; /** * @var array mapping from physical column types (keys) to abstract column types (values) */ public $typeMap = [ // exact numbers '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 numbers 'float' => self::TYPE_FLOAT, 'double' => self::TYPE_DOUBLE, '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_CHAR, 'varchar' => self::TYPE_STRING, 'text' => self::TYPE_TEXT, // unicode character strings 'nchar' => self::TYPE_CHAR, '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, ]; /** * @inheritdoc */ public function createSavepoint($name) { $this->db->createCommand("SAVE TRANSACTION $name")->execute(); } /** * @inheritdoc */ public function releaseSavepoint($name) { // does nothing as MSSQL does not support this } /** * @inheritdoc */ public function rollBackSavepoint($name) { $this->db->createCommand("ROLLBACK TRANSACTION $name")->execute(); } /** * 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; } 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(['[', ']'], '', $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]; $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name; } elseif ($partCount === 2) { // only schema name and table name passed $table->schemaName = $parts[0]; $table->name = $parts[1]; $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; } else { // only table name passed $table->schemaName = $this->defaultSchema; $table->fullName = $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 = $this->createColumnSchema(); $column->name = $info['column_name']; $column->allowNull = $info['is_nullable'] === 'YES'; $column->dbType = $info['data_type']; $column->enumValues = []; // mssql has only vague equivalents to enum $column->isPrimaryKey = null; // primary key will be determined in findColumns() method $column->autoIncrement = $info['is_identity'] == 1; $column->unsigned = stripos($column->dbType, 'unsigned') !== false; $column->comment = $info['comment'] === null ? '' : $info['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->isPrimaryKey && ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) { $column->defaultValue = $column->phpTypecast($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(); if (empty($columns)) { return false; } } catch (\Exception $e) { return false; } foreach ($columns as $column) { $column = $this->loadColumnSchema($column); foreach ($table->primaryKey as $primaryKey) { if (strcasecmp($column->name, $primaryKey) === 0) { $column->isPrimaryKey = true; break; } } if ($column->isPrimaryKey && $column->autoIncrement) { $table->sequenceName = ''; } $table->columns[$column->name] = $column; } return true; } /** * Collects the constraint details for the given table and constraint type. * @param TableSchema $table * @param string $type either PRIMARY KEY or UNIQUE * @return array each entry contains index_name and field_name * @since 2.0.4 */ protected function findTableConstraints($table, $type) { $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 = <<db ->createCommand($sql, [ ':tableName' => $table->name, ':schemaName' => $table->schemaName, ':type' => $type, ]) ->queryAll(); } /** * Collects the primary key column details for the given table. * @param TableSchema $table the table metadata */ protected function findPrimaryKeys($table) { $result = []; foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) { $result[] = $row['field_name']; } $table->primaryKey = $result; } /** * 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, [ ':tableName' => $table->name, ':schemaName' => $table->schemaName, ])->queryAll(); $table->foreignKeys = []; foreach ($rows as $row) { $table->foreignKeys[] = [$row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']]; } } /** * Returns all table names in the database. * @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 schema name prefix. */ protected function findTableNames($schema = '') { if ($schema === '') { $schema = $this->defaultSchema; } $sql = <<db->createCommand($sql, [':schema' => $schema])->queryColumn(); } /** * Returns all unique indexes for the given table. * Each array element is of the following structure: * * ```php * [ * 'IndexName1' => ['col1' [, ...]], * 'IndexName2' => ['col2' [, ...]], * ] * ``` * * @param TableSchema $table the table metadata * @return array all unique indexes for the given table. * @since 2.0.4 */ public function findUniqueIndexes($table) { $result = []; foreach ($this->findTableConstraints($table, 'UNIQUE') as $row) { $result[$row['index_name']][] = $row['field_name']; } return $result; } }