SqlserverTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Database\Driver;
  17. use Cake\Database\Driver;
  18. use Cake\Database\Driver\Sqlserver;
  19. use Cake\Database\Exception\MissingConnectionException;
  20. use Cake\Database\Query;
  21. use Cake\Datasource\ConnectionManager;
  22. use Cake\TestSuite\TestCase;
  23. use InvalidArgumentException;
  24. use PDO;
  25. /**
  26. * Tests Sqlserver driver
  27. */
  28. class SqlserverTest extends TestCase
  29. {
  30. /**
  31. * @var bool
  32. */
  33. protected $missingExtension;
  34. /**
  35. * Set up
  36. */
  37. public function setUp(): void
  38. {
  39. parent::setUp();
  40. $this->missingExtension = !defined('PDO::SQLSRV_ENCODING_UTF8');
  41. }
  42. /**
  43. * data provider for testDnsString
  44. *
  45. * @return array
  46. */
  47. public function dnsStringDataProvider(): array
  48. {
  49. return [
  50. [
  51. [
  52. 'app' => 'CakePHP-Testapp',
  53. 'encoding' => '',
  54. 'connectionPooling' => true,
  55. 'failoverPartner' => 'failover.local',
  56. 'loginTimeout' => 10,
  57. 'multiSubnetFailover' => 'failover.local',
  58. ],
  59. 'sqlsrv:Server=localhost\SQLEXPRESS;Database=cake;MultipleActiveResultSets=false;APP=CakePHP-Testapp;ConnectionPooling=1;Failover_Partner=failover.local;LoginTimeout=10;MultiSubnetFailover=failover.local',
  60. ],
  61. [
  62. [
  63. 'app' => 'CakePHP-Testapp',
  64. 'encoding' => '',
  65. 'failoverPartner' => 'failover.local',
  66. 'multiSubnetFailover' => 'failover.local',
  67. ],
  68. 'sqlsrv:Server=localhost\SQLEXPRESS;Database=cake;MultipleActiveResultSets=false;APP=CakePHP-Testapp;Failover_Partner=failover.local;MultiSubnetFailover=failover.local',
  69. ],
  70. [
  71. [
  72. 'encoding' => '',
  73. ],
  74. 'sqlsrv:Server=localhost\SQLEXPRESS;Database=cake;MultipleActiveResultSets=false',
  75. ],
  76. [
  77. [
  78. 'app' => 'CakePHP-Testapp',
  79. 'encoding' => '',
  80. 'host' => 'localhost\SQLEXPRESS',
  81. 'port' => 9001,
  82. ],
  83. 'sqlsrv:Server=localhost\SQLEXPRESS,9001;Database=cake;MultipleActiveResultSets=false;APP=CakePHP-Testapp',
  84. ],
  85. ];
  86. }
  87. /**
  88. * Test if all options in dns string are set
  89. *
  90. * @dataProvider dnsStringDataProvider
  91. * @param array $constructorArgs
  92. * @param string $dnsString
  93. */
  94. public function testDnsString($constructorArgs, $dnsString): void
  95. {
  96. $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlserver')
  97. ->onlyMethods(['createPdo'])
  98. ->setConstructorArgs([$constructorArgs])
  99. ->getMock();
  100. $driver->method('createPdo')
  101. ->with($this->callback(function ($dns) use ($dnsString) {
  102. $this->assertSame($dns, $dnsString);
  103. return true;
  104. }));
  105. $driver->connect();
  106. }
  107. /**
  108. * Test connecting to Sqlserver with custom configuration
  109. */
  110. public function testConnectionConfigCustom(): void
  111. {
  112. $this->skipIf($this->missingExtension, 'pdo_sqlsrv is not installed.');
  113. $config = [
  114. 'host' => 'foo',
  115. 'username' => 'Administrator',
  116. 'password' => 'blablabla',
  117. 'database' => 'bar',
  118. 'encoding' => 'a-language',
  119. 'flags' => [1 => true, 2 => false],
  120. 'init' => ['Execute this', 'this too'],
  121. 'settings' => ['config1' => 'value1', 'config2' => 'value2'],
  122. ];
  123. $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlserver')
  124. ->onlyMethods(['createPdo', 'getPdo'])
  125. ->setConstructorArgs([$config])
  126. ->getMock();
  127. $dsn = 'sqlsrv:Server=foo;Database=bar;MultipleActiveResultSets=false';
  128. $expected = $config;
  129. $expected['flags'] += [
  130. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  131. PDO::SQLSRV_ATTR_ENCODING => 'a-language',
  132. ];
  133. $expected['attributes'] = [];
  134. $expected['app'] = null;
  135. $expected['connectionPooling'] = null;
  136. $expected['failoverPartner'] = null;
  137. $expected['loginTimeout'] = null;
  138. $expected['multiSubnetFailover'] = null;
  139. $expected['port'] = null;
  140. $expected['log'] = false;
  141. $expected['encrypt'] = null;
  142. $expected['trustServerCertificate'] = null;
  143. $connection = $this->getMockBuilder('PDO')
  144. ->disableOriginalConstructor()
  145. ->onlyMethods(['exec', 'quote'])
  146. ->getMock();
  147. $connection->expects($this->any())
  148. ->method('quote')
  149. ->will($this->onConsecutiveCalls(
  150. $this->returnArgument(0),
  151. $this->returnArgument(0),
  152. $this->returnArgument(0)
  153. ));
  154. $connection->expects($this->exactly(4))
  155. ->method('exec')
  156. ->withConsecutive(
  157. ['Execute this'],
  158. ['this too'],
  159. ['SET config1 value1'],
  160. ['SET config2 value2']
  161. );
  162. $driver->expects($this->once())->method('createPdo')
  163. ->with($dsn, $expected)
  164. ->will($this->returnValue($connection));
  165. $driver->connect();
  166. }
  167. /**
  168. * Test connecting to Sqlserver with persistent set to false
  169. */
  170. public function testConnectionPersistentFalse(): void
  171. {
  172. $this->skipIf($this->missingExtension, 'pdo_sqlsrv is not installed.');
  173. $driver = new Sqlserver([
  174. 'persistent' => false,
  175. 'host' => 'shouldnotexist',
  176. 'username' => 'Administrator',
  177. 'password' => 'blablabla',
  178. 'database' => 'bar',
  179. 'loginTimeout' => 1,
  180. ]);
  181. // This should not throw an InvalidArgumentException because
  182. // persistent is false (the default).
  183. $this->expectException(MissingConnectionException::class);
  184. $driver->connect();
  185. }
  186. /**
  187. * Test if attempting to connect with the driver throws an exception when
  188. * using an invalid config setting.
  189. */
  190. public function testConnectionPersistentTrueException(): void
  191. {
  192. $this->skipIf($this->missingExtension, 'pdo_sqlsrv is not installed.');
  193. $driver = new Sqlserver([
  194. 'persistent' => true,
  195. 'host' => 'shouldnotexist',
  196. 'username' => 'Administrator',
  197. 'password' => 'blablabla',
  198. 'database' => 'bar',
  199. 'loginTimeout' => 1,
  200. ]);
  201. $this->expectException(InvalidArgumentException::class);
  202. $this->expectExceptionMessage('Config setting "persistent" cannot be set to true, as the Sqlserver PDO driver does not support PDO::ATTR_PERSISTENT');
  203. $driver->connect();
  204. }
  205. /**
  206. * Test select with limit only and SQLServer2012+
  207. */
  208. public function testSelectLimitVersion12(): void
  209. {
  210. $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlserver')
  211. ->onlyMethods(['createPdo', 'getPdo', 'version', 'enabled'])
  212. ->setConstructorArgs([[]])
  213. ->getMock();
  214. $driver->method('version')
  215. ->will($this->returnValue('12'));
  216. $driver->method('enabled')
  217. ->will($this->returnValue(true));
  218. $connection = $this->getMockBuilder('Cake\Database\Connection')
  219. ->onlyMethods(['connect'])
  220. ->setConstructorArgs([['driver' => $driver, 'log' => false]])
  221. ->getMock();
  222. $query = new Query($connection);
  223. $query->select(['id', 'title'])
  224. ->from('articles')
  225. ->order(['id'])
  226. ->offset(10);
  227. $this->assertSame('SELECT id, title FROM articles ORDER BY id OFFSET 10 ROWS', $query->sql());
  228. $query = new Query($connection);
  229. $query->select(['id', 'title'])
  230. ->from('articles')
  231. ->order(['id'])
  232. ->limit(10)
  233. ->offset(50);
  234. $this->assertSame('SELECT id, title FROM articles ORDER BY id OFFSET 50 ROWS FETCH FIRST 10 ROWS ONLY', $query->sql());
  235. $query = new Query($connection);
  236. $query->select(['id', 'title'])
  237. ->from('articles')
  238. ->offset(10);
  239. $this->assertSame('SELECT id, title FROM articles ORDER BY (SELECT NULL) OFFSET 10 ROWS', $query->sql());
  240. $query = new Query($connection);
  241. $query->select(['id', 'title'])
  242. ->from('articles')
  243. ->limit(10);
  244. $this->assertSame('SELECT TOP 10 id, title FROM articles', $query->sql());
  245. }
  246. /**
  247. * Test select with limit on lte SQLServer2008
  248. */
  249. public function testSelectLimitOldServer(): void
  250. {
  251. $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlserver')
  252. ->onlyMethods(['createPdo', 'getPdo', 'version', 'enabled'])
  253. ->setConstructorArgs([[]])
  254. ->getMock();
  255. $driver->expects($this->any())
  256. ->method('version')
  257. ->will($this->returnValue('8'));
  258. $driver->method('enabled')
  259. ->will($this->returnValue(true));
  260. $connection = $this->getMockBuilder('Cake\Database\Connection')
  261. ->onlyMethods(['connect'])
  262. ->setConstructorArgs([['driver' => $driver, 'log' => false]])
  263. ->getMock();
  264. $query = new Query($connection);
  265. $query->select(['id', 'title'])
  266. ->from('articles')
  267. ->limit(10);
  268. $expected = 'SELECT TOP 10 id, title FROM articles';
  269. $this->assertSame($expected, $query->sql());
  270. $query = new Query($connection);
  271. $query->select(['id', 'title'])
  272. ->from('articles')
  273. ->offset(10);
  274. $identifier = '_cake_page_rownum_';
  275. if ($connection->getDriver()->isAutoQuotingEnabled()) {
  276. $identifier = $connection->getDriver()->quoteIdentifier($identifier);
  277. }
  278. $expected = 'SELECT * FROM (SELECT id, title, (ROW_NUMBER() OVER (ORDER BY (SELECT NULL))) AS ' . $identifier . ' ' .
  279. 'FROM articles) _cake_paging_ ' .
  280. 'WHERE _cake_paging_._cake_page_rownum_ > 10';
  281. $this->assertSame($expected, $query->sql());
  282. $query = new Query($connection);
  283. $query->select(['id', 'title'])
  284. ->from('articles')
  285. ->order(['id'])
  286. ->offset(10);
  287. $expected = 'SELECT * FROM (SELECT id, title, (ROW_NUMBER() OVER (ORDER BY id)) AS ' . $identifier . ' ' .
  288. 'FROM articles) _cake_paging_ ' .
  289. 'WHERE _cake_paging_._cake_page_rownum_ > 10';
  290. $this->assertSame($expected, $query->sql());
  291. $query = new Query($connection);
  292. $query->select(['id', 'title'])
  293. ->from('articles')
  294. ->order(['id'])
  295. ->where(['title' => 'Something'])
  296. ->limit(10)
  297. ->offset(50);
  298. $expected = 'SELECT * FROM (SELECT id, title, (ROW_NUMBER() OVER (ORDER BY id)) AS ' . $identifier . ' ' .
  299. 'FROM articles WHERE title = :c0) _cake_paging_ ' .
  300. 'WHERE (_cake_paging_._cake_page_rownum_ > 50 AND _cake_paging_._cake_page_rownum_ <= 60)';
  301. $this->assertSame($expected, $query->sql());
  302. $query = new Query($connection);
  303. $subquery = new Query($connection);
  304. $subquery->select(1);
  305. $query
  306. ->select([
  307. 'id',
  308. 'computed' => $subquery,
  309. ])
  310. ->from('articles')
  311. ->order([
  312. 'computed' => 'ASC',
  313. ])
  314. ->offset(10);
  315. $expected =
  316. 'SELECT * FROM (' .
  317. 'SELECT id, (SELECT 1) AS computed, ' .
  318. '(ROW_NUMBER() OVER (ORDER BY (SELECT 1) ASC)) AS _cake_page_rownum_ FROM articles' .
  319. ') _cake_paging_ ' .
  320. 'WHERE _cake_paging_._cake_page_rownum_ > 10';
  321. $this->assertSame($expected, $query->sql());
  322. $subqueryA = new Query($connection);
  323. $subqueryA
  324. ->select('count(*)')
  325. ->from(['a' => 'articles'])
  326. ->where([
  327. 'a.id = articles.id',
  328. 'a.published' => 'Y',
  329. ]);
  330. $subqueryB = new Query($connection);
  331. $subqueryB
  332. ->select('count(*)')
  333. ->from(['b' => 'articles'])
  334. ->where([
  335. 'b.id = articles.id',
  336. 'b.published' => 'N',
  337. ]);
  338. $query = new Query($connection);
  339. $query
  340. ->select([
  341. 'id',
  342. 'computedA' => $subqueryA,
  343. 'computedB' => $subqueryB,
  344. ])
  345. ->from('articles')
  346. ->order([
  347. 'computedA' => 'ASC',
  348. ])
  349. ->offset(10);
  350. $this->assertSame(
  351. 'SELECT * FROM (' .
  352. 'SELECT id, ' .
  353. '(SELECT count(*) FROM articles a WHERE (a.id = articles.id AND a.published = :c0)) AS computedA, ' .
  354. '(SELECT count(*) FROM articles b WHERE (b.id = articles.id AND b.published = :c1)) AS computedB, ' .
  355. '(ROW_NUMBER() OVER (ORDER BY (SELECT count(*) FROM articles a WHERE (a.id = articles.id AND a.published = :c2)) ASC)) AS _cake_page_rownum_ FROM articles' .
  356. ') _cake_paging_ ' .
  357. 'WHERE _cake_paging_._cake_page_rownum_ > 10',
  358. $query->sql()
  359. );
  360. }
  361. /**
  362. * Test that insert queries have results available to them.
  363. */
  364. public function testInsertUsesOutput(): void
  365. {
  366. $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlserver')
  367. ->onlyMethods(['createPdo', 'getPdo', 'enabled'])
  368. ->setConstructorArgs([[]])
  369. ->getMock();
  370. $driver->method('enabled')
  371. ->will($this->returnValue(true));
  372. $connection = $this->getMockBuilder('Cake\Database\Connection')
  373. ->onlyMethods(['connect'])
  374. ->setConstructorArgs([['driver' => $driver, 'log' => false]])
  375. ->getMock();
  376. $query = new Query($connection);
  377. $query->insert(['title'])
  378. ->into('articles')
  379. ->values(['title' => 'A new article']);
  380. $expected = 'INSERT INTO articles (title) OUTPUT INSERTED.* VALUES (:c0)';
  381. $this->assertSame($expected, $query->sql());
  382. }
  383. /**
  384. * Test that having queries replace the aggregated alias field.
  385. */
  386. public function testHavingReplacesAlias(): void
  387. {
  388. $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlserver')
  389. ->onlyMethods(['connect', 'getPdo', 'version', 'enabled'])
  390. ->setConstructorArgs([[]])
  391. ->getMock();
  392. $driver->expects($this->any())
  393. ->method('version')
  394. ->will($this->returnValue('8'));
  395. $driver->method('enabled')
  396. ->will($this->returnValue(true));
  397. $connection = $this->getMockBuilder('\Cake\Database\Connection')
  398. ->onlyMethods(['connect'])
  399. ->setConstructorArgs([['driver' => $driver, 'log' => false]])
  400. ->getMock();
  401. $query = new Query($connection);
  402. $query
  403. ->select([
  404. 'posts.author_id',
  405. 'post_count' => $query->func()->count('posts.id'),
  406. ])
  407. ->group(['posts.author_id'])
  408. ->having([$query->newExpr()->gte('post_count', 2, 'integer')]);
  409. $expected = 'SELECT posts.author_id, (COUNT(posts.id)) AS post_count ' .
  410. 'GROUP BY posts.author_id HAVING COUNT(posts.id) >= :c0';
  411. $this->assertSame($expected, $query->sql());
  412. }
  413. /**
  414. * Test that having queries replaces nothing is no alias is used.
  415. */
  416. public function testHavingWhenNoAliasIsUsed(): void
  417. {
  418. $driver = $this->getMockBuilder('Cake\Database\Driver\Sqlserver')
  419. ->onlyMethods(['connect', 'getPdo', 'version', 'enabled'])
  420. ->setConstructorArgs([[]])
  421. ->getMock();
  422. $driver->expects($this->any())
  423. ->method('version')
  424. ->will($this->returnValue('8'));
  425. $driver->method('enabled')
  426. ->will($this->returnValue(true));
  427. $connection = $this->getMockBuilder('\Cake\Database\Connection')
  428. ->onlyMethods(['connect'])
  429. ->setConstructorArgs([['driver' => $driver, 'log' => false]])
  430. ->getMock();
  431. $query = new Query($connection);
  432. $query
  433. ->select([
  434. 'posts.author_id',
  435. 'post_count' => $query->func()->count('posts.id'),
  436. ])
  437. ->group(['posts.author_id'])
  438. ->having([$query->newExpr()->gte('posts.author_id', 2, 'integer')]);
  439. $expected = 'SELECT posts.author_id, (COUNT(posts.id)) AS post_count ' .
  440. 'GROUP BY posts.author_id HAVING posts.author_id >= :c0';
  441. $this->assertSame($expected, $query->sql());
  442. }
  443. public function testExceedingMaxParameters(): void
  444. {
  445. $connection = ConnectionManager::get('test');
  446. $this->skipIf(!$connection->getDriver() instanceof Sqlserver);
  447. $query = $connection->newQuery()
  448. ->from('articles')
  449. ->whereInList('id', range(0, 2100));
  450. $this->expectException(InvalidArgumentException::class);
  451. $this->expectExceptionMessage(
  452. 'Exceeded maximum number of parameters (2100) for prepared statements in Sql Server'
  453. );
  454. $connection->getDriver()->prepare($query);
  455. }
  456. /**
  457. * Tests driver-specific feature support check.
  458. */
  459. public function testSupports(): void
  460. {
  461. $driver = ConnectionManager::get('test')->getDriver();
  462. $this->skipIf(!$driver instanceof Sqlserver);
  463. $this->assertTrue($driver->supports(Driver::FEATURE_DISABLE_CONSTRAINT_WITHOUT_TRANSACTION));
  464. $this->assertTrue($driver->supports(Driver::FEATURE_TRUNCATE_WITH_CONSTRAINTS));
  465. $this->assertTrue($driver->supports(Driver::FEATURE_CTE));
  466. $this->assertTrue($driver->supports(Driver::FEATURE_SAVEPOINT));
  467. $this->assertTrue($driver->supports(Driver::FEATURE_QUOTE));
  468. $this->assertTrue($driver->supports(Driver::FEATURE_WINDOW));
  469. $this->assertFalse($driver->supports(Driver::FEATURE_JSON));
  470. $this->assertFalse($driver->supports('this-is-fake'));
  471. }
  472. }