SQLServerSchemaManager.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. <?php
  2. namespace Doctrine\DBAL\Schema;
  3. use Doctrine\DBAL\DBALException;
  4. use Doctrine\DBAL\Driver\Exception;
  5. use Doctrine\DBAL\Platforms\SQLServerPlatform;
  6. use Doctrine\DBAL\Types\Type;
  7. use Throwable;
  8. use function assert;
  9. use function count;
  10. use function in_array;
  11. use function is_string;
  12. use function preg_match;
  13. use function sprintf;
  14. use function str_replace;
  15. use function strpos;
  16. use function strtok;
  17. /**
  18. * SQL Server Schema Manager.
  19. */
  20. class SQLServerSchemaManager extends AbstractSchemaManager
  21. {
  22. /**
  23. * {@inheritdoc}
  24. */
  25. public function dropDatabase($database)
  26. {
  27. try {
  28. parent::dropDatabase($database);
  29. } catch (DBALException $exception) {
  30. $exception = $exception->getPrevious();
  31. assert($exception instanceof Throwable);
  32. if (! $exception instanceof Exception) {
  33. throw $exception;
  34. }
  35. // If we have a error code 3702, the drop database operation failed
  36. // because of active connections on the database.
  37. // To force dropping the database, we first have to close all active connections
  38. // on that database and issue the drop database operation again.
  39. if ($exception->getErrorCode() !== 3702) {
  40. throw $exception;
  41. }
  42. $this->closeActiveDatabaseConnections($database);
  43. parent::dropDatabase($database);
  44. }
  45. }
  46. /**
  47. * {@inheritdoc}
  48. */
  49. protected function _getPortableSequenceDefinition($sequence)
  50. {
  51. return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']);
  52. }
  53. /**
  54. * {@inheritdoc}
  55. */
  56. protected function _getPortableTableColumnDefinition($tableColumn)
  57. {
  58. $dbType = strtok($tableColumn['type'], '(), ');
  59. assert(is_string($dbType));
  60. $fixed = null;
  61. $length = (int) $tableColumn['length'];
  62. $default = $tableColumn['default'];
  63. if (! isset($tableColumn['name'])) {
  64. $tableColumn['name'] = '';
  65. }
  66. if ($default !== null) {
  67. $default = $this->parseDefaultExpression($default);
  68. }
  69. switch ($dbType) {
  70. case 'nchar':
  71. case 'nvarchar':
  72. case 'ntext':
  73. // Unicode data requires 2 bytes per character
  74. $length /= 2;
  75. break;
  76. case 'varchar':
  77. // TEXT type is returned as VARCHAR(MAX) with a length of -1
  78. if ($length === -1) {
  79. $dbType = 'text';
  80. }
  81. break;
  82. }
  83. if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') {
  84. $fixed = true;
  85. }
  86. $type = $this->_platform->getDoctrineTypeMapping($dbType);
  87. $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
  88. $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
  89. $options = [
  90. 'length' => $length === 0 || ! in_array($type, ['text', 'string']) ? null : $length,
  91. 'unsigned' => false,
  92. 'fixed' => (bool) $fixed,
  93. 'default' => $default,
  94. 'notnull' => (bool) $tableColumn['notnull'],
  95. 'scale' => $tableColumn['scale'],
  96. 'precision' => $tableColumn['precision'],
  97. 'autoincrement' => (bool) $tableColumn['autoincrement'],
  98. 'comment' => $tableColumn['comment'] !== '' ? $tableColumn['comment'] : null,
  99. ];
  100. $column = new Column($tableColumn['name'], Type::getType($type), $options);
  101. if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') {
  102. $column->setPlatformOption('collation', $tableColumn['collation']);
  103. }
  104. return $column;
  105. }
  106. private function parseDefaultExpression(string $value): ?string
  107. {
  108. while (preg_match('/^\((.*)\)$/s', $value, $matches)) {
  109. $value = $matches[1];
  110. }
  111. if ($value === 'NULL') {
  112. return null;
  113. }
  114. if (preg_match('/^\'(.*)\'$/s', $value, $matches)) {
  115. $value = str_replace("''", "'", $matches[1]);
  116. }
  117. if ($value === 'getdate()') {
  118. return $this->_platform->getCurrentTimestampSQL();
  119. }
  120. return $value;
  121. }
  122. /**
  123. * {@inheritdoc}
  124. */
  125. protected function _getPortableTableForeignKeysList($tableForeignKeys)
  126. {
  127. $foreignKeys = [];
  128. foreach ($tableForeignKeys as $tableForeignKey) {
  129. $name = $tableForeignKey['ForeignKey'];
  130. if (! isset($foreignKeys[$name])) {
  131. $foreignKeys[$name] = [
  132. 'local_columns' => [$tableForeignKey['ColumnName']],
  133. 'foreign_table' => $tableForeignKey['ReferenceTableName'],
  134. 'foreign_columns' => [$tableForeignKey['ReferenceColumnName']],
  135. 'name' => $name,
  136. 'options' => [
  137. 'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']),
  138. 'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']),
  139. ],
  140. ];
  141. } else {
  142. $foreignKeys[$name]['local_columns'][] = $tableForeignKey['ColumnName'];
  143. $foreignKeys[$name]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName'];
  144. }
  145. }
  146. return parent::_getPortableTableForeignKeysList($foreignKeys);
  147. }
  148. /**
  149. * {@inheritdoc}
  150. */
  151. protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
  152. {
  153. foreach ($tableIndexes as &$tableIndex) {
  154. $tableIndex['non_unique'] = (bool) $tableIndex['non_unique'];
  155. $tableIndex['primary'] = (bool) $tableIndex['primary'];
  156. $tableIndex['flags'] = $tableIndex['flags'] ? [$tableIndex['flags']] : null;
  157. }
  158. return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
  159. }
  160. /**
  161. * {@inheritdoc}
  162. */
  163. protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
  164. {
  165. return new ForeignKeyConstraint(
  166. $tableForeignKey['local_columns'],
  167. $tableForeignKey['foreign_table'],
  168. $tableForeignKey['foreign_columns'],
  169. $tableForeignKey['name'],
  170. $tableForeignKey['options']
  171. );
  172. }
  173. /**
  174. * {@inheritdoc}
  175. */
  176. protected function _getPortableTableDefinition($table)
  177. {
  178. if (isset($table['schema_name']) && $table['schema_name'] !== 'dbo') {
  179. return $table['schema_name'] . '.' . $table['name'];
  180. }
  181. return $table['name'];
  182. }
  183. /**
  184. * {@inheritdoc}
  185. */
  186. protected function _getPortableDatabaseDefinition($database)
  187. {
  188. return $database['name'];
  189. }
  190. /**
  191. * {@inheritdoc}
  192. */
  193. protected function getPortableNamespaceDefinition(array $namespace)
  194. {
  195. return $namespace['name'];
  196. }
  197. /**
  198. * {@inheritdoc}
  199. */
  200. protected function _getPortableViewDefinition($view)
  201. {
  202. // @todo
  203. return new View($view['name'], '');
  204. }
  205. /**
  206. * {@inheritdoc}
  207. */
  208. public function listTableIndexes($table)
  209. {
  210. $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase());
  211. try {
  212. $tableIndexes = $this->_conn->fetchAllAssociative($sql);
  213. } catch (DBALException $e) {
  214. if (strpos($e->getMessage(), 'SQLSTATE [01000, 15472]') === 0) {
  215. return [];
  216. }
  217. throw $e;
  218. }
  219. return $this->_getPortableTableIndexesList($tableIndexes, $table);
  220. }
  221. /**
  222. * {@inheritdoc}
  223. */
  224. public function alterTable(TableDiff $tableDiff)
  225. {
  226. if (count($tableDiff->removedColumns) > 0) {
  227. foreach ($tableDiff->removedColumns as $col) {
  228. $columnConstraintSql = $this->getColumnConstraintSQL($tableDiff->name, $col->getName());
  229. foreach ($this->_conn->fetchAllAssociative($columnConstraintSql) as $constraint) {
  230. $this->_conn->exec(
  231. sprintf(
  232. 'ALTER TABLE %s DROP CONSTRAINT %s',
  233. $tableDiff->name,
  234. $constraint['Name']
  235. )
  236. );
  237. }
  238. }
  239. }
  240. parent::alterTable($tableDiff);
  241. }
  242. /**
  243. * Returns the SQL to retrieve the constraints for a given column.
  244. *
  245. * @param string $table
  246. * @param string $column
  247. *
  248. * @return string
  249. */
  250. private function getColumnConstraintSQL($table, $column)
  251. {
  252. return "SELECT sysobjects.[Name]
  253. FROM sysobjects INNER JOIN (SELECT [Name],[ID] FROM sysobjects WHERE XType = 'U') AS Tab
  254. ON Tab.[ID] = sysobjects.[Parent_Obj]
  255. INNER JOIN sys.default_constraints DefCons ON DefCons.[object_id] = sysobjects.[ID]
  256. INNER JOIN syscolumns Col ON Col.[ColID] = DefCons.[parent_column_id] AND Col.[ID] = Tab.[ID]
  257. WHERE Col.[Name] = " . $this->_conn->quote($column) . ' AND Tab.[Name] = ' . $this->_conn->quote($table) . '
  258. ORDER BY Col.[Name]';
  259. }
  260. /**
  261. * Closes currently active connections on the given database.
  262. *
  263. * This is useful to force DROP DATABASE operations which could fail because of active connections.
  264. *
  265. * @param string $database The name of the database to close currently active connections for.
  266. *
  267. * @return void
  268. */
  269. private function closeActiveDatabaseConnections($database)
  270. {
  271. $database = new Identifier($database);
  272. $this->_execSql(
  273. sprintf(
  274. 'ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE',
  275. $database->getQuotedName($this->_platform)
  276. )
  277. );
  278. }
  279. /**
  280. * @param string $name
  281. */
  282. public function listTableDetails($name): Table
  283. {
  284. $table = parent::listTableDetails($name);
  285. $platform = $this->_platform;
  286. assert($platform instanceof SQLServerPlatform);
  287. $sql = $platform->getListTableMetadataSQL($name);
  288. $tableOptions = $this->_conn->fetchAssociative($sql);
  289. if ($tableOptions !== false) {
  290. $table->addOption('comment', $tableOptions['table_comment']);
  291. }
  292. return $table;
  293. }
  294. }