Schema.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\db\mssql;
  8. use yii\db\ColumnSchema;
  9. use yii\db\ViewFinderTrait;
  10. /**
  11. * Schema is the class for retrieving metadata from a MS SQL Server databases (version 2008 and above).
  12. *
  13. * @author Timur Ruziev <resurtm@gmail.com>
  14. * @since 2.0
  15. */
  16. class Schema extends \yii\db\Schema
  17. {
  18. use ViewFinderTrait;
  19. /**
  20. * @var string the default schema used for the current session.
  21. */
  22. public $defaultSchema = 'dbo';
  23. /**
  24. * @var array mapping from physical column types (keys) to abstract column types (values)
  25. */
  26. public $typeMap = [
  27. // exact numbers
  28. 'bigint' => self::TYPE_BIGINT,
  29. 'numeric' => self::TYPE_DECIMAL,
  30. 'bit' => self::TYPE_SMALLINT,
  31. 'smallint' => self::TYPE_SMALLINT,
  32. 'decimal' => self::TYPE_DECIMAL,
  33. 'smallmoney' => self::TYPE_MONEY,
  34. 'int' => self::TYPE_INTEGER,
  35. 'tinyint' => self::TYPE_SMALLINT,
  36. 'money' => self::TYPE_MONEY,
  37. // approximate numbers
  38. 'float' => self::TYPE_FLOAT,
  39. 'double' => self::TYPE_DOUBLE,
  40. 'real' => self::TYPE_FLOAT,
  41. // date and time
  42. 'date' => self::TYPE_DATE,
  43. 'datetimeoffset' => self::TYPE_DATETIME,
  44. 'datetime2' => self::TYPE_DATETIME,
  45. 'smalldatetime' => self::TYPE_DATETIME,
  46. 'datetime' => self::TYPE_DATETIME,
  47. 'time' => self::TYPE_TIME,
  48. // character strings
  49. 'char' => self::TYPE_CHAR,
  50. 'varchar' => self::TYPE_STRING,
  51. 'text' => self::TYPE_TEXT,
  52. // unicode character strings
  53. 'nchar' => self::TYPE_CHAR,
  54. 'nvarchar' => self::TYPE_STRING,
  55. 'ntext' => self::TYPE_TEXT,
  56. // binary strings
  57. 'binary' => self::TYPE_BINARY,
  58. 'varbinary' => self::TYPE_BINARY,
  59. 'image' => self::TYPE_BINARY,
  60. // other data types
  61. // 'cursor' type cannot be used with tables
  62. 'timestamp' => self::TYPE_TIMESTAMP,
  63. 'hierarchyid' => self::TYPE_STRING,
  64. 'uniqueidentifier' => self::TYPE_STRING,
  65. 'sql_variant' => self::TYPE_STRING,
  66. 'xml' => self::TYPE_STRING,
  67. 'table' => self::TYPE_STRING,
  68. ];
  69. /**
  70. * @inheritdoc
  71. */
  72. public function createSavepoint($name)
  73. {
  74. $this->db->createCommand("SAVE TRANSACTION $name")->execute();
  75. }
  76. /**
  77. * @inheritdoc
  78. */
  79. public function releaseSavepoint($name)
  80. {
  81. // does nothing as MSSQL does not support this
  82. }
  83. /**
  84. * @inheritdoc
  85. */
  86. public function rollBackSavepoint($name)
  87. {
  88. $this->db->createCommand("ROLLBACK TRANSACTION $name")->execute();
  89. }
  90. /**
  91. * Quotes a table name for use in a query.
  92. * A simple table name has no schema prefix.
  93. * @param string $name table name.
  94. * @return string the properly quoted table name.
  95. */
  96. public function quoteSimpleTableName($name)
  97. {
  98. return strpos($name, '[') === false ? "[{$name}]" : $name;
  99. }
  100. /**
  101. * Quotes a column name for use in a query.
  102. * A simple column name has no prefix.
  103. * @param string $name column name.
  104. * @return string the properly quoted column name.
  105. */
  106. public function quoteSimpleColumnName($name)
  107. {
  108. return strpos($name, '[') === false && $name !== '*' ? "[{$name}]" : $name;
  109. }
  110. /**
  111. * Creates a query builder for the MSSQL database.
  112. * @return QueryBuilder query builder interface.
  113. */
  114. public function createQueryBuilder()
  115. {
  116. return new QueryBuilder($this->db);
  117. }
  118. /**
  119. * Loads the metadata for the specified table.
  120. * @param string $name table name
  121. * @return TableSchema|null driver dependent table metadata. Null if the table does not exist.
  122. */
  123. public function loadTableSchema($name)
  124. {
  125. $table = new TableSchema();
  126. $this->resolveTableNames($table, $name);
  127. $this->findPrimaryKeys($table);
  128. if ($this->findColumns($table)) {
  129. $this->findForeignKeys($table);
  130. return $table;
  131. } else {
  132. return null;
  133. }
  134. }
  135. /**
  136. * Resolves the table name and schema name (if any).
  137. * @param TableSchema $table the table metadata object
  138. * @param string $name the table name
  139. */
  140. protected function resolveTableNames($table, $name)
  141. {
  142. $parts = explode('.', str_replace(['[', ']'], '', $name));
  143. $partCount = count($parts);
  144. if ($partCount === 4) {
  145. // server name, catalog name, schema name and table name passed
  146. $table->catalogName = $parts[1];
  147. $table->schemaName = $parts[2];
  148. $table->name = $parts[3];
  149. $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
  150. } elseif ($partCount === 3) {
  151. // catalog name, schema name and table name passed
  152. $table->catalogName = $parts[0];
  153. $table->schemaName = $parts[1];
  154. $table->name = $parts[2];
  155. $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
  156. } elseif ($partCount === 2) {
  157. // only schema name and table name passed
  158. $table->schemaName = $parts[0];
  159. $table->name = $parts[1];
  160. $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name;
  161. } else {
  162. // only table name passed
  163. $table->schemaName = $this->defaultSchema;
  164. $table->fullName = $table->name = $parts[0];
  165. }
  166. }
  167. /**
  168. * Loads the column information into a [[ColumnSchema]] object.
  169. * @param array $info column information
  170. * @return ColumnSchema the column schema object
  171. */
  172. protected function loadColumnSchema($info)
  173. {
  174. $column = $this->createColumnSchema();
  175. $column->name = $info['column_name'];
  176. $column->allowNull = $info['is_nullable'] === 'YES';
  177. $column->dbType = $info['data_type'];
  178. $column->enumValues = []; // mssql has only vague equivalents to enum
  179. $column->isPrimaryKey = null; // primary key will be determined in findColumns() method
  180. $column->autoIncrement = $info['is_identity'] == 1;
  181. $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
  182. $column->comment = $info['comment'] === null ? '' : $info['comment'];
  183. $column->type = self::TYPE_STRING;
  184. if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
  185. $type = $matches[1];
  186. if (isset($this->typeMap[$type])) {
  187. $column->type = $this->typeMap[$type];
  188. }
  189. if (!empty($matches[2])) {
  190. $values = explode(',', $matches[2]);
  191. $column->size = $column->precision = (int) $values[0];
  192. if (isset($values[1])) {
  193. $column->scale = (int) $values[1];
  194. }
  195. if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
  196. $column->type = 'boolean';
  197. } elseif ($type === 'bit') {
  198. if ($column->size > 32) {
  199. $column->type = 'bigint';
  200. } elseif ($column->size === 32) {
  201. $column->type = 'integer';
  202. }
  203. }
  204. }
  205. }
  206. $column->phpType = $this->getColumnPhpType($column);
  207. if ($info['column_default'] === '(NULL)') {
  208. $info['column_default'] = null;
  209. }
  210. if (!$column->isPrimaryKey && ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
  211. $column->defaultValue = $column->phpTypecast($info['column_default']);
  212. }
  213. return $column;
  214. }
  215. /**
  216. * Collects the metadata of table columns.
  217. * @param TableSchema $table the table metadata
  218. * @return bool whether the table exists in the database
  219. */
  220. protected function findColumns($table)
  221. {
  222. $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS';
  223. $whereSql = "[t1].[table_name] = '{$table->name}'";
  224. if ($table->catalogName !== null) {
  225. $columnsTableName = "{$table->catalogName}.{$columnsTableName}";
  226. $whereSql .= " AND [t1].[table_catalog] = '{$table->catalogName}'";
  227. }
  228. if ($table->schemaName !== null) {
  229. $whereSql .= " AND [t1].[table_schema] = '{$table->schemaName}'";
  230. }
  231. $columnsTableName = $this->quoteTableName($columnsTableName);
  232. $sql = <<<SQL
  233. SELECT
  234. [t1].[column_name],
  235. [t1].[is_nullable],
  236. [t1].[data_type],
  237. [t1].[column_default],
  238. COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity,
  239. (
  240. SELECT CONVERT(VARCHAR, [t2].[value])
  241. FROM [sys].[extended_properties] AS [t2]
  242. WHERE
  243. [t2].[class] = 1 AND
  244. [t2].[class_desc] = 'OBJECT_OR_COLUMN' AND
  245. [t2].[name] = 'MS_Description' AND
  246. [t2].[major_id] = OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[table_name]) AND
  247. [t2].[minor_id] = COLUMNPROPERTY(OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[TABLE_NAME]), [t1].[COLUMN_NAME], 'ColumnID')
  248. ) as comment
  249. FROM {$columnsTableName} AS [t1]
  250. WHERE {$whereSql}
  251. SQL;
  252. try {
  253. $columns = $this->db->createCommand($sql)->queryAll();
  254. if (empty($columns)) {
  255. return false;
  256. }
  257. } catch (\Exception $e) {
  258. return false;
  259. }
  260. foreach ($columns as $column) {
  261. $column = $this->loadColumnSchema($column);
  262. foreach ($table->primaryKey as $primaryKey) {
  263. if (strcasecmp($column->name, $primaryKey) === 0) {
  264. $column->isPrimaryKey = true;
  265. break;
  266. }
  267. }
  268. if ($column->isPrimaryKey && $column->autoIncrement) {
  269. $table->sequenceName = '';
  270. }
  271. $table->columns[$column->name] = $column;
  272. }
  273. return true;
  274. }
  275. /**
  276. * Collects the constraint details for the given table and constraint type.
  277. * @param TableSchema $table
  278. * @param string $type either PRIMARY KEY or UNIQUE
  279. * @return array each entry contains index_name and field_name
  280. * @since 2.0.4
  281. */
  282. protected function findTableConstraints($table, $type)
  283. {
  284. $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE';
  285. $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS';
  286. if ($table->catalogName !== null) {
  287. $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName;
  288. $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName;
  289. }
  290. $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName);
  291. $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName);
  292. $sql = <<<SQL
  293. SELECT
  294. [kcu].[constraint_name] AS [index_name],
  295. [kcu].[column_name] AS [field_name]
  296. FROM {$keyColumnUsageTableName} AS [kcu]
  297. LEFT JOIN {$tableConstraintsTableName} AS [tc] ON
  298. [kcu].[table_schema] = [tc].[table_schema] AND
  299. [kcu].[table_name] = [tc].[table_name] AND
  300. [kcu].[constraint_name] = [tc].[constraint_name]
  301. WHERE
  302. [tc].[constraint_type] = :type AND
  303. [kcu].[table_name] = :tableName AND
  304. [kcu].[table_schema] = :schemaName
  305. SQL;
  306. return $this->db
  307. ->createCommand($sql, [
  308. ':tableName' => $table->name,
  309. ':schemaName' => $table->schemaName,
  310. ':type' => $type,
  311. ])
  312. ->queryAll();
  313. }
  314. /**
  315. * Collects the primary key column details for the given table.
  316. * @param TableSchema $table the table metadata
  317. */
  318. protected function findPrimaryKeys($table)
  319. {
  320. $result = [];
  321. foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) {
  322. $result[] = $row['field_name'];
  323. }
  324. $table->primaryKey = $result;
  325. }
  326. /**
  327. * Collects the foreign key column details for the given table.
  328. * @param TableSchema $table the table metadata
  329. */
  330. protected function findForeignKeys($table)
  331. {
  332. $object = $table->name;
  333. if ($table->schemaName !== null) {
  334. $object = $table->schemaName . '.' . $object;
  335. }
  336. if ($table->catalogName !== null) {
  337. $object = $table->catalogName . '.' . $object;
  338. }
  339. // please refer to the following page for more details:
  340. // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx
  341. $sql = <<<SQL
  342. SELECT
  343. [fk].[name] AS [fk_name],
  344. [cp].[name] AS [fk_column_name],
  345. OBJECT_NAME([fk].[referenced_object_id]) AS [uq_table_name],
  346. [cr].[name] AS [uq_column_name]
  347. FROM
  348. [sys].[foreign_keys] AS [fk]
  349. INNER JOIN [sys].[foreign_key_columns] AS [fkc] ON
  350. [fk].[object_id] = [fkc].[constraint_object_id]
  351. INNER JOIN [sys].[columns] AS [cp] ON
  352. [fk].[parent_object_id] = [cp].[object_id] AND
  353. [fkc].[parent_column_id] = [cp].[column_id]
  354. INNER JOIN [sys].[columns] AS [cr] ON
  355. [fk].[referenced_object_id] = [cr].[object_id] AND
  356. [fkc].[referenced_column_id] = [cr].[column_id]
  357. WHERE
  358. [fk].[parent_object_id] = OBJECT_ID(:object)
  359. SQL;
  360. $rows = $this->db->createCommand($sql, [
  361. ':object' => $object,
  362. ])->queryAll();
  363. $table->foreignKeys = [];
  364. foreach ($rows as $row) {
  365. $table->foreignKeys[$row['fk_name']] = [$row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']];
  366. }
  367. }
  368. /**
  369. * Returns all table names in the database.
  370. * @param string $schema the schema of the tables. Defaults to empty string, meaning the current or default schema.
  371. * @return array all table names in the database. The names have NO schema name prefix.
  372. */
  373. protected function findTableNames($schema = '')
  374. {
  375. if ($schema === '') {
  376. $schema = $this->defaultSchema;
  377. }
  378. $sql = <<<SQL
  379. SELECT [t].[table_name]
  380. FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
  381. WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
  382. ORDER BY [t].[table_name]
  383. SQL;
  384. return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
  385. }
  386. /**
  387. * @inheritdoc
  388. */
  389. protected function findViewNames($schema = '')
  390. {
  391. if ($schema === '') {
  392. $schema = $this->defaultSchema;
  393. }
  394. $sql = <<<SQL
  395. SELECT [t].[table_name]
  396. FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
  397. WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW'
  398. ORDER BY [t].[table_name]
  399. SQL;
  400. return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
  401. }
  402. /**
  403. * Returns all unique indexes for the given table.
  404. * Each array element is of the following structure:
  405. *
  406. * ```php
  407. * [
  408. * 'IndexName1' => ['col1' [, ...]],
  409. * 'IndexName2' => ['col2' [, ...]],
  410. * ]
  411. * ```
  412. *
  413. * @param TableSchema $table the table metadata
  414. * @return array all unique indexes for the given table.
  415. * @since 2.0.4
  416. */
  417. public function findUniqueIndexes($table)
  418. {
  419. $result = [];
  420. foreach ($this->findTableConstraints($table, 'UNIQUE') as $row) {
  421. $result[$row['index_name']][] = $row['field_name'];
  422. }
  423. return $result;
  424. }
  425. }