* @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 ? '' : $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->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(); } /** * 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; } }