Schema.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  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\CheckConstraint;
  9. use yii\db\Constraint;
  10. use yii\db\ConstraintFinderInterface;
  11. use yii\db\ConstraintFinderTrait;
  12. use yii\db\DefaultValueConstraint;
  13. use yii\db\ForeignKeyConstraint;
  14. use yii\db\IndexConstraint;
  15. use yii\db\ViewFinderTrait;
  16. use yii\helpers\ArrayHelper;
  17. /**
  18. * Schema is the class for retrieving metadata from MS SQL Server databases (version 2008 and above).
  19. *
  20. * @author Timur Ruziev <resurtm@gmail.com>
  21. * @since 2.0
  22. */
  23. class Schema extends \yii\db\Schema implements ConstraintFinderInterface
  24. {
  25. use ViewFinderTrait;
  26. use ConstraintFinderTrait;
  27. /**
  28. * {@inheritdoc}
  29. */
  30. public $columnSchemaClass = 'yii\db\mssql\ColumnSchema';
  31. /**
  32. * @var string the default schema used for the current session.
  33. */
  34. public $defaultSchema = 'dbo';
  35. /**
  36. * @var array mapping from physical column types (keys) to abstract column types (values)
  37. */
  38. public $typeMap = [
  39. // exact numbers
  40. 'bigint' => self::TYPE_BIGINT,
  41. 'numeric' => self::TYPE_DECIMAL,
  42. 'bit' => self::TYPE_SMALLINT,
  43. 'smallint' => self::TYPE_SMALLINT,
  44. 'decimal' => self::TYPE_DECIMAL,
  45. 'smallmoney' => self::TYPE_MONEY,
  46. 'int' => self::TYPE_INTEGER,
  47. 'tinyint' => self::TYPE_TINYINT,
  48. 'money' => self::TYPE_MONEY,
  49. // approximate numbers
  50. 'float' => self::TYPE_FLOAT,
  51. 'double' => self::TYPE_DOUBLE,
  52. 'real' => self::TYPE_FLOAT,
  53. // date and time
  54. 'date' => self::TYPE_DATE,
  55. 'datetimeoffset' => self::TYPE_DATETIME,
  56. 'datetime2' => self::TYPE_DATETIME,
  57. 'smalldatetime' => self::TYPE_DATETIME,
  58. 'datetime' => self::TYPE_DATETIME,
  59. 'time' => self::TYPE_TIME,
  60. // character strings
  61. 'char' => self::TYPE_CHAR,
  62. 'varchar' => self::TYPE_STRING,
  63. 'text' => self::TYPE_TEXT,
  64. // unicode character strings
  65. 'nchar' => self::TYPE_CHAR,
  66. 'nvarchar' => self::TYPE_STRING,
  67. 'ntext' => self::TYPE_TEXT,
  68. // binary strings
  69. 'binary' => self::TYPE_BINARY,
  70. 'varbinary' => self::TYPE_BINARY,
  71. 'image' => self::TYPE_BINARY,
  72. // other data types
  73. // 'cursor' type cannot be used with tables
  74. 'timestamp' => self::TYPE_TIMESTAMP,
  75. 'hierarchyid' => self::TYPE_STRING,
  76. 'uniqueidentifier' => self::TYPE_STRING,
  77. 'sql_variant' => self::TYPE_STRING,
  78. 'xml' => self::TYPE_STRING,
  79. 'table' => self::TYPE_STRING,
  80. ];
  81. /**
  82. * {@inheritdoc}
  83. */
  84. protected $tableQuoteCharacter = ['[', ']'];
  85. /**
  86. * {@inheritdoc}
  87. */
  88. protected $columnQuoteCharacter = ['[', ']'];
  89. /**
  90. * Resolves the table name and schema name (if any).
  91. * @param string $name the table name
  92. * @return TableSchema resolved table, schema, etc. names.
  93. */
  94. protected function resolveTableName($name)
  95. {
  96. $resolvedName = new TableSchema();
  97. $parts = $this->getTableNameParts($name);
  98. $partCount = count($parts);
  99. if ($partCount === 4) {
  100. // server name, catalog name, schema name and table name passed
  101. $resolvedName->catalogName = $parts[1];
  102. $resolvedName->schemaName = $parts[2];
  103. $resolvedName->name = $parts[3];
  104. $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name;
  105. } elseif ($partCount === 3) {
  106. // catalog name, schema name and table name passed
  107. $resolvedName->catalogName = $parts[0];
  108. $resolvedName->schemaName = $parts[1];
  109. $resolvedName->name = $parts[2];
  110. $resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name;
  111. } elseif ($partCount === 2) {
  112. // only schema name and table name passed
  113. $resolvedName->schemaName = $parts[0];
  114. $resolvedName->name = $parts[1];
  115. $resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name;
  116. } else {
  117. // only table name passed
  118. $resolvedName->schemaName = $this->defaultSchema;
  119. $resolvedName->fullName = $resolvedName->name = $parts[0];
  120. }
  121. return $resolvedName;
  122. }
  123. /**
  124. * {@inheritDoc}
  125. * @param string $name
  126. * @return array
  127. * @since 2.0.22
  128. */
  129. protected function getTableNameParts($name)
  130. {
  131. $parts = [$name];
  132. preg_match_all('/([^.\[\]]+)|\[([^\[\]]+)\]/', $name, $matches);
  133. if (isset($matches[0]) && is_array($matches[0]) && !empty($matches[0])) {
  134. $parts = $matches[0];
  135. }
  136. $parts = str_replace(['[', ']'], '', $parts);
  137. return $parts;
  138. }
  139. /**
  140. * {@inheritdoc}
  141. * @see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql
  142. */
  143. protected function findSchemaNames()
  144. {
  145. static $sql = <<<'SQL'
  146. SELECT [s].[name]
  147. FROM [sys].[schemas] AS [s]
  148. INNER JOIN [sys].[database_principals] AS [p] ON [p].[principal_id] = [s].[principal_id]
  149. WHERE [p].[is_fixed_role] = 0 AND [p].[sid] IS NOT NULL
  150. ORDER BY [s].[name] ASC
  151. SQL;
  152. return $this->db->createCommand($sql)->queryColumn();
  153. }
  154. /**
  155. * {@inheritdoc}
  156. */
  157. protected function findTableNames($schema = '')
  158. {
  159. if ($schema === '') {
  160. $schema = $this->defaultSchema;
  161. }
  162. $sql = <<<'SQL'
  163. SELECT [t].[table_name]
  164. FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
  165. WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW')
  166. ORDER BY [t].[table_name]
  167. SQL;
  168. $tables = $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
  169. $tables = array_map(static function ($item) {
  170. return '[' . $item . ']';
  171. }, $tables);
  172. return $tables;
  173. }
  174. /**
  175. * {@inheritdoc}
  176. */
  177. protected function loadTableSchema($name)
  178. {
  179. $table = new TableSchema();
  180. $this->resolveTableNames($table, $name);
  181. $this->findPrimaryKeys($table);
  182. if ($this->findColumns($table)) {
  183. $this->findForeignKeys($table);
  184. return $table;
  185. }
  186. return null;
  187. }
  188. /**
  189. * {@inheritdoc}
  190. */
  191. protected function loadTablePrimaryKey($tableName)
  192. {
  193. return $this->loadTableConstraints($tableName, 'primaryKey');
  194. }
  195. /**
  196. * {@inheritdoc}
  197. */
  198. protected function loadTableForeignKeys($tableName)
  199. {
  200. return $this->loadTableConstraints($tableName, 'foreignKeys');
  201. }
  202. /**
  203. * {@inheritdoc}
  204. */
  205. protected function loadTableIndexes($tableName)
  206. {
  207. static $sql = <<<'SQL'
  208. SELECT
  209. [i].[name] AS [name],
  210. [iccol].[name] AS [column_name],
  211. [i].[is_unique] AS [index_is_unique],
  212. [i].[is_primary_key] AS [index_is_primary]
  213. FROM [sys].[indexes] AS [i]
  214. INNER JOIN [sys].[index_columns] AS [ic]
  215. ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id]
  216. INNER JOIN [sys].[columns] AS [iccol]
  217. ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id]
  218. WHERE [i].[object_id] = OBJECT_ID(:fullName)
  219. ORDER BY [ic].[key_ordinal] ASC
  220. SQL;
  221. $resolvedName = $this->resolveTableName($tableName);
  222. $indexes = $this->db->createCommand($sql, [
  223. ':fullName' => $resolvedName->fullName,
  224. ])->queryAll();
  225. $indexes = $this->normalizePdoRowKeyCase($indexes, true);
  226. $indexes = ArrayHelper::index($indexes, null, 'name');
  227. $result = [];
  228. foreach ($indexes as $name => $index) {
  229. $result[] = new IndexConstraint([
  230. 'isPrimary' => (bool)$index[0]['index_is_primary'],
  231. 'isUnique' => (bool)$index[0]['index_is_unique'],
  232. 'name' => $name,
  233. 'columnNames' => ArrayHelper::getColumn($index, 'column_name'),
  234. ]);
  235. }
  236. return $result;
  237. }
  238. /**
  239. * {@inheritdoc}
  240. */
  241. protected function loadTableUniques($tableName)
  242. {
  243. return $this->loadTableConstraints($tableName, 'uniques');
  244. }
  245. /**
  246. * {@inheritdoc}
  247. */
  248. protected function loadTableChecks($tableName)
  249. {
  250. return $this->loadTableConstraints($tableName, 'checks');
  251. }
  252. /**
  253. * {@inheritdoc}
  254. */
  255. protected function loadTableDefaultValues($tableName)
  256. {
  257. return $this->loadTableConstraints($tableName, 'defaults');
  258. }
  259. /**
  260. * {@inheritdoc}
  261. */
  262. public function createSavepoint($name)
  263. {
  264. $this->db->createCommand("SAVE TRANSACTION $name")->execute();
  265. }
  266. /**
  267. * {@inheritdoc}
  268. */
  269. public function releaseSavepoint($name)
  270. {
  271. // does nothing as MSSQL does not support this
  272. }
  273. /**
  274. * {@inheritdoc}
  275. */
  276. public function rollBackSavepoint($name)
  277. {
  278. $this->db->createCommand("ROLLBACK TRANSACTION $name")->execute();
  279. }
  280. /**
  281. * Creates a query builder for the MSSQL database.
  282. * @return QueryBuilder query builder interface.
  283. */
  284. public function createQueryBuilder()
  285. {
  286. return new QueryBuilder($this->db);
  287. }
  288. /**
  289. * Resolves the table name and schema name (if any).
  290. * @param TableSchema $table the table metadata object
  291. * @param string $name the table name
  292. */
  293. protected function resolveTableNames($table, $name)
  294. {
  295. $parts = $this->getTableNameParts($name);
  296. $partCount = count($parts);
  297. if ($partCount === 4) {
  298. // server name, catalog name, schema name and table name passed
  299. $table->catalogName = $parts[1];
  300. $table->schemaName = $parts[2];
  301. $table->name = $parts[3];
  302. $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
  303. } elseif ($partCount === 3) {
  304. // catalog name, schema name and table name passed
  305. $table->catalogName = $parts[0];
  306. $table->schemaName = $parts[1];
  307. $table->name = $parts[2];
  308. $table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name;
  309. } elseif ($partCount === 2) {
  310. // only schema name and table name passed
  311. $table->schemaName = $parts[0];
  312. $table->name = $parts[1];
  313. $table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name;
  314. } else {
  315. // only table name passed
  316. $table->schemaName = $this->defaultSchema;
  317. $table->fullName = $table->name = $parts[0];
  318. }
  319. }
  320. /**
  321. * Loads the column information into a [[ColumnSchema]] object.
  322. * @param array $info column information
  323. * @return ColumnSchema the column schema object
  324. */
  325. protected function loadColumnSchema($info)
  326. {
  327. $column = $this->createColumnSchema();
  328. $column->name = $info['column_name'];
  329. $column->allowNull = $info['is_nullable'] === 'YES';
  330. $column->dbType = $info['data_type'];
  331. $column->enumValues = []; // mssql has only vague equivalents to enum
  332. $column->isPrimaryKey = null; // primary key will be determined in findColumns() method
  333. $column->autoIncrement = $info['is_identity'] == 1;
  334. $column->unsigned = stripos($column->dbType, 'unsigned') !== false;
  335. $column->comment = $info['comment'] === null ? '' : $info['comment'];
  336. $column->type = self::TYPE_STRING;
  337. if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) {
  338. $type = $matches[1];
  339. if (isset($this->typeMap[$type])) {
  340. $column->type = $this->typeMap[$type];
  341. }
  342. if (!empty($matches[2])) {
  343. $values = explode(',', $matches[2]);
  344. $column->size = $column->precision = (int)$values[0];
  345. if (isset($values[1])) {
  346. $column->scale = (int)$values[1];
  347. }
  348. if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) {
  349. $column->type = 'boolean';
  350. } elseif ($type === 'bit') {
  351. if ($column->size > 32) {
  352. $column->type = 'bigint';
  353. } elseif ($column->size === 32) {
  354. $column->type = 'integer';
  355. }
  356. }
  357. }
  358. }
  359. $column->phpType = $this->getColumnPhpType($column);
  360. if ($info['column_default'] === '(NULL)') {
  361. $info['column_default'] = null;
  362. }
  363. if (!$column->isPrimaryKey && ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) {
  364. $column->defaultValue = $column->defaultPhpTypecast($info['column_default']);
  365. }
  366. return $column;
  367. }
  368. /**
  369. * Collects the metadata of table columns.
  370. * @param TableSchema $table the table metadata
  371. * @return bool whether the table exists in the database
  372. */
  373. protected function findColumns($table)
  374. {
  375. $columnsTableName = 'INFORMATION_SCHEMA.COLUMNS';
  376. $whereSql = "[t1].[table_name] = " . $this->db->quoteValue($table->name);
  377. if ($table->catalogName !== null) {
  378. $columnsTableName = "{$table->catalogName}.{$columnsTableName}";
  379. $whereSql .= " AND [t1].[table_catalog] = '{$table->catalogName}'";
  380. }
  381. if ($table->schemaName !== null) {
  382. $whereSql .= " AND [t1].[table_schema] = '{$table->schemaName}'";
  383. }
  384. $columnsTableName = $this->quoteTableName($columnsTableName);
  385. $sql = <<<SQL
  386. SELECT
  387. [t1].[column_name],
  388. [t1].[is_nullable],
  389. CASE WHEN [t1].[data_type] IN ('char','varchar','nchar','nvarchar','binary','varbinary') THEN
  390. CASE WHEN [t1].[character_maximum_length] = NULL OR [t1].[character_maximum_length] = -1 THEN
  391. [t1].[data_type]
  392. ELSE
  393. [t1].[data_type] + '(' + LTRIM(RTRIM(CONVERT(CHAR,[t1].[character_maximum_length]))) + ')'
  394. END
  395. ELSE
  396. [t1].[data_type]
  397. END AS 'data_type',
  398. [t1].[column_default],
  399. COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity,
  400. (
  401. SELECT CONVERT(VARCHAR, [t2].[value])
  402. FROM [sys].[extended_properties] AS [t2]
  403. WHERE
  404. [t2].[class] = 1 AND
  405. [t2].[class_desc] = 'OBJECT_OR_COLUMN' AND
  406. [t2].[name] = 'MS_Description' AND
  407. [t2].[major_id] = OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[table_name]) AND
  408. [t2].[minor_id] = COLUMNPROPERTY(OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[TABLE_NAME]), [t1].[COLUMN_NAME], 'ColumnID')
  409. ) as comment
  410. FROM {$columnsTableName} AS [t1]
  411. WHERE {$whereSql}
  412. SQL;
  413. try {
  414. $columns = $this->db->createCommand($sql)->queryAll();
  415. if (empty($columns)) {
  416. return false;
  417. }
  418. } catch (\Exception $e) {
  419. return false;
  420. }
  421. foreach ($columns as $column) {
  422. $column = $this->loadColumnSchema($column);
  423. foreach ($table->primaryKey as $primaryKey) {
  424. if (strcasecmp($column->name, $primaryKey) === 0) {
  425. $column->isPrimaryKey = true;
  426. break;
  427. }
  428. }
  429. if ($column->isPrimaryKey && $column->autoIncrement) {
  430. $table->sequenceName = '';
  431. }
  432. $table->columns[$column->name] = $column;
  433. }
  434. return true;
  435. }
  436. /**
  437. * Collects the constraint details for the given table and constraint type.
  438. * @param TableSchema $table
  439. * @param string $type either PRIMARY KEY or UNIQUE
  440. * @return array each entry contains index_name and field_name
  441. * @since 2.0.4
  442. */
  443. protected function findTableConstraints($table, $type)
  444. {
  445. $keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE';
  446. $tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS';
  447. if ($table->catalogName !== null) {
  448. $keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName;
  449. $tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName;
  450. }
  451. $keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName);
  452. $tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName);
  453. $sql = <<<SQL
  454. SELECT
  455. [kcu].[constraint_name] AS [index_name],
  456. [kcu].[column_name] AS [field_name]
  457. FROM {$keyColumnUsageTableName} AS [kcu]
  458. LEFT JOIN {$tableConstraintsTableName} AS [tc] ON
  459. [kcu].[table_schema] = [tc].[table_schema] AND
  460. [kcu].[table_name] = [tc].[table_name] AND
  461. [kcu].[constraint_name] = [tc].[constraint_name]
  462. WHERE
  463. [tc].[constraint_type] = :type AND
  464. [kcu].[table_name] = :tableName AND
  465. [kcu].[table_schema] = :schemaName
  466. SQL;
  467. return $this->db
  468. ->createCommand($sql, [
  469. ':tableName' => $table->name,
  470. ':schemaName' => $table->schemaName,
  471. ':type' => $type,
  472. ])
  473. ->queryAll();
  474. }
  475. /**
  476. * Collects the primary key column details for the given table.
  477. * @param TableSchema $table the table metadata
  478. */
  479. protected function findPrimaryKeys($table)
  480. {
  481. $result = [];
  482. foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) {
  483. $result[] = $row['field_name'];
  484. }
  485. $table->primaryKey = $result;
  486. }
  487. /**
  488. * Collects the foreign key column details for the given table.
  489. * @param TableSchema $table the table metadata
  490. */
  491. protected function findForeignKeys($table)
  492. {
  493. $object = $table->name;
  494. if ($table->schemaName !== null) {
  495. $object = $table->schemaName . '.' . $object;
  496. }
  497. if ($table->catalogName !== null) {
  498. $object = $table->catalogName . '.' . $object;
  499. }
  500. // please refer to the following page for more details:
  501. // http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx
  502. $sql = <<<'SQL'
  503. SELECT
  504. [fk].[name] AS [fk_name],
  505. [cp].[name] AS [fk_column_name],
  506. OBJECT_NAME([fk].[referenced_object_id]) AS [uq_table_name],
  507. [cr].[name] AS [uq_column_name]
  508. FROM
  509. [sys].[foreign_keys] AS [fk]
  510. INNER JOIN [sys].[foreign_key_columns] AS [fkc] ON
  511. [fk].[object_id] = [fkc].[constraint_object_id]
  512. INNER JOIN [sys].[columns] AS [cp] ON
  513. [fk].[parent_object_id] = [cp].[object_id] AND
  514. [fkc].[parent_column_id] = [cp].[column_id]
  515. INNER JOIN [sys].[columns] AS [cr] ON
  516. [fk].[referenced_object_id] = [cr].[object_id] AND
  517. [fkc].[referenced_column_id] = [cr].[column_id]
  518. WHERE
  519. [fk].[parent_object_id] = OBJECT_ID(:object)
  520. SQL;
  521. $rows = $this->db->createCommand($sql, [
  522. ':object' => $object,
  523. ])->queryAll();
  524. $table->foreignKeys = [];
  525. foreach ($rows as $row) {
  526. if (!isset($table->foreignKeys[$row['fk_name']])) {
  527. $table->foreignKeys[$row['fk_name']][] = $row['uq_table_name'];
  528. }
  529. $table->foreignKeys[$row['fk_name']][$row['fk_column_name']] = $row['uq_column_name'];
  530. }
  531. }
  532. /**
  533. * {@inheritdoc}
  534. */
  535. protected function findViewNames($schema = '')
  536. {
  537. if ($schema === '') {
  538. $schema = $this->defaultSchema;
  539. }
  540. $sql = <<<'SQL'
  541. SELECT [t].[table_name]
  542. FROM [INFORMATION_SCHEMA].[TABLES] AS [t]
  543. WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW'
  544. ORDER BY [t].[table_name]
  545. SQL;
  546. return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn();
  547. }
  548. /**
  549. * Returns all unique indexes for the given table.
  550. *
  551. * Each array element is of the following structure:
  552. *
  553. * ```php
  554. * [
  555. * 'IndexName1' => ['col1' [, ...]],
  556. * 'IndexName2' => ['col2' [, ...]],
  557. * ]
  558. * ```
  559. *
  560. * @param TableSchema $table the table metadata
  561. * @return array all unique indexes for the given table.
  562. * @since 2.0.4
  563. */
  564. public function findUniqueIndexes($table)
  565. {
  566. $result = [];
  567. foreach ($this->findTableConstraints($table, 'UNIQUE') as $row) {
  568. $result[$row['index_name']][] = $row['field_name'];
  569. }
  570. return $result;
  571. }
  572. /**
  573. * Loads multiple types of constraints and returns the specified ones.
  574. * @param string $tableName table name.
  575. * @param string $returnType return type:
  576. * - primaryKey
  577. * - foreignKeys
  578. * - uniques
  579. * - checks
  580. * - defaults
  581. * @return mixed constraints.
  582. */
  583. private function loadTableConstraints($tableName, $returnType)
  584. {
  585. static $sql = <<<'SQL'
  586. SELECT
  587. [o].[name] AS [name],
  588. COALESCE([ccol].[name], [dcol].[name], [fccol].[name], [kiccol].[name]) AS [column_name],
  589. RTRIM([o].[type]) AS [type],
  590. OBJECT_SCHEMA_NAME([f].[referenced_object_id]) AS [foreign_table_schema],
  591. OBJECT_NAME([f].[referenced_object_id]) AS [foreign_table_name],
  592. [ffccol].[name] AS [foreign_column_name],
  593. [f].[update_referential_action_desc] AS [on_update],
  594. [f].[delete_referential_action_desc] AS [on_delete],
  595. [c].[definition] AS [check_expr],
  596. [d].[definition] AS [default_expr]
  597. FROM (SELECT OBJECT_ID(:fullName) AS [object_id]) AS [t]
  598. INNER JOIN [sys].[objects] AS [o]
  599. ON [o].[parent_object_id] = [t].[object_id] AND [o].[type] IN ('PK', 'UQ', 'C', 'D', 'F')
  600. LEFT JOIN [sys].[check_constraints] AS [c]
  601. ON [c].[object_id] = [o].[object_id]
  602. LEFT JOIN [sys].[columns] AS [ccol]
  603. ON [ccol].[object_id] = [c].[parent_object_id] AND [ccol].[column_id] = [c].[parent_column_id]
  604. LEFT JOIN [sys].[default_constraints] AS [d]
  605. ON [d].[object_id] = [o].[object_id]
  606. LEFT JOIN [sys].[columns] AS [dcol]
  607. ON [dcol].[object_id] = [d].[parent_object_id] AND [dcol].[column_id] = [d].[parent_column_id]
  608. LEFT JOIN [sys].[key_constraints] AS [k]
  609. ON [k].[object_id] = [o].[object_id]
  610. LEFT JOIN [sys].[index_columns] AS [kic]
  611. ON [kic].[object_id] = [k].[parent_object_id] AND [kic].[index_id] = [k].[unique_index_id]
  612. LEFT JOIN [sys].[columns] AS [kiccol]
  613. ON [kiccol].[object_id] = [kic].[object_id] AND [kiccol].[column_id] = [kic].[column_id]
  614. LEFT JOIN [sys].[foreign_keys] AS [f]
  615. ON [f].[object_id] = [o].[object_id]
  616. LEFT JOIN [sys].[foreign_key_columns] AS [fc]
  617. ON [fc].[constraint_object_id] = [o].[object_id]
  618. LEFT JOIN [sys].[columns] AS [fccol]
  619. ON [fccol].[object_id] = [fc].[parent_object_id] AND [fccol].[column_id] = [fc].[parent_column_id]
  620. LEFT JOIN [sys].[columns] AS [ffccol]
  621. ON [ffccol].[object_id] = [fc].[referenced_object_id] AND [ffccol].[column_id] = [fc].[referenced_column_id]
  622. ORDER BY [kic].[key_ordinal] ASC, [fc].[constraint_column_id] ASC
  623. SQL;
  624. $resolvedName = $this->resolveTableName($tableName);
  625. $constraints = $this->db->createCommand($sql, [
  626. ':fullName' => $resolvedName->fullName,
  627. ])->queryAll();
  628. $constraints = $this->normalizePdoRowKeyCase($constraints, true);
  629. $constraints = ArrayHelper::index($constraints, null, ['type', 'name']);
  630. $result = [
  631. 'primaryKey' => null,
  632. 'foreignKeys' => [],
  633. 'uniques' => [],
  634. 'checks' => [],
  635. 'defaults' => [],
  636. ];
  637. foreach ($constraints as $type => $names) {
  638. foreach ($names as $name => $constraint) {
  639. switch ($type) {
  640. case 'PK':
  641. $result['primaryKey'] = new Constraint([
  642. 'name' => $name,
  643. 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
  644. ]);
  645. break;
  646. case 'F':
  647. $result['foreignKeys'][] = new ForeignKeyConstraint([
  648. 'name' => $name,
  649. 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
  650. 'foreignSchemaName' => $constraint[0]['foreign_table_schema'],
  651. 'foreignTableName' => $constraint[0]['foreign_table_name'],
  652. 'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'),
  653. 'onDelete' => str_replace('_', '', $constraint[0]['on_delete']),
  654. 'onUpdate' => str_replace('_', '', $constraint[0]['on_update']),
  655. ]);
  656. break;
  657. case 'UQ':
  658. $result['uniques'][] = new Constraint([
  659. 'name' => $name,
  660. 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
  661. ]);
  662. break;
  663. case 'C':
  664. $result['checks'][] = new CheckConstraint([
  665. 'name' => $name,
  666. 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
  667. 'expression' => $constraint[0]['check_expr'],
  668. ]);
  669. break;
  670. case 'D':
  671. $result['defaults'][] = new DefaultValueConstraint([
  672. 'name' => $name,
  673. 'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'),
  674. 'value' => $constraint[0]['default_expr'],
  675. ]);
  676. break;
  677. }
  678. }
  679. }
  680. foreach ($result as $type => $data) {
  681. $this->setTableMetadata($tableName, $type, $data);
  682. }
  683. return $result[$returnType];
  684. }
  685. /**
  686. * {@inheritdoc}
  687. */
  688. public function quoteColumnName($name)
  689. {
  690. if (preg_match('/^\[.*\]$/', $name)) {
  691. return $name;
  692. }
  693. return parent::quoteColumnName($name);
  694. }
  695. }