SqlserverDialectTrait.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Database\Dialect;
  16. use Cake\Database\Dialect\TupleComparisonTranslatorTrait;
  17. use Cake\Database\Expression\FunctionExpression;
  18. use Cake\Database\Expression\OrderByExpression;
  19. use Cake\Database\Expression\UnaryExpression;
  20. use Cake\Database\Query;
  21. use Cake\Database\SqlDialectTrait;
  22. use Cake\Database\SqlserverCompiler;
  23. use PDO;
  24. /**
  25. * Contains functions that encapsulates the SQL dialect used by SQLServer,
  26. * including query translators and schema introspection.
  27. *
  28. * @internal
  29. */
  30. trait SqlserverDialectTrait
  31. {
  32. use SqlDialectTrait;
  33. use TupleComparisonTranslatorTrait;
  34. /**
  35. * String used to start a database identifier quoting to make it safe
  36. *
  37. * @var string
  38. */
  39. protected $_startQuote = '[';
  40. /**
  41. * String used to end a database identifier quoting to make it safe
  42. *
  43. * @var string
  44. */
  45. protected $_endQuote = ']';
  46. /**
  47. * Modify the limit/offset to TSQL
  48. *
  49. * @param \Cake\Database\Query $query The query to translate
  50. * @return \Cake\Database\Query The modified query
  51. */
  52. protected function _selectQueryTranslator($query)
  53. {
  54. $limit = $query->clause('limit');
  55. $offset = $query->clause('offset');
  56. if ($limit && $offset === null) {
  57. $query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]);
  58. }
  59. if ($offset !== null && !$query->clause('order')) {
  60. $query->order($query->newExpr()->add('(SELECT NULL)'));
  61. }
  62. if ($this->_version() < 11 && $offset !== null) {
  63. return $this->_pagingSubquery($query, $limit, $offset);
  64. }
  65. return $this->_transformDistinct($query);
  66. }
  67. /**
  68. * Get the version of SQLserver we are connected to.
  69. *
  70. * @return int
  71. */
  72. public function _version()
  73. {
  74. return $this->_connection->getAttribute(PDO::ATTR_SERVER_VERSION);
  75. }
  76. /**
  77. * Generate a paging subquery for older versions of SQLserver.
  78. *
  79. * Prior to SQLServer 2012 there was no equivalent to LIMIT OFFSET, so a subquery must
  80. * be used.
  81. *
  82. * @param \Cake\Database\Query $original The query to wrap in a subquery.
  83. * @param int $limit The number of rows to fetch.
  84. * @param int $offset The number of rows to offset.
  85. * @return \Cake\Database\Query Modified query object.
  86. */
  87. protected function _pagingSubquery($original, $limit, $offset)
  88. {
  89. $field = '_cake_paging_._cake_page_rownum_';
  90. $order = $original->clause('order') ?: new OrderByExpression('(SELECT NULL)');
  91. $query = clone $original;
  92. $query->select([
  93. '_cake_page_rownum_' => new UnaryExpression('ROW_NUMBER() OVER', $order)
  94. ])->limit(null)
  95. ->offset(null)
  96. ->order([], true);
  97. $outer = new Query($query->connection());
  98. $outer->select('*')
  99. ->from(['_cake_paging_' => $query]);
  100. if ($offset) {
  101. $outer->where(["$field > " . (int)$offset]);
  102. }
  103. if ($limit) {
  104. $value = (int)$offset + (int)$limit;
  105. $outer->where(["$field <= $value"]);
  106. }
  107. // Decorate the original query as that is what the
  108. // end developer will be calling execute() on originally.
  109. $original->decorateResults(function ($row) {
  110. if (isset($row['_cake_page_rownum_'])) {
  111. unset($row['_cake_page_rownum_']);
  112. }
  113. return $row;
  114. });
  115. return $outer;
  116. }
  117. /**
  118. * Returns the passed query after rewriting the DISTINCT clause, so that drivers
  119. * that do not support the "ON" part can provide the actual way it should be done
  120. *
  121. * @param Query $original The query to be transformed
  122. * @return Query
  123. */
  124. protected function _transformDistinct($original)
  125. {
  126. if (!is_array($original->clause('distinct'))) {
  127. return $original;
  128. }
  129. $query = clone $original;
  130. $distinct = $query->clause('distinct');
  131. $query->distinct(false);
  132. $order = new OrderByExpression($distinct);
  133. $query
  134. ->select(function ($q) use ($distinct, $order) {
  135. $over = $q->newExpr('ROW_NUMBER() OVER')
  136. ->add('(PARTITION BY')
  137. ->add($q->newExpr()->add($distinct)->type(','))
  138. ->add($order)
  139. ->add(')')
  140. ->type(' ');
  141. return [
  142. '_cake_distinct_pivot_' => $over
  143. ];
  144. })
  145. ->limit(null)
  146. ->offset(null)
  147. ->order([], true);
  148. $outer = new Query($query->connection());
  149. $outer->select('*')
  150. ->from(['_cake_distinct_' => $query])
  151. ->where(['_cake_distinct_pivot_' => 1]);
  152. // Decorate the original query as that is what the
  153. // end developer will be calling execute() on originally.
  154. $original->decorateResults(function ($row) {
  155. if (isset($row['_cake_distinct_pivot_'])) {
  156. unset($row['_cake_distinct_pivot_']);
  157. }
  158. return $row;
  159. });
  160. return $outer;
  161. }
  162. /**
  163. * Returns a dictionary of expressions to be transformed when compiling a Query
  164. * to SQL. Array keys are method names to be called in this class
  165. *
  166. * @return array
  167. */
  168. protected function _expressionTranslators()
  169. {
  170. $namespace = 'Cake\Database\Expression';
  171. return [
  172. $namespace . '\FunctionExpression' => '_transformFunctionExpression',
  173. $namespace . '\TupleComparison' => '_transformTupleComparison'
  174. ];
  175. }
  176. /**
  177. * Receives a FunctionExpression and changes it so that it conforms to this
  178. * SQL dialect.
  179. *
  180. * @param \Cake\Database\Expression\FunctionExpression $expression The function expression to convert to TSQL.
  181. * @return void
  182. */
  183. protected function _transformFunctionExpression(FunctionExpression $expression)
  184. {
  185. switch ($expression->name()) {
  186. case 'CONCAT':
  187. // CONCAT function is expressed as exp1 + exp2
  188. $expression->name('')->type(' +');
  189. break;
  190. case 'DATEDIFF':
  191. $hasDay = false;
  192. $visitor = function ($value) use (&$hasDay) {
  193. if ($value === 'day') {
  194. $hasDay = true;
  195. }
  196. return $value;
  197. };
  198. $expression->iterateParts($visitor);
  199. if (!$hasDay) {
  200. $expression->add(['day' => 'literal'], [], true);
  201. }
  202. break;
  203. case 'CURRENT_DATE':
  204. $time = new FunctionExpression('GETUTCDATE');
  205. $expression->name('CONVERT')->add(['date' => 'literal', $time]);
  206. break;
  207. case 'CURRENT_TIME':
  208. $time = new FunctionExpression('GETUTCDATE');
  209. $expression->name('CONVERT')->add(['time' => 'literal', $time]);
  210. break;
  211. case 'NOW':
  212. $expression->name('GETUTCDATE');
  213. break;
  214. case 'EXTRACT':
  215. $expression->name('DATEPART')->type(' ,');
  216. break;
  217. case 'DATE_ADD':
  218. $params = [];
  219. $visitor = function ($p, $key) use (&$params) {
  220. if ($key === 0) {
  221. $params[2] = $p;
  222. } else {
  223. $valueUnit = explode(' ', $p);
  224. $params[0] = rtrim($valueUnit[1], 's');
  225. $params[1] = $valueUnit[0];
  226. }
  227. return $p;
  228. };
  229. $manipulator = function ($p, $key) use (&$params) {
  230. return $params[$key];
  231. };
  232. $expression
  233. ->name('DATEADD')
  234. ->type(',')
  235. ->iterateParts($visitor)
  236. ->iterateParts($manipulator)
  237. ->add([$params[2] => 'literal']);
  238. break;
  239. case 'DAYOFWEEK':
  240. $expression
  241. ->name('DATEPART')
  242. ->type(' ')
  243. ->add(['weekday, ' => 'literal'], [], true);
  244. break;
  245. }
  246. }
  247. /**
  248. * Get the schema dialect.
  249. *
  250. * Used by Cake\Schema package to reflect schema and
  251. * generate schema.
  252. *
  253. * @return \Cake\Database\Schema\MysqlSchema
  254. */
  255. public function schemaDialect()
  256. {
  257. return new \Cake\Database\Schema\SqlserverSchema($this);
  258. }
  259. /**
  260. * Returns a SQL snippet for creating a new transaction savepoint
  261. *
  262. * @param string $name save point name
  263. * @return string
  264. */
  265. public function savePointSQL($name)
  266. {
  267. return 'SAVE TRANSACTION t' . $name;
  268. }
  269. /**
  270. * Returns a SQL snippet for releasing a previously created save point
  271. *
  272. * @param string $name save point name
  273. * @return string
  274. */
  275. public function releaseSavePointSQL($name)
  276. {
  277. return 'COMMIT TRANSACTION t' . $name;
  278. }
  279. /**
  280. * Returns a SQL snippet for rollbacking a previously created save point
  281. *
  282. * @param string $name save point name
  283. * @return string
  284. */
  285. public function rollbackSavePointSQL($name)
  286. {
  287. return 'ROLLBACK TRANSACTION t' . $name;
  288. }
  289. /**
  290. * {@inheritDoc}
  291. *
  292. * @return \Cake\Database\SqlserverCompiler
  293. */
  294. public function newCompiler()
  295. {
  296. return new SqlserverCompiler();
  297. }
  298. /**
  299. * {@inheritDoc}
  300. */
  301. public function disableForeignKeySQL()
  302. {
  303. return 'EXEC sp_msforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT all"';
  304. }
  305. /**
  306. * {@inheritDoc}
  307. */
  308. public function enableForeignKeySQL()
  309. {
  310. return 'EXEC sp_msforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all"';
  311. }
  312. }