SQLServerPlatform.php 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660
  1. <?php
  2. namespace Doctrine\DBAL\Platforms;
  3. use Doctrine\DBAL\Exception\InvalidLockMode;
  4. use Doctrine\DBAL\LockMode;
  5. use Doctrine\DBAL\Schema\Column;
  6. use Doctrine\DBAL\Schema\ColumnDiff;
  7. use Doctrine\DBAL\Schema\ForeignKeyConstraint;
  8. use Doctrine\DBAL\Schema\Identifier;
  9. use Doctrine\DBAL\Schema\Index;
  10. use Doctrine\DBAL\Schema\Sequence;
  11. use Doctrine\DBAL\Schema\Table;
  12. use Doctrine\DBAL\Schema\TableDiff;
  13. use Doctrine\Deprecations\Deprecation;
  14. use InvalidArgumentException;
  15. use function array_merge;
  16. use function array_unique;
  17. use function array_values;
  18. use function count;
  19. use function crc32;
  20. use function dechex;
  21. use function explode;
  22. use function func_get_args;
  23. use function implode;
  24. use function is_array;
  25. use function is_bool;
  26. use function is_numeric;
  27. use function is_string;
  28. use function preg_match;
  29. use function preg_match_all;
  30. use function sprintf;
  31. use function str_replace;
  32. use function strpos;
  33. use function strtoupper;
  34. use function substr_count;
  35. use const PREG_OFFSET_CAPTURE;
  36. /**
  37. * Provides the behavior, features and SQL dialect of the Microsoft SQL Server database platform
  38. * of the oldest supported version.
  39. */
  40. class SQLServerPlatform extends AbstractPlatform
  41. {
  42. /**
  43. * {@inheritdoc}
  44. */
  45. public function getCurrentDateSQL()
  46. {
  47. return $this->getConvertExpression('date', 'GETDATE()');
  48. }
  49. /**
  50. * {@inheritdoc}
  51. */
  52. public function getCurrentTimeSQL()
  53. {
  54. return $this->getConvertExpression('time', 'GETDATE()');
  55. }
  56. /**
  57. * Returns an expression that converts an expression of one data type to another.
  58. *
  59. * @param string $dataType The target native data type. Alias data types cannot be used.
  60. * @param string $expression The SQL expression to convert.
  61. */
  62. private function getConvertExpression($dataType, $expression): string
  63. {
  64. return sprintf('CONVERT(%s, %s)', $dataType, $expression);
  65. }
  66. /**
  67. * {@inheritdoc}
  68. */
  69. protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit)
  70. {
  71. $factorClause = '';
  72. if ($operator === '-') {
  73. $factorClause = '-1 * ';
  74. }
  75. return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')';
  76. }
  77. /**
  78. * {@inheritDoc}
  79. */
  80. public function getDateDiffExpression($date1, $date2)
  81. {
  82. return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')';
  83. }
  84. /**
  85. * {@inheritDoc}
  86. *
  87. * Microsoft SQL Server prefers "autoincrement" identity columns
  88. * since sequences can only be emulated with a table.
  89. *
  90. * @deprecated
  91. */
  92. public function prefersIdentityColumns()
  93. {
  94. Deprecation::trigger(
  95. 'doctrine/dbal',
  96. 'https://github.com/doctrine/dbal/pulls/1519',
  97. 'SQLServerPlatform::prefersIdentityColumns() is deprecated.'
  98. );
  99. return true;
  100. }
  101. /**
  102. * {@inheritDoc}
  103. *
  104. * Microsoft SQL Server supports this through AUTO_INCREMENT columns.
  105. */
  106. public function supportsIdentityColumns()
  107. {
  108. return true;
  109. }
  110. /**
  111. * {@inheritDoc}
  112. */
  113. public function supportsReleaseSavepoints()
  114. {
  115. return false;
  116. }
  117. /**
  118. * {@inheritdoc}
  119. */
  120. public function supportsSchemas()
  121. {
  122. return true;
  123. }
  124. /**
  125. * {@inheritdoc}
  126. */
  127. public function getDefaultSchemaName()
  128. {
  129. return 'dbo';
  130. }
  131. /**
  132. * {@inheritDoc}
  133. */
  134. public function supportsColumnCollation()
  135. {
  136. return true;
  137. }
  138. public function supportsSequences(): bool
  139. {
  140. return true;
  141. }
  142. public function getAlterSequenceSQL(Sequence $sequence): string
  143. {
  144. return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) .
  145. ' INCREMENT BY ' . $sequence->getAllocationSize();
  146. }
  147. public function getCreateSequenceSQL(Sequence $sequence): string
  148. {
  149. return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) .
  150. ' START WITH ' . $sequence->getInitialValue() .
  151. ' INCREMENT BY ' . $sequence->getAllocationSize() .
  152. ' MINVALUE ' . $sequence->getInitialValue();
  153. }
  154. /**
  155. * {@inheritdoc}
  156. */
  157. public function getListSequencesSQL($database)
  158. {
  159. return 'SELECT seq.name,
  160. CAST(
  161. seq.increment AS VARCHAR(MAX)
  162. ) AS increment, -- CAST avoids driver error for sql_variant type
  163. CAST(
  164. seq.start_value AS VARCHAR(MAX)
  165. ) AS start_value -- CAST avoids driver error for sql_variant type
  166. FROM sys.sequences AS seq';
  167. }
  168. /**
  169. * {@inheritdoc}
  170. */
  171. public function getSequenceNextValSQL($sequence)
  172. {
  173. return 'SELECT NEXT VALUE FOR ' . $sequence;
  174. }
  175. /**
  176. * {@inheritDoc}
  177. */
  178. public function hasNativeGuidType()
  179. {
  180. return true;
  181. }
  182. /**
  183. * {@inheritDoc}
  184. */
  185. public function getDropForeignKeySQL($foreignKey, $table)
  186. {
  187. if (! $foreignKey instanceof ForeignKeyConstraint) {
  188. $foreignKey = new Identifier($foreignKey);
  189. }
  190. if (! $table instanceof Table) {
  191. $table = new Identifier($table);
  192. }
  193. $foreignKey = $foreignKey->getQuotedName($this);
  194. $table = $table->getQuotedName($this);
  195. return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey;
  196. }
  197. /**
  198. * {@inheritDoc}
  199. */
  200. public function getDropIndexSQL($index, $table = null)
  201. {
  202. if ($index instanceof Index) {
  203. $index = $index->getQuotedName($this);
  204. } elseif (! is_string($index)) {
  205. throw new InvalidArgumentException(
  206. __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.'
  207. );
  208. }
  209. if ($table instanceof Table) {
  210. $table = $table->getQuotedName($this);
  211. } elseif (! is_string($table)) {
  212. throw new InvalidArgumentException(
  213. __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.'
  214. );
  215. }
  216. return 'DROP INDEX ' . $index . ' ON ' . $table;
  217. }
  218. /**
  219. * {@inheritDoc}
  220. */
  221. protected function _getCreateTableSQL($name, array $columns, array $options = [])
  222. {
  223. $defaultConstraintsSql = [];
  224. $commentsSql = [];
  225. $tableComment = $options['comment'] ?? null;
  226. if ($tableComment !== null) {
  227. $commentsSql[] = $this->getCommentOnTableSQL($name, $tableComment);
  228. }
  229. // @todo does other code breaks because of this?
  230. // force primary keys to be not null
  231. foreach ($columns as &$column) {
  232. if (! empty($column['primary'])) {
  233. $column['notnull'] = true;
  234. }
  235. // Build default constraints SQL statements.
  236. if (isset($column['default'])) {
  237. $defaultConstraintsSql[] = 'ALTER TABLE ' . $name .
  238. ' ADD' . $this->getDefaultConstraintDeclarationSQL($name, $column);
  239. }
  240. if (empty($column['comment']) && ! is_numeric($column['comment'])) {
  241. continue;
  242. }
  243. $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']);
  244. }
  245. $columnListSql = $this->getColumnDeclarationListSQL($columns);
  246. if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) {
  247. foreach ($options['uniqueConstraints'] as $constraintName => $definition) {
  248. $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition);
  249. }
  250. }
  251. if (isset($options['primary']) && ! empty($options['primary'])) {
  252. $flags = '';
  253. if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) {
  254. $flags = ' NONCLUSTERED';
  255. }
  256. $columnListSql .= ', PRIMARY KEY' . $flags
  257. . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')';
  258. }
  259. $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql;
  260. $check = $this->getCheckDeclarationSQL($columns);
  261. if (! empty($check)) {
  262. $query .= ', ' . $check;
  263. }
  264. $query .= ')';
  265. $sql = [$query];
  266. if (isset($options['indexes']) && ! empty($options['indexes'])) {
  267. foreach ($options['indexes'] as $index) {
  268. $sql[] = $this->getCreateIndexSQL($index, $name);
  269. }
  270. }
  271. if (isset($options['foreignKeys'])) {
  272. foreach ((array) $options['foreignKeys'] as $definition) {
  273. $sql[] = $this->getCreateForeignKeySQL($definition, $name);
  274. }
  275. }
  276. return array_merge($sql, $commentsSql, $defaultConstraintsSql);
  277. }
  278. /**
  279. * {@inheritDoc}
  280. */
  281. public function getCreatePrimaryKeySQL(Index $index, $table)
  282. {
  283. if ($table instanceof Table) {
  284. $identifier = $table->getQuotedName($this);
  285. } else {
  286. $identifier = $table;
  287. }
  288. $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY';
  289. if ($index->hasFlag('nonclustered')) {
  290. $sql .= ' NONCLUSTERED';
  291. }
  292. return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')';
  293. }
  294. /**
  295. * Returns the SQL statement for creating a column comment.
  296. *
  297. * SQL Server does not support native column comments,
  298. * therefore the extended properties functionality is used
  299. * as a workaround to store them.
  300. * The property name used to store column comments is "MS_Description"
  301. * which provides compatibility with SQL Server Management Studio,
  302. * as column comments are stored in the same property there when
  303. * specifying a column's "Description" attribute.
  304. *
  305. * @param string $tableName The quoted table name to which the column belongs.
  306. * @param string $columnName The quoted column name to create the comment for.
  307. * @param string|null $comment The column's comment.
  308. *
  309. * @return string
  310. */
  311. protected function getCreateColumnCommentSQL($tableName, $columnName, $comment)
  312. {
  313. if (strpos($tableName, '.') !== false) {
  314. [$schemaSQL, $tableSQL] = explode('.', $tableName);
  315. $schemaSQL = $this->quoteStringLiteral($schemaSQL);
  316. $tableSQL = $this->quoteStringLiteral($tableSQL);
  317. } else {
  318. $schemaSQL = "'dbo'";
  319. $tableSQL = $this->quoteStringLiteral($tableName);
  320. }
  321. return $this->getAddExtendedPropertySQL(
  322. 'MS_Description',
  323. $comment,
  324. 'SCHEMA',
  325. $schemaSQL,
  326. 'TABLE',
  327. $tableSQL,
  328. 'COLUMN',
  329. $columnName
  330. );
  331. }
  332. /**
  333. * Returns the SQL snippet for declaring a default constraint.
  334. *
  335. * @internal The method should be only used from within the SQLServerPlatform class hierarchy.
  336. *
  337. * @param string $table Name of the table to return the default constraint declaration for.
  338. * @param mixed[] $column Column definition.
  339. *
  340. * @return string
  341. *
  342. * @throws InvalidArgumentException
  343. */
  344. public function getDefaultConstraintDeclarationSQL($table, array $column)
  345. {
  346. if (! isset($column['default'])) {
  347. throw new InvalidArgumentException("Incomplete column definition. 'default' required.");
  348. }
  349. $columnName = new Identifier($column['name']);
  350. return ' CONSTRAINT ' .
  351. $this->generateDefaultConstraintName($table, $column['name']) .
  352. $this->getDefaultValueDeclarationSQL($column) .
  353. ' FOR ' . $columnName->getQuotedName($this);
  354. }
  355. /**
  356. * {@inheritDoc}
  357. */
  358. public function getCreateIndexSQL(Index $index, $table)
  359. {
  360. $constraint = parent::getCreateIndexSQL($index, $table);
  361. if ($index->isUnique() && ! $index->isPrimary()) {
  362. $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index);
  363. }
  364. return $constraint;
  365. }
  366. /**
  367. * {@inheritDoc}
  368. */
  369. protected function getCreateIndexSQLFlags(Index $index)
  370. {
  371. $type = '';
  372. if ($index->isUnique()) {
  373. $type .= 'UNIQUE ';
  374. }
  375. if ($index->hasFlag('clustered')) {
  376. $type .= 'CLUSTERED ';
  377. } elseif ($index->hasFlag('nonclustered')) {
  378. $type .= 'NONCLUSTERED ';
  379. }
  380. return $type;
  381. }
  382. /**
  383. * Extend unique key constraint with required filters
  384. *
  385. * @param string $sql
  386. */
  387. private function _appendUniqueConstraintDefinition($sql, Index $index): string
  388. {
  389. $fields = [];
  390. foreach ($index->getQuotedColumns($this) as $field) {
  391. $fields[] = $field . ' IS NOT NULL';
  392. }
  393. return $sql . ' WHERE ' . implode(' AND ', $fields);
  394. }
  395. /**
  396. * {@inheritDoc}
  397. */
  398. public function getAlterTableSQL(TableDiff $diff)
  399. {
  400. $queryParts = [];
  401. $sql = [];
  402. $columnSql = [];
  403. $commentsSql = [];
  404. foreach ($diff->addedColumns as $column) {
  405. if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) {
  406. continue;
  407. }
  408. $columnDef = $column->toArray();
  409. $addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
  410. if (isset($columnDef['default'])) {
  411. $addColumnSql .= ' CONSTRAINT ' .
  412. $this->generateDefaultConstraintName($diff->name, $column->getQuotedName($this)) .
  413. $this->getDefaultValueDeclarationSQL($columnDef);
  414. }
  415. $queryParts[] = $addColumnSql;
  416. $comment = $this->getColumnComment($column);
  417. if (empty($comment) && ! is_numeric($comment)) {
  418. continue;
  419. }
  420. $commentsSql[] = $this->getCreateColumnCommentSQL(
  421. $diff->name,
  422. $column->getQuotedName($this),
  423. $comment
  424. );
  425. }
  426. foreach ($diff->removedColumns as $column) {
  427. if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) {
  428. continue;
  429. }
  430. $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this);
  431. }
  432. foreach ($diff->changedColumns as $columnDiff) {
  433. if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) {
  434. continue;
  435. }
  436. $column = $columnDiff->column;
  437. $comment = $this->getColumnComment($column);
  438. $hasComment = ! empty($comment) || is_numeric($comment);
  439. if ($columnDiff->fromColumn instanceof Column) {
  440. $fromComment = $this->getColumnComment($columnDiff->fromColumn);
  441. $hasFromComment = ! empty($fromComment) || is_numeric($fromComment);
  442. if ($hasFromComment && $hasComment && $fromComment !== $comment) {
  443. $commentsSql[] = $this->getAlterColumnCommentSQL(
  444. $diff->name,
  445. $column->getQuotedName($this),
  446. $comment
  447. );
  448. } elseif ($hasFromComment && ! $hasComment) {
  449. $commentsSql[] = $this->getDropColumnCommentSQL($diff->name, $column->getQuotedName($this));
  450. } elseif (! $hasFromComment && $hasComment) {
  451. $commentsSql[] = $this->getCreateColumnCommentSQL(
  452. $diff->name,
  453. $column->getQuotedName($this),
  454. $comment
  455. );
  456. }
  457. }
  458. // Do not add query part if only comment has changed.
  459. if ($columnDiff->hasChanged('comment') && count($columnDiff->changedProperties) === 1) {
  460. continue;
  461. }
  462. $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff);
  463. if ($requireDropDefaultConstraint) {
  464. $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
  465. $diff->name,
  466. $columnDiff->oldColumnName
  467. );
  468. }
  469. $columnDef = $column->toArray();
  470. $queryParts[] = 'ALTER COLUMN ' .
  471. $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef);
  472. if (
  473. ! isset($columnDef['default'])
  474. || (! $requireDropDefaultConstraint && ! $columnDiff->hasChanged('default'))
  475. ) {
  476. continue;
  477. }
  478. $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
  479. }
  480. foreach ($diff->renamedColumns as $oldColumnName => $column) {
  481. if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) {
  482. continue;
  483. }
  484. $oldColumnName = new Identifier($oldColumnName);
  485. $sql[] = "sp_rename '" .
  486. $diff->getName($this)->getQuotedName($this) . '.' . $oldColumnName->getQuotedName($this) .
  487. "', '" . $column->getQuotedName($this) . "', 'COLUMN'";
  488. // Recreate default constraint with new column name if necessary (for future reference).
  489. if ($column->getDefault() === null) {
  490. continue;
  491. }
  492. $queryParts[] = $this->getAlterTableDropDefaultConstraintClause(
  493. $diff->name,
  494. $oldColumnName->getQuotedName($this)
  495. );
  496. $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($diff->name, $column);
  497. }
  498. $tableSql = [];
  499. if ($this->onSchemaAlterTable($diff, $tableSql)) {
  500. return array_merge($tableSql, $columnSql);
  501. }
  502. foreach ($queryParts as $query) {
  503. $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' . $query;
  504. }
  505. $sql = array_merge($sql, $commentsSql);
  506. $newName = $diff->getNewName();
  507. if ($newName !== false) {
  508. $sql[] = "sp_rename '" . $diff->getName($this)->getQuotedName($this) . "', '" . $newName->getName() . "'";
  509. /**
  510. * Rename table's default constraints names
  511. * to match the new table name.
  512. * This is necessary to ensure that the default
  513. * constraints can be referenced in future table
  514. * alterations as the table name is encoded in
  515. * default constraints' names.
  516. */
  517. $sql[] = "DECLARE @sql NVARCHAR(MAX) = N''; " .
  518. "SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' " .
  519. "+ REPLACE(dc.name, '" . $this->generateIdentifierName($diff->name) . "', " .
  520. "'" . $this->generateIdentifierName($newName->getName()) . "') + ''', ''OBJECT'';' " .
  521. 'FROM sys.default_constraints dc ' .
  522. 'JOIN sys.tables tbl ON dc.parent_object_id = tbl.object_id ' .
  523. "WHERE tbl.name = '" . $newName->getName() . "';" .
  524. 'EXEC sp_executesql @sql';
  525. }
  526. $sql = array_merge(
  527. $this->getPreAlterTableIndexForeignKeySQL($diff),
  528. $sql,
  529. $this->getPostAlterTableIndexForeignKeySQL($diff)
  530. );
  531. return array_merge($sql, $tableSql, $columnSql);
  532. }
  533. /**
  534. * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement.
  535. *
  536. * @param string $tableName The name of the table to generate the clause for.
  537. * @param Column $column The column to generate the clause for.
  538. */
  539. private function getAlterTableAddDefaultConstraintClause($tableName, Column $column): string
  540. {
  541. $columnDef = $column->toArray();
  542. $columnDef['name'] = $column->getQuotedName($this);
  543. return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef);
  544. }
  545. /**
  546. * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement.
  547. *
  548. * @param string $tableName The name of the table to generate the clause for.
  549. * @param string $columnName The name of the column to generate the clause for.
  550. */
  551. private function getAlterTableDropDefaultConstraintClause($tableName, $columnName): string
  552. {
  553. return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName);
  554. }
  555. /**
  556. * Checks whether a column alteration requires dropping its default constraint first.
  557. *
  558. * Different to other database vendors SQL Server implements column default values
  559. * as constraints and therefore changes in a column's default value as well as changes
  560. * in a column's type require dropping the default constraint first before being to
  561. * alter the particular column to the new definition.
  562. */
  563. private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff): bool
  564. {
  565. // We can only decide whether to drop an existing default constraint
  566. // if we know the original default value.
  567. if (! $columnDiff->fromColumn instanceof Column) {
  568. return false;
  569. }
  570. // We only need to drop an existing default constraint if we know the
  571. // column was defined with a default value before.
  572. if ($columnDiff->fromColumn->getDefault() === null) {
  573. return false;
  574. }
  575. // We need to drop an existing default constraint if the column was
  576. // defined with a default value before and it has changed.
  577. if ($columnDiff->hasChanged('default')) {
  578. return true;
  579. }
  580. // We need to drop an existing default constraint if the column was
  581. // defined with a default value before and the native column type has changed.
  582. return $columnDiff->hasChanged('type') || $columnDiff->hasChanged('fixed');
  583. }
  584. /**
  585. * Returns the SQL statement for altering a column comment.
  586. *
  587. * SQL Server does not support native column comments,
  588. * therefore the extended properties functionality is used
  589. * as a workaround to store them.
  590. * The property name used to store column comments is "MS_Description"
  591. * which provides compatibility with SQL Server Management Studio,
  592. * as column comments are stored in the same property there when
  593. * specifying a column's "Description" attribute.
  594. *
  595. * @param string $tableName The quoted table name to which the column belongs.
  596. * @param string $columnName The quoted column name to alter the comment for.
  597. * @param string|null $comment The column's comment.
  598. *
  599. * @return string
  600. */
  601. protected function getAlterColumnCommentSQL($tableName, $columnName, $comment)
  602. {
  603. if (strpos($tableName, '.') !== false) {
  604. [$schemaSQL, $tableSQL] = explode('.', $tableName);
  605. $schemaSQL = $this->quoteStringLiteral($schemaSQL);
  606. $tableSQL = $this->quoteStringLiteral($tableSQL);
  607. } else {
  608. $schemaSQL = "'dbo'";
  609. $tableSQL = $this->quoteStringLiteral($tableName);
  610. }
  611. return $this->getUpdateExtendedPropertySQL(
  612. 'MS_Description',
  613. $comment,
  614. 'SCHEMA',
  615. $schemaSQL,
  616. 'TABLE',
  617. $tableSQL,
  618. 'COLUMN',
  619. $columnName
  620. );
  621. }
  622. /**
  623. * Returns the SQL statement for dropping a column comment.
  624. *
  625. * SQL Server does not support native column comments,
  626. * therefore the extended properties functionality is used
  627. * as a workaround to store them.
  628. * The property name used to store column comments is "MS_Description"
  629. * which provides compatibility with SQL Server Management Studio,
  630. * as column comments are stored in the same property there when
  631. * specifying a column's "Description" attribute.
  632. *
  633. * @param string $tableName The quoted table name to which the column belongs.
  634. * @param string $columnName The quoted column name to drop the comment for.
  635. *
  636. * @return string
  637. */
  638. protected function getDropColumnCommentSQL($tableName, $columnName)
  639. {
  640. if (strpos($tableName, '.') !== false) {
  641. [$schemaSQL, $tableSQL] = explode('.', $tableName);
  642. $schemaSQL = $this->quoteStringLiteral($schemaSQL);
  643. $tableSQL = $this->quoteStringLiteral($tableSQL);
  644. } else {
  645. $schemaSQL = "'dbo'";
  646. $tableSQL = $this->quoteStringLiteral($tableName);
  647. }
  648. return $this->getDropExtendedPropertySQL(
  649. 'MS_Description',
  650. 'SCHEMA',
  651. $schemaSQL,
  652. 'TABLE',
  653. $tableSQL,
  654. 'COLUMN',
  655. $columnName
  656. );
  657. }
  658. /**
  659. * {@inheritdoc}
  660. */
  661. protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName)
  662. {
  663. return [sprintf(
  664. "EXEC sp_rename N'%s.%s', N'%s', N'INDEX'",
  665. $tableName,
  666. $oldIndexName,
  667. $index->getQuotedName($this)
  668. ),
  669. ];
  670. }
  671. /**
  672. * Returns the SQL statement for adding an extended property to a database object.
  673. *
  674. * @internal The method should be only used from within the SQLServerPlatform class hierarchy.
  675. *
  676. * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx
  677. *
  678. * @param string $name The name of the property to add.
  679. * @param string|null $value The value of the property to add.
  680. * @param string|null $level0Type The type of the object at level 0 the property belongs to.
  681. * @param string|null $level0Name The name of the object at level 0 the property belongs to.
  682. * @param string|null $level1Type The type of the object at level 1 the property belongs to.
  683. * @param string|null $level1Name The name of the object at level 1 the property belongs to.
  684. * @param string|null $level2Type The type of the object at level 2 the property belongs to.
  685. * @param string|null $level2Name The name of the object at level 2 the property belongs to.
  686. *
  687. * @return string
  688. */
  689. public function getAddExtendedPropertySQL(
  690. $name,
  691. $value = null,
  692. $level0Type = null,
  693. $level0Name = null,
  694. $level1Type = null,
  695. $level1Name = null,
  696. $level2Type = null,
  697. $level2Name = null
  698. ) {
  699. return 'EXEC sp_addextendedproperty ' .
  700. 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
  701. 'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
  702. 'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
  703. 'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
  704. }
  705. /**
  706. * Returns the SQL statement for dropping an extended property from a database object.
  707. *
  708. * @internal The method should be only used from within the SQLServerPlatform class hierarchy.
  709. *
  710. * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx
  711. *
  712. * @param string $name The name of the property to drop.
  713. * @param string|null $level0Type The type of the object at level 0 the property belongs to.
  714. * @param string|null $level0Name The name of the object at level 0 the property belongs to.
  715. * @param string|null $level1Type The type of the object at level 1 the property belongs to.
  716. * @param string|null $level1Name The name of the object at level 1 the property belongs to.
  717. * @param string|null $level2Type The type of the object at level 2 the property belongs to.
  718. * @param string|null $level2Name The name of the object at level 2 the property belongs to.
  719. *
  720. * @return string
  721. */
  722. public function getDropExtendedPropertySQL(
  723. $name,
  724. $level0Type = null,
  725. $level0Name = null,
  726. $level1Type = null,
  727. $level1Name = null,
  728. $level2Type = null,
  729. $level2Name = null
  730. ) {
  731. return 'EXEC sp_dropextendedproperty ' .
  732. 'N' . $this->quoteStringLiteral($name) . ', ' .
  733. 'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
  734. 'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
  735. 'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
  736. }
  737. /**
  738. * Returns the SQL statement for updating an extended property of a database object.
  739. *
  740. * @internal The method should be only used from within the SQLServerPlatform class hierarchy.
  741. *
  742. * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx
  743. *
  744. * @param string $name The name of the property to update.
  745. * @param string|null $value The value of the property to update.
  746. * @param string|null $level0Type The type of the object at level 0 the property belongs to.
  747. * @param string|null $level0Name The name of the object at level 0 the property belongs to.
  748. * @param string|null $level1Type The type of the object at level 1 the property belongs to.
  749. * @param string|null $level1Name The name of the object at level 1 the property belongs to.
  750. * @param string|null $level2Type The type of the object at level 2 the property belongs to.
  751. * @param string|null $level2Name The name of the object at level 2 the property belongs to.
  752. *
  753. * @return string
  754. */
  755. public function getUpdateExtendedPropertySQL(
  756. $name,
  757. $value = null,
  758. $level0Type = null,
  759. $level0Name = null,
  760. $level1Type = null,
  761. $level1Name = null,
  762. $level2Type = null,
  763. $level2Name = null
  764. ) {
  765. return 'EXEC sp_updateextendedproperty ' .
  766. 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral((string) $value) . ', ' .
  767. 'N' . $this->quoteStringLiteral((string) $level0Type) . ', ' . $level0Name . ', ' .
  768. 'N' . $this->quoteStringLiteral((string) $level1Type) . ', ' . $level1Name . ', ' .
  769. 'N' . $this->quoteStringLiteral((string) $level2Type) . ', ' . $level2Name;
  770. }
  771. /**
  772. * {@inheritDoc}
  773. */
  774. public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName)
  775. {
  776. return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES';
  777. }
  778. /**
  779. * {@inheritDoc}
  780. */
  781. public function getListTablesSQL()
  782. {
  783. // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
  784. // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication
  785. return 'SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects'
  786. . " WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name";
  787. }
  788. /**
  789. * {@inheritDoc}
  790. */
  791. public function getListTableColumnsSQL($table, $database = null)
  792. {
  793. return "SELECT col.name,
  794. type.name AS type,
  795. col.max_length AS length,
  796. ~col.is_nullable AS notnull,
  797. def.definition AS [default],
  798. col.scale,
  799. col.precision,
  800. col.is_identity AS autoincrement,
  801. col.collation_name AS collation,
  802. CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type
  803. FROM sys.columns AS col
  804. JOIN sys.types AS type
  805. ON col.user_type_id = type.user_type_id
  806. JOIN sys.objects AS obj
  807. ON col.object_id = obj.object_id
  808. JOIN sys.schemas AS scm
  809. ON obj.schema_id = scm.schema_id
  810. LEFT JOIN sys.default_constraints def
  811. ON col.default_object_id = def.object_id
  812. AND col.object_id = def.parent_object_id
  813. LEFT JOIN sys.extended_properties AS prop
  814. ON obj.object_id = prop.major_id
  815. AND col.column_id = prop.minor_id
  816. AND prop.name = 'MS_Description'
  817. WHERE obj.type = 'U'
  818. AND " . $this->getTableWhereClause($table, 'scm.name', 'obj.name');
  819. }
  820. /**
  821. * @param string $table
  822. * @param string|null $database
  823. *
  824. * @return string
  825. */
  826. public function getListTableForeignKeysSQL($table, $database = null)
  827. {
  828. return 'SELECT f.name AS ForeignKey,
  829. SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
  830. OBJECT_NAME (f.parent_object_id) AS TableName,
  831. COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
  832. SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
  833. OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
  834. COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
  835. f.delete_referential_action_desc,
  836. f.update_referential_action_desc
  837. FROM sys.foreign_keys AS f
  838. INNER JOIN sys.foreign_key_columns AS fc
  839. INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
  840. ON f.OBJECT_ID = fc.constraint_object_id
  841. WHERE ' .
  842. $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)') .
  843. ' ORDER BY fc.constraint_column_id';
  844. }
  845. /**
  846. * {@inheritDoc}
  847. */
  848. public function getListTableIndexesSQL($table, $database = null)
  849. {
  850. return "SELECT idx.name AS key_name,
  851. col.name AS column_name,
  852. ~idx.is_unique AS non_unique,
  853. idx.is_primary_key AS [primary],
  854. CASE idx.type
  855. WHEN '1' THEN 'clustered'
  856. WHEN '2' THEN 'nonclustered'
  857. ELSE NULL
  858. END AS flags
  859. FROM sys.tables AS tbl
  860. JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id
  861. JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id
  862. JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id
  863. JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id
  864. WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . '
  865. ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC';
  866. }
  867. /**
  868. * {@inheritDoc}
  869. */
  870. public function getListViewsSQL($database)
  871. {
  872. return "SELECT name, definition FROM sysobjects
  873. INNER JOIN sys.sql_modules ON sysobjects.id = sys.sql_modules.object_id
  874. WHERE type = 'V' ORDER BY name";
  875. }
  876. /**
  877. * Returns the where clause to filter schema and table name in a query.
  878. *
  879. * @param string $table The full qualified name of the table.
  880. * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
  881. * @param string $tableColumn The name of the column to compare the table to in the where clause.
  882. */
  883. private function getTableWhereClause($table, $schemaColumn, $tableColumn): string
  884. {
  885. if (strpos($table, '.') !== false) {
  886. [$schema, $table] = explode('.', $table);
  887. $schema = $this->quoteStringLiteral($schema);
  888. $table = $this->quoteStringLiteral($table);
  889. } else {
  890. $schema = 'SCHEMA_NAME()';
  891. $table = $this->quoteStringLiteral($table);
  892. }
  893. return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
  894. }
  895. /**
  896. * {@inheritDoc}
  897. */
  898. public function getLocateExpression($str, $substr, $startPos = false)
  899. {
  900. if ($startPos === false) {
  901. return 'CHARINDEX(' . $substr . ', ' . $str . ')';
  902. }
  903. return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')';
  904. }
  905. /**
  906. * {@inheritDoc}
  907. */
  908. public function getModExpression($expression1, $expression2)
  909. {
  910. return $expression1 . ' % ' . $expression2;
  911. }
  912. /**
  913. * {@inheritDoc}
  914. */
  915. public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false)
  916. {
  917. if ($char === false) {
  918. switch ($mode) {
  919. case TrimMode::LEADING:
  920. $trimFn = 'LTRIM';
  921. break;
  922. case TrimMode::TRAILING:
  923. $trimFn = 'RTRIM';
  924. break;
  925. default:
  926. return 'LTRIM(RTRIM(' . $str . '))';
  927. }
  928. return $trimFn . '(' . $str . ')';
  929. }
  930. $pattern = "'%[^' + " . $char . " + ']%'";
  931. if ($mode === TrimMode::LEADING) {
  932. return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)';
  933. }
  934. if ($mode === TrimMode::TRAILING) {
  935. return 'reverse(stuff(reverse(' . $str . '), 1, '
  936. . 'patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))';
  937. }
  938. return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, '
  939. . 'patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str
  940. . ') - 1, null))) - 1, null))';
  941. }
  942. /**
  943. * {@inheritDoc}
  944. */
  945. public function getConcatExpression()
  946. {
  947. return sprintf('CONCAT(%s)', implode(', ', func_get_args()));
  948. }
  949. /**
  950. * {@inheritDoc}
  951. */
  952. public function getListDatabasesSQL()
  953. {
  954. return 'SELECT * FROM sys.databases';
  955. }
  956. /**
  957. * {@inheritDoc}
  958. *
  959. * @deprecated Use {@see SQLServerSchemaManager::listSchemaNames()} instead.
  960. */
  961. public function getListNamespacesSQL()
  962. {
  963. Deprecation::triggerIfCalledFromOutside(
  964. 'doctrine/dbal',
  965. 'https://github.com/doctrine/dbal/issues/4503',
  966. 'SQLServerPlatform::getListNamespacesSQL() is deprecated,'
  967. . ' use SQLServerSchemaManager::listSchemaNames() instead.'
  968. );
  969. return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')";
  970. }
  971. /**
  972. * {@inheritDoc}
  973. */
  974. public function getSubstringExpression($string, $start, $length = null)
  975. {
  976. if ($length !== null) {
  977. return 'SUBSTRING(' . $string . ', ' . $start . ', ' . $length . ')';
  978. }
  979. return 'SUBSTRING(' . $string . ', ' . $start . ', LEN(' . $string . ') - ' . $start . ' + 1)';
  980. }
  981. /**
  982. * {@inheritDoc}
  983. */
  984. public function getLengthExpression($column)
  985. {
  986. return 'LEN(' . $column . ')';
  987. }
  988. public function getCurrentDatabaseExpression(): string
  989. {
  990. return 'DB_NAME()';
  991. }
  992. /**
  993. * {@inheritDoc}
  994. */
  995. public function getSetTransactionIsolationSQL($level)
  996. {
  997. return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level);
  998. }
  999. /**
  1000. * {@inheritDoc}
  1001. */
  1002. public function getIntegerTypeDeclarationSQL(array $column)
  1003. {
  1004. return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
  1005. }
  1006. /**
  1007. * {@inheritDoc}
  1008. */
  1009. public function getBigIntTypeDeclarationSQL(array $column)
  1010. {
  1011. return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
  1012. }
  1013. /**
  1014. * {@inheritDoc}
  1015. */
  1016. public function getSmallIntTypeDeclarationSQL(array $column)
  1017. {
  1018. return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column);
  1019. }
  1020. /**
  1021. * {@inheritDoc}
  1022. */
  1023. public function getGuidTypeDeclarationSQL(array $column)
  1024. {
  1025. return 'UNIQUEIDENTIFIER';
  1026. }
  1027. /**
  1028. * {@inheritDoc}
  1029. */
  1030. public function getDateTimeTzTypeDeclarationSQL(array $column)
  1031. {
  1032. return 'DATETIMEOFFSET(6)';
  1033. }
  1034. /**
  1035. * {@inheritDoc}
  1036. */
  1037. public function getAsciiStringTypeDeclarationSQL(array $column): string
  1038. {
  1039. $length = $column['length'] ?? null;
  1040. if (! isset($column['fixed'])) {
  1041. return sprintf('VARCHAR(%d)', $length ?? 255);
  1042. }
  1043. return sprintf('CHAR(%d)', $length ?? 255);
  1044. }
  1045. /**
  1046. * {@inheritDoc}
  1047. */
  1048. protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
  1049. {
  1050. return $fixed
  1051. ? ($length > 0 ? 'NCHAR(' . $length . ')' : 'CHAR(255)')
  1052. : ($length > 0 ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)');
  1053. }
  1054. /**
  1055. * {@inheritdoc}
  1056. */
  1057. protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed)
  1058. {
  1059. return $fixed
  1060. ? 'BINARY(' . ($length > 0 ? $length : 255) . ')'
  1061. : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')';
  1062. }
  1063. /**
  1064. * {@inheritdoc}
  1065. */
  1066. public function getBinaryMaxLength()
  1067. {
  1068. return 8000;
  1069. }
  1070. /**
  1071. * {@inheritDoc}
  1072. */
  1073. public function getClobTypeDeclarationSQL(array $column)
  1074. {
  1075. return 'VARCHAR(MAX)';
  1076. }
  1077. /**
  1078. * {@inheritDoc}
  1079. */
  1080. protected function _getCommonIntegerTypeDeclarationSQL(array $column)
  1081. {
  1082. return ! empty($column['autoincrement']) ? ' IDENTITY' : '';
  1083. }
  1084. /**
  1085. * {@inheritDoc}
  1086. */
  1087. public function getDateTimeTypeDeclarationSQL(array $column)
  1088. {
  1089. // 3 - microseconds precision length
  1090. // http://msdn.microsoft.com/en-us/library/ms187819.aspx
  1091. return 'DATETIME2(6)';
  1092. }
  1093. /**
  1094. * {@inheritDoc}
  1095. */
  1096. public function getDateTypeDeclarationSQL(array $column)
  1097. {
  1098. return 'DATE';
  1099. }
  1100. /**
  1101. * {@inheritDoc}
  1102. */
  1103. public function getTimeTypeDeclarationSQL(array $column)
  1104. {
  1105. return 'TIME(0)';
  1106. }
  1107. /**
  1108. * {@inheritDoc}
  1109. */
  1110. public function getBooleanTypeDeclarationSQL(array $column)
  1111. {
  1112. return 'BIT';
  1113. }
  1114. /**
  1115. * {@inheritDoc}
  1116. */
  1117. protected function doModifyLimitQuery($query, $limit, $offset)
  1118. {
  1119. if ($limit === null && $offset <= 0) {
  1120. return $query;
  1121. }
  1122. if ($this->shouldAddOrderBy($query)) {
  1123. if (preg_match('/^SELECT\s+DISTINCT/im', $query) > 0) {
  1124. // SQL Server won't let us order by a non-selected column in a DISTINCT query,
  1125. // so we have to do this madness. This says, order by the first column in the
  1126. // result. SQL Server's docs say that a nonordered query's result order is non-
  1127. // deterministic anyway, so this won't do anything that a bunch of update and
  1128. // deletes to the table wouldn't do anyway.
  1129. $query .= ' ORDER BY 1';
  1130. } else {
  1131. // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you
  1132. // use constant expressions in the order by list.
  1133. $query .= ' ORDER BY (SELECT 0)';
  1134. }
  1135. }
  1136. // This looks somewhat like MYSQL, but limit/offset are in inverse positions
  1137. // Supposedly SQL:2008 core standard.
  1138. // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS.
  1139. $query .= sprintf(' OFFSET %d ROWS', $offset);
  1140. if ($limit !== null) {
  1141. $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit);
  1142. }
  1143. return $query;
  1144. }
  1145. /**
  1146. * {@inheritDoc}
  1147. */
  1148. public function convertBooleans($item)
  1149. {
  1150. if (is_array($item)) {
  1151. foreach ($item as $key => $value) {
  1152. if (! is_bool($value) && ! is_numeric($value)) {
  1153. continue;
  1154. }
  1155. $item[$key] = (int) (bool) $value;
  1156. }
  1157. } elseif (is_bool($item) || is_numeric($item)) {
  1158. $item = (int) (bool) $item;
  1159. }
  1160. return $item;
  1161. }
  1162. /**
  1163. * {@inheritDoc}
  1164. */
  1165. public function getCreateTemporaryTableSnippetSQL()
  1166. {
  1167. return 'CREATE TABLE';
  1168. }
  1169. /**
  1170. * {@inheritDoc}
  1171. */
  1172. public function getTemporaryTableName($tableName)
  1173. {
  1174. return '#' . $tableName;
  1175. }
  1176. /**
  1177. * {@inheritDoc}
  1178. */
  1179. public function getDateTimeFormatString()
  1180. {
  1181. return 'Y-m-d H:i:s.u';
  1182. }
  1183. /**
  1184. * {@inheritDoc}
  1185. */
  1186. public function getDateFormatString()
  1187. {
  1188. return 'Y-m-d';
  1189. }
  1190. /**
  1191. * {@inheritDoc}
  1192. */
  1193. public function getTimeFormatString()
  1194. {
  1195. return 'H:i:s';
  1196. }
  1197. /**
  1198. * {@inheritDoc}
  1199. */
  1200. public function getDateTimeTzFormatString()
  1201. {
  1202. return 'Y-m-d H:i:s.u P';
  1203. }
  1204. /**
  1205. * {@inheritDoc}
  1206. */
  1207. public function getName()
  1208. {
  1209. return 'mssql';
  1210. }
  1211. /**
  1212. * {@inheritDoc}
  1213. */
  1214. protected function initializeDoctrineTypeMappings()
  1215. {
  1216. $this->doctrineTypeMapping = [
  1217. 'bigint' => 'bigint',
  1218. 'binary' => 'binary',
  1219. 'bit' => 'boolean',
  1220. 'blob' => 'blob',
  1221. 'char' => 'string',
  1222. 'date' => 'date',
  1223. 'datetime' => 'datetime',
  1224. 'datetime2' => 'datetime',
  1225. 'datetimeoffset' => 'datetimetz',
  1226. 'decimal' => 'decimal',
  1227. 'double' => 'float',
  1228. 'double precision' => 'float',
  1229. 'float' => 'float',
  1230. 'image' => 'blob',
  1231. 'int' => 'integer',
  1232. 'money' => 'integer',
  1233. 'nchar' => 'string',
  1234. 'ntext' => 'text',
  1235. 'numeric' => 'decimal',
  1236. 'nvarchar' => 'string',
  1237. 'real' => 'float',
  1238. 'smalldatetime' => 'datetime',
  1239. 'smallint' => 'smallint',
  1240. 'smallmoney' => 'integer',
  1241. 'text' => 'text',
  1242. 'time' => 'time',
  1243. 'tinyint' => 'smallint',
  1244. 'uniqueidentifier' => 'guid',
  1245. 'varbinary' => 'binary',
  1246. 'varchar' => 'string',
  1247. ];
  1248. }
  1249. /**
  1250. * {@inheritDoc}
  1251. */
  1252. public function createSavePoint($savepoint)
  1253. {
  1254. return 'SAVE TRANSACTION ' . $savepoint;
  1255. }
  1256. /**
  1257. * {@inheritDoc}
  1258. */
  1259. public function releaseSavePoint($savepoint)
  1260. {
  1261. return '';
  1262. }
  1263. /**
  1264. * {@inheritDoc}
  1265. */
  1266. public function rollbackSavePoint($savepoint)
  1267. {
  1268. return 'ROLLBACK TRANSACTION ' . $savepoint;
  1269. }
  1270. /**
  1271. * {@inheritdoc}
  1272. */
  1273. public function getForeignKeyReferentialActionSQL($action)
  1274. {
  1275. // RESTRICT is not supported, therefore falling back to NO ACTION.
  1276. if (strtoupper($action) === 'RESTRICT') {
  1277. return 'NO ACTION';
  1278. }
  1279. return parent::getForeignKeyReferentialActionSQL($action);
  1280. }
  1281. public function appendLockHint(string $fromClause, int $lockMode): string
  1282. {
  1283. switch ($lockMode) {
  1284. case LockMode::NONE:
  1285. case LockMode::OPTIMISTIC:
  1286. return $fromClause;
  1287. case LockMode::PESSIMISTIC_READ:
  1288. return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)';
  1289. case LockMode::PESSIMISTIC_WRITE:
  1290. return $fromClause . ' WITH (UPDLOCK, ROWLOCK)';
  1291. default:
  1292. throw InvalidLockMode::fromLockMode($lockMode);
  1293. }
  1294. }
  1295. /**
  1296. * {@inheritDoc}
  1297. */
  1298. public function getForUpdateSQL()
  1299. {
  1300. return ' ';
  1301. }
  1302. /**
  1303. * {@inheritDoc}
  1304. *
  1305. * @deprecated Implement {@see createReservedKeywordsList()} instead.
  1306. */
  1307. protected function getReservedKeywordsClass()
  1308. {
  1309. Deprecation::triggerIfCalledFromOutside(
  1310. 'doctrine/dbal',
  1311. 'https://github.com/doctrine/dbal/issues/4510',
  1312. 'SQLServerPlatform::getReservedKeywordsClass() is deprecated,'
  1313. . ' use SQLServerPlatform::createReservedKeywordsList() instead.'
  1314. );
  1315. return Keywords\SQLServer2012Keywords::class;
  1316. }
  1317. /**
  1318. * {@inheritDoc}
  1319. */
  1320. public function quoteSingleIdentifier($str)
  1321. {
  1322. return '[' . str_replace(']', ']]', $str) . ']';
  1323. }
  1324. /**
  1325. * {@inheritDoc}
  1326. */
  1327. public function getTruncateTableSQL($tableName, $cascade = false)
  1328. {
  1329. $tableIdentifier = new Identifier($tableName);
  1330. return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this);
  1331. }
  1332. /**
  1333. * {@inheritDoc}
  1334. */
  1335. public function getBlobTypeDeclarationSQL(array $column)
  1336. {
  1337. return 'VARBINARY(MAX)';
  1338. }
  1339. /**
  1340. * {@inheritdoc}
  1341. *
  1342. * Modifies column declaration order as it differs in Microsoft SQL Server.
  1343. */
  1344. public function getColumnDeclarationSQL($name, array $column)
  1345. {
  1346. if (isset($column['columnDefinition'])) {
  1347. $columnDef = $this->getCustomTypeDeclarationSQL($column);
  1348. } else {
  1349. $collation = ! empty($column['collation']) ?
  1350. ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : '';
  1351. $notnull = ! empty($column['notnull']) ? ' NOT NULL' : '';
  1352. $unique = ! empty($column['unique']) ?
  1353. ' ' . $this->getUniqueFieldDeclarationSQL() : '';
  1354. $check = ! empty($column['check']) ?
  1355. ' ' . $column['check'] : '';
  1356. $typeDecl = $column['type']->getSQLDeclaration($column, $this);
  1357. $columnDef = $typeDecl . $collation . $notnull . $unique . $check;
  1358. }
  1359. return $name . ' ' . $columnDef;
  1360. }
  1361. public function columnsEqual(Column $column1, Column $column2): bool
  1362. {
  1363. if (! parent::columnsEqual($column1, $column2)) {
  1364. return false;
  1365. }
  1366. return $this->getDefaultValueDeclarationSQL($column1->toArray())
  1367. === $this->getDefaultValueDeclarationSQL($column2->toArray());
  1368. }
  1369. protected function getLikeWildcardCharacters(): string
  1370. {
  1371. return parent::getLikeWildcardCharacters() . '[]^';
  1372. }
  1373. /**
  1374. * Returns a unique default constraint name for a table and column.
  1375. *
  1376. * @param string $table Name of the table to generate the unique default constraint name for.
  1377. * @param string $column Name of the column in the table to generate the unique default constraint name for.
  1378. */
  1379. private function generateDefaultConstraintName($table, $column): string
  1380. {
  1381. return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column);
  1382. }
  1383. /**
  1384. * Returns a hash value for a given identifier.
  1385. *
  1386. * @param string $identifier Identifier to generate a hash value for.
  1387. */
  1388. private function generateIdentifierName($identifier): string
  1389. {
  1390. // Always generate name for unquoted identifiers to ensure consistency.
  1391. $identifier = new Identifier($identifier);
  1392. return strtoupper(dechex(crc32($identifier->getName())));
  1393. }
  1394. protected function getCommentOnTableSQL(string $tableName, ?string $comment): string
  1395. {
  1396. return sprintf(
  1397. <<<'SQL'
  1398. EXEC sys.sp_addextendedproperty @name=N'MS_Description',
  1399. @value=N%s, @level0type=N'SCHEMA', @level0name=N'dbo',
  1400. @level1type=N'TABLE', @level1name=N%s
  1401. SQL
  1402. ,
  1403. $this->quoteStringLiteral((string) $comment),
  1404. $this->quoteStringLiteral($tableName)
  1405. );
  1406. }
  1407. public function getListTableMetadataSQL(string $table): string
  1408. {
  1409. return sprintf(
  1410. <<<'SQL'
  1411. SELECT
  1412. p.value AS [table_comment]
  1413. FROM
  1414. sys.tables AS tbl
  1415. INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
  1416. WHERE
  1417. (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description')
  1418. SQL
  1419. ,
  1420. $this->quoteStringLiteral($table)
  1421. );
  1422. }
  1423. /**
  1424. * @param string $query
  1425. */
  1426. private function shouldAddOrderBy($query): bool
  1427. {
  1428. // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement
  1429. // but can be in a newline
  1430. $matches = [];
  1431. $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE);
  1432. if ($matchesCount === 0) {
  1433. return true;
  1434. }
  1435. // ORDER BY instance may be in a subquery after ORDER BY
  1436. // e.g. SELECT col1 FROM test ORDER BY (SELECT col2 from test ORDER BY col2)
  1437. // if in the searched query ORDER BY clause was found where
  1438. // number of open parentheses after the occurrence of the clause is equal to
  1439. // number of closed brackets after the occurrence of the clause,
  1440. // it means that ORDER BY is included in the query being checked
  1441. while ($matchesCount > 0) {
  1442. $orderByPos = $matches[0][--$matchesCount][1];
  1443. $openBracketsCount = substr_count($query, '(', $orderByPos);
  1444. $closedBracketsCount = substr_count($query, ')', $orderByPos);
  1445. if ($openBracketsCount === $closedBracketsCount) {
  1446. return false;
  1447. }
  1448. }
  1449. return true;
  1450. }
  1451. }