ConnectionTest.php 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133
  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;
  17. use Cake\Cache\Engine\NullEngine;
  18. use Cake\Core\App;
  19. use Cake\Database\Connection;
  20. use Cake\Database\Driver;
  21. use Cake\Database\Driver\Mysql;
  22. use Cake\Database\Driver\Sqlserver;
  23. use Cake\Database\Exception\MissingConnectionException;
  24. use Cake\Database\Exception\MissingDriverException;
  25. use Cake\Database\Exception\MissingExtensionException;
  26. use Cake\Database\Exception\NestedTransactionRollbackException;
  27. use Cake\Database\Schema\CachedCollection;
  28. use Cake\Database\StatementInterface;
  29. use Cake\Datasource\ConnectionManager;
  30. use Cake\Log\Log;
  31. use Cake\TestSuite\TestCase;
  32. use DateTime;
  33. use Error;
  34. use Exception;
  35. use InvalidArgumentException;
  36. use PDO;
  37. use ReflectionMethod;
  38. use ReflectionProperty;
  39. use TestApp\Database\Driver\DisabledDriver;
  40. use TestApp\Database\Driver\RetryDriver;
  41. use function Cake\Core\namespaceSplit;
  42. /**
  43. * Tests Connection class
  44. */
  45. class ConnectionTest extends TestCase
  46. {
  47. /**
  48. * @var array<string>
  49. */
  50. protected array $fixtures = ['core.Things'];
  51. /**
  52. * Where the NestedTransactionRollbackException was created.
  53. *
  54. * @var int
  55. */
  56. protected $rollbackSourceLine = -1;
  57. /**
  58. * Internal states of nested transaction.
  59. *
  60. * @var array
  61. */
  62. protected $nestedTransactionStates = [];
  63. /**
  64. * @var bool
  65. */
  66. protected $logState;
  67. /**
  68. * @var \Cake\Database\Connection
  69. */
  70. protected $connection;
  71. /**
  72. * @var \Cake\Database\Log\QueryLogger
  73. */
  74. protected $defaultLogger;
  75. public function setUp(): void
  76. {
  77. parent::setUp();
  78. $this->connection = ConnectionManager::get('test');
  79. static::setAppNamespace();
  80. }
  81. public function tearDown(): void
  82. {
  83. parent::tearDown();
  84. $this->connection->disableSavePoints();
  85. ConnectionManager::drop('test:read');
  86. ConnectionManager::dropAlias('test:read');
  87. Log::reset();
  88. unset($this->connection);
  89. }
  90. /**
  91. * Auxiliary method to build a mock for a driver so it can be injected into
  92. * the connection object
  93. *
  94. * @return \Cake\Database\Driver|\PHPUnit\Framework\MockObject\MockObject
  95. */
  96. public function getMockFormDriver()
  97. {
  98. $driver = $this->getMockBuilder(Driver::class)->getMock();
  99. $driver->expects($this->once())
  100. ->method('enabled')
  101. ->will($this->returnValue(true));
  102. return $driver;
  103. }
  104. /**
  105. * Tests creating a connection using no driver throws an exception
  106. */
  107. public function testNoDriver(): void
  108. {
  109. $this->expectException(MissingDriverException::class);
  110. $this->expectExceptionMessage('Could not find driver `` for connection ``.');
  111. new Connection([]);
  112. }
  113. /**
  114. * Tests creating a connection using an invalid driver throws an exception
  115. */
  116. public function testEmptyDriver(): void
  117. {
  118. $this->expectException(Error::class);
  119. new Connection(['driver' => false]);
  120. }
  121. /**
  122. * Tests creating a connection using an invalid driver throws an exception
  123. */
  124. public function testMissingDriver(): void
  125. {
  126. $this->expectException(MissingDriverException::class);
  127. $this->expectExceptionMessage('Could not find driver `\Foo\InvalidDriver` for connection ``.');
  128. new Connection(['driver' => '\Foo\InvalidDriver']);
  129. }
  130. /**
  131. * Tests trying to use a disabled driver throws an exception
  132. */
  133. public function testDisabledDriver(): void
  134. {
  135. $this->expectException(MissingExtensionException::class);
  136. $this->expectExceptionMessage(
  137. 'Database driver `DriverMock` cannot be used due to a missing PHP extension or unmet dependency. ' .
  138. 'Requested by connection `custom_connection_name`'
  139. );
  140. $mock = $this->getMockBuilder(Mysql::class)
  141. ->onlyMethods(['enabled'])
  142. ->setMockClassName('DriverMock')
  143. ->getMock();
  144. new Connection(['driver' => $mock, 'name' => 'custom_connection_name']);
  145. }
  146. /**
  147. * Tests that the `driver` option supports the short classname/plugin syntax.
  148. */
  149. public function testDriverOptionClassNameSupport(): void
  150. {
  151. $connection = new Connection(['driver' => 'TestDriver']);
  152. $this->assertInstanceOf('TestApp\Database\Driver\TestDriver', $connection->getDriver());
  153. $connection = new Connection(['driver' => 'TestPlugin.TestDriver']);
  154. $this->assertInstanceOf('TestPlugin\Database\Driver\TestDriver', $connection->getDriver());
  155. [, $name] = namespaceSplit(get_class($this->connection->getDriver()));
  156. $connection = new Connection(['driver' => $name]);
  157. $this->assertInstanceOf(get_class($this->connection->getDriver()), $connection->getDriver());
  158. }
  159. /**
  160. * Test providing a unique read config only creates separate drivers.
  161. */
  162. public function testDifferentReadDriver(): void
  163. {
  164. $this->skipIf(!extension_loaded('pdo_sqlite'), 'Skipping as SQLite extension is missing');
  165. $config = ConnectionManager::getConfig('test') + ['read' => ['database' => 'read_test.db']];
  166. $connection = new Connection($config);
  167. $this->assertNotSame($connection->getDriver(Connection::ROLE_READ), $connection->getDriver(Connection::ROLE_WRITE));
  168. $this->assertSame(Connection::ROLE_READ, $connection->getDriver(Connection::ROLE_READ)->getRole());
  169. $this->assertSame(Connection::ROLE_WRITE, $connection->getDriver(Connection::ROLE_WRITE)->getRole());
  170. }
  171. /**
  172. * Test providing a unique write config only creates separate drivers.
  173. */
  174. public function testDifferentWriteDriver(): void
  175. {
  176. $this->skipIf(!extension_loaded('pdo_sqlite'), 'Skipping as SQLite extension is missing');
  177. $config = ConnectionManager::getConfig('test') + ['write' => ['database' => 'read_test.db']];
  178. $connection = new Connection($config);
  179. $this->assertNotSame($connection->getDriver(Connection::ROLE_READ), $connection->getDriver(Connection::ROLE_WRITE));
  180. $this->assertSame(Connection::ROLE_READ, $connection->getDriver(Connection::ROLE_READ)->getRole());
  181. $this->assertSame(Connection::ROLE_WRITE, $connection->getDriver(Connection::ROLE_WRITE)->getRole());
  182. }
  183. /**
  184. * Test providing the same read and write config uses a shared driver.
  185. */
  186. public function testSameReadWriteDriver(): void
  187. {
  188. $this->skipIf(!extension_loaded('pdo_sqlite'), 'Skipping as SQLite extension is missing');
  189. $config = ConnectionManager::getConfig('test') + ['read' => ['database' => 'read_test.db'], 'write' => ['database' => 'read_test.db']];
  190. $connection = new Connection($config);
  191. $this->assertSame($connection->getDriver(Connection::ROLE_READ), $connection->getDriver(Connection::ROLE_WRITE));
  192. $this->assertSame(Connection::ROLE_WRITE, $connection->getDriver(Connection::ROLE_READ)->getRole());
  193. $this->assertSame(Connection::ROLE_WRITE, $connection->getDriver(Connection::ROLE_WRITE)->getRole());
  194. }
  195. public function testDisabledReadWriteDriver(): void
  196. {
  197. $this->skipIf(!extension_loaded('pdo_sqlite'), 'Skipping as SQLite extension is missing');
  198. $config = ['driver' => DisabledDriver::class] + ConnectionManager::getConfig('test');
  199. $this->expectException(MissingExtensionException::class);
  200. new Connection($config);
  201. }
  202. /**
  203. * Tests that connecting with invalid credentials or database name throws an exception
  204. */
  205. public function testWrongCredentials(): void
  206. {
  207. $config = ConnectionManager::getConfig('test');
  208. $this->skipIf(isset($config['url']), 'Datasource has dsn, skipping.');
  209. $connection = new Connection(['database' => '/dev/nonexistent'] + ConnectionManager::getConfig('test'));
  210. $e = null;
  211. try {
  212. $connection->getDriver()->connect();
  213. } catch (MissingConnectionException $e) {
  214. }
  215. $this->assertNotNull($e);
  216. $this->assertStringStartsWith(
  217. sprintf(
  218. 'Connection to %s could not be established:',
  219. App::shortName(get_class($connection->getDriver()), 'Database/Driver')
  220. ),
  221. $e->getMessage()
  222. );
  223. $this->assertInstanceOf('PDOException', $e->getPrevious());
  224. }
  225. public function testConnectRetry(): void
  226. {
  227. $this->skipIf(!ConnectionManager::get('test')->getDriver() instanceof Sqlserver);
  228. $connection = new Connection(['driver' => 'RetryDriver']);
  229. $this->assertInstanceOf(RetryDriver::class, $connection->getDriver());
  230. try {
  231. $connection->execute('SELECT 1');
  232. } catch (MissingConnectionException $e) {
  233. $this->assertSame(4, $connection->getDriver()->getConnectRetries());
  234. }
  235. }
  236. /**
  237. * Tests executing a simple query using bound values
  238. */
  239. public function testExecuteWithArguments(): void
  240. {
  241. $sql = 'SELECT 1 + ?';
  242. $statement = $this->connection->execute($sql, [1], ['integer']);
  243. $result = $statement->fetchAll();
  244. $this->assertCount(1, $result);
  245. $this->assertEquals([2], $result[0]);
  246. $statement->closeCursor();
  247. $sql = 'SELECT 1 + ? + ? AS total';
  248. $statement = $this->connection->execute($sql, [2, 3], ['integer', 'integer']);
  249. $result = $statement->fetchAll('assoc');
  250. $statement->closeCursor();
  251. $this->assertCount(1, $result);
  252. $this->assertEquals(['total' => 6], $result[0]);
  253. $sql = 'SELECT 1 + :one + :two AS total';
  254. $statement = $this->connection->execute($sql, ['one' => 2, 'two' => 3], ['one' => 'integer', 'two' => 'integer']);
  255. $result = $statement->fetchAll('assoc');
  256. $statement->closeCursor();
  257. $this->assertCount(1, $result);
  258. $this->assertEquals(['total' => 6], $result[0]);
  259. }
  260. /**
  261. * Tests executing a query with params and associated types
  262. */
  263. public function testExecuteWithArgumentsAndTypes(): void
  264. {
  265. $sql = "SELECT '2012-01-01' = ?";
  266. $statement = $this->connection->execute($sql, [new DateTime('2012-01-01')], ['date']);
  267. $result = $statement->fetch();
  268. $statement->closeCursor();
  269. $this->assertTrue((bool)$result[0]);
  270. }
  271. /**
  272. * Tests that passing a unknown value to a query throws an exception
  273. */
  274. public function testExecuteWithMissingType(): void
  275. {
  276. $this->expectException(InvalidArgumentException::class);
  277. $sql = 'SELECT ?';
  278. $this->connection->execute($sql, [new DateTime('2012-01-01')], ['bar']);
  279. }
  280. /**
  281. * Tests executing a query with no params also works
  282. */
  283. public function testExecuteWithNoParams(): void
  284. {
  285. $sql = 'SELECT 1';
  286. $statement = $this->connection->execute($sql);
  287. $result = $statement->fetch();
  288. $this->assertCount(1, $result);
  289. $this->assertEquals([1], $result);
  290. $statement->closeCursor();
  291. }
  292. /**
  293. * Tests it is possible to insert data into a table using matching types by key name
  294. */
  295. public function testInsertWithMatchingTypes(): void
  296. {
  297. $data = ['id' => '3', 'title' => 'a title', 'body' => 'a body'];
  298. $result = $this->connection->insert(
  299. 'things',
  300. $data,
  301. ['id' => 'integer', 'title' => 'string', 'body' => 'string']
  302. );
  303. $this->assertInstanceOf(StatementInterface::class, $result);
  304. $result->closeCursor();
  305. $result = $this->connection->execute('SELECT * from things where id = 3');
  306. $rows = $result->fetchAll('assoc');
  307. $this->assertCount(1, $rows);
  308. $result->closeCursor();
  309. $this->assertEquals($data, $rows[0]);
  310. }
  311. /**
  312. * Tests insertQuery
  313. */
  314. public function testInsertQuery(): void
  315. {
  316. $data = ['id' => '3', 'title' => 'a title', 'body' => 'a body'];
  317. $query = $this->connection->insertQuery(
  318. 'things',
  319. $data,
  320. ['id' => 'integer', 'title' => 'string', 'body' => 'string']
  321. );
  322. $result = $query->execute();
  323. $this->assertInstanceOf('Cake\Database\StatementInterface', $result);
  324. $result->closeCursor();
  325. $result = $this->connection->execute('SELECT * from things where id = 3');
  326. $row = $result->fetch('assoc');
  327. $result->closeCursor();
  328. $this->assertEquals($data, $row);
  329. }
  330. /**
  331. * Tests it is possible to insert data into a table using matching types by array position
  332. */
  333. public function testInsertWithPositionalTypes(): void
  334. {
  335. $data = ['id' => '3', 'title' => 'a title', 'body' => 'a body'];
  336. $result = $this->connection->insert(
  337. 'things',
  338. $data,
  339. ['integer', 'string', 'string']
  340. );
  341. $result->closeCursor();
  342. $this->assertInstanceOf(StatementInterface::class, $result);
  343. $result = $this->connection->execute('SELECT * from things where id = 3');
  344. $rows = $result->fetchAll('assoc');
  345. $result->closeCursor();
  346. $this->assertCount(1, $rows);
  347. $this->assertEquals($data, $rows[0]);
  348. }
  349. /**
  350. * Tests an statement class can be reused for multiple executions
  351. */
  352. public function testStatementReusing(): void
  353. {
  354. $total = $this->connection->execute('SELECT COUNT(*) AS total FROM things');
  355. $result = $total->fetch('assoc');
  356. $this->assertEquals(2, $result['total']);
  357. $total->closeCursor();
  358. $total->execute();
  359. $result = $total->fetch('assoc');
  360. $this->assertEquals(2, $result['total']);
  361. $total->closeCursor();
  362. $result = $this->connection->execute('SELECT title, body FROM things');
  363. $row = $result->fetch('assoc');
  364. $this->assertSame('a title', $row['title']);
  365. $this->assertSame('a body', $row['body']);
  366. $row = $result->fetch('assoc');
  367. $result->closeCursor();
  368. $this->assertSame('another title', $row['title']);
  369. $this->assertSame('another body', $row['body']);
  370. $result->execute();
  371. $row = $result->fetch('assoc');
  372. $result->closeCursor();
  373. $this->assertSame('a title', $row['title']);
  374. }
  375. /**
  376. * Tests that it is possible to pass PDO constants to the underlying statement
  377. * object for using alternate fetch types
  378. */
  379. public function testStatementFetchObject(): void
  380. {
  381. $statement = $this->connection->execute('SELECT title, body FROM things');
  382. $row = $statement->fetch(PDO::FETCH_OBJ);
  383. $this->assertSame('a title', $row->title);
  384. $this->assertSame('a body', $row->body);
  385. $statement->closeCursor();
  386. }
  387. /**
  388. * Tests rows can be updated without specifying any conditions nor types
  389. */
  390. public function testUpdateWithoutConditionsNorTypes(): void
  391. {
  392. $title = 'changed the title!';
  393. $body = 'changed the body!';
  394. $this->connection->update('things', ['title' => $title, 'body' => $body]);
  395. $result = $this->connection->execute('SELECT * FROM things WHERE title = ? AND body = ?', [$title, $body]);
  396. $this->assertCount(2, $result->fetchAll());
  397. $result->closeCursor();
  398. }
  399. /**
  400. * Tests it is possible to use key => value conditions for update
  401. */
  402. public function testUpdateWithConditionsNoTypes(): void
  403. {
  404. $title = 'changed the title!';
  405. $body = 'changed the body!';
  406. $this->connection->update('things', ['title' => $title, 'body' => $body], ['id' => 2]);
  407. $result = $this->connection->execute('SELECT * FROM things WHERE title = ? AND body = ?', [$title, $body]);
  408. $this->assertCount(1, $result->fetchAll());
  409. $result->closeCursor();
  410. }
  411. /**
  412. * Tests it is possible to use key => value and string conditions for update
  413. */
  414. public function testUpdateWithConditionsCombinedNoTypes(): void
  415. {
  416. $title = 'changed the title!';
  417. $body = 'changed the body!';
  418. $this->connection->update('things', ['title' => $title, 'body' => $body], ['id' => 2, 'body is not null']);
  419. $result = $this->connection->execute('SELECT * FROM things WHERE title = ? AND body = ?', [$title, $body]);
  420. $this->assertCount(1, $result->fetchAll());
  421. $result->closeCursor();
  422. }
  423. /**
  424. * Tests you can bind types to update values
  425. */
  426. public function testUpdateWithTypes(): void
  427. {
  428. $title = 'changed the title!';
  429. $body = new DateTime('2012-01-01');
  430. $values = compact('title', 'body');
  431. $this->connection->update('things', $values, [], ['body' => 'date']);
  432. $result = $this->connection->execute('SELECT * FROM things WHERE title = :title AND body = :body', $values, ['body' => 'date']);
  433. $rows = $result->fetchAll('assoc');
  434. $this->assertCount(2, $rows);
  435. $this->assertSame('2012-01-01', $rows[0]['body']);
  436. $this->assertSame('2012-01-01', $rows[1]['body']);
  437. $result->closeCursor();
  438. }
  439. /**
  440. * Tests you can bind types to update values
  441. */
  442. public function testUpdateWithConditionsAndTypes(): void
  443. {
  444. $title = 'changed the title!';
  445. $body = new DateTime('2012-01-01');
  446. $values = compact('title', 'body');
  447. $this->connection->update('things', $values, ['id' => '1'], ['body' => 'date', 'id' => 'integer']);
  448. $result = $this->connection->execute('SELECT * FROM things WHERE title = :title AND body = :body', $values, ['body' => 'date']);
  449. $rows = $result->fetchAll('assoc');
  450. $this->assertCount(1, $rows);
  451. $this->assertSame('2012-01-01', $rows[0]['body']);
  452. $result->closeCursor();
  453. }
  454. /**
  455. * Tests you can bind types to update values
  456. */
  457. public function testUpdateQueryWithConditionsAndTypes(): void
  458. {
  459. $title = 'changed the title!';
  460. $body = new DateTime('2012-01-01');
  461. $values = compact('title', 'body');
  462. $query = $this->connection->updateQuery('things', $values, ['id' => '1'], ['body' => 'date', 'id' => 'integer']);
  463. $query->execute()->closeCursor();
  464. $result = $this->connection->execute('SELECT * FROM things WHERE title = :title AND body = :body', $values, ['body' => 'date']);
  465. $row = $result->fetch('assoc');
  466. $this->assertSame('2012-01-01', $row['body']);
  467. $result->closeCursor();
  468. }
  469. /**
  470. * Tests delete from table with no conditions
  471. */
  472. public function testDeleteNoConditions(): void
  473. {
  474. $this->connection->delete('things');
  475. $result = $this->connection->execute('SELECT * FROM things');
  476. $this->assertCount(0, $result->fetchAll());
  477. $result->closeCursor();
  478. }
  479. /**
  480. * Tests delete from table with conditions
  481. */
  482. public function testDeleteWithConditions(): void
  483. {
  484. $this->connection->delete('things', ['id' => '1'], ['id' => 'integer']);
  485. $result = $this->connection->execute('SELECT * FROM things');
  486. $this->assertCount(1, $result->fetchAll());
  487. $result->closeCursor();
  488. $this->connection->delete('things', ['id' => '2'], ['id' => 'integer']);
  489. $result = $this->connection->execute('SELECT * FROM things');
  490. $this->assertCount(0, $result->fetchAll());
  491. $result->closeCursor();
  492. }
  493. /**
  494. * Tests delete from table with conditions
  495. */
  496. public function testDeleteQuery(): void
  497. {
  498. $query = $this->connection->deleteQuery('things', ['id' => '1'], ['id' => 'integer']);
  499. $query->execute()->closeCursor();
  500. $result = $this->connection->execute('SELECT * FROM things');
  501. $result->closeCursor();
  502. $query = $this->connection->deleteQuery('things')->where(['id' => 2], ['id' => 'integer']);
  503. $query->execute()->closeCursor();
  504. $result = $this->connection->execute('SELECT * FROM things');
  505. $this->assertCount(0, $result->fetchAll());
  506. $result->closeCursor();
  507. }
  508. /**
  509. * Test basic selectQuery behavior
  510. */
  511. public function testSelectQuery(): void
  512. {
  513. $query = $this->connection->selectQuery(['*'], 'things');
  514. $statement = $query->execute();
  515. $row = $statement->fetchAssoc();
  516. $statement->closeCursor();
  517. $this->assertArrayHasKey('title', $row);
  518. $this->assertArrayHasKey('body', $row);
  519. }
  520. /**
  521. * Tests that it is possible to use simple database transactions
  522. */
  523. public function testSimpleTransactions(): void
  524. {
  525. $this->connection->begin();
  526. $this->assertTrue($this->connection->getDriver()->inTransaction());
  527. $this->connection->delete('things', ['id' => 1]);
  528. $this->connection->rollback();
  529. $this->assertFalse($this->connection->getDriver()->inTransaction());
  530. $result = $this->connection->execute('SELECT * FROM things');
  531. $this->assertCount(2, $result->fetchAll());
  532. $result->closeCursor();
  533. $this->connection->begin();
  534. $this->assertTrue($this->connection->getDriver()->inTransaction());
  535. $this->connection->delete('things', ['id' => 1]);
  536. $this->connection->commit();
  537. $this->assertFalse($this->connection->getDriver()->inTransaction());
  538. $result = $this->connection->execute('SELECT * FROM things');
  539. $this->assertCount(1, $result->fetchAll());
  540. }
  541. /**
  542. * Tests that the destructor of Connection generates a warning log
  543. * when transaction is not closed
  544. */
  545. public function testDestructorWithUncommittedTransaction(): void
  546. {
  547. $driver = $this->getMockFormDriver();
  548. $connection = new Connection(['driver' => $driver]);
  549. $connection->begin();
  550. $this->assertTrue($connection->inTransaction());
  551. $logger = $this->createMock('Psr\Log\AbstractLogger');
  552. $logger->expects($this->once())
  553. ->method('log')
  554. ->with('warning', $this->stringContains('The connection is going to be closed'));
  555. Log::setConfig('error', $logger);
  556. // Destroy the connection
  557. unset($connection);
  558. }
  559. /**
  560. * Tests that it is possible to use virtualized nested transaction
  561. * with early rollback algorithm
  562. */
  563. public function testVirtualNestedTransaction(): void
  564. {
  565. //starting 3 virtual transaction
  566. $this->connection->begin();
  567. $this->connection->begin();
  568. $this->connection->begin();
  569. $this->assertTrue($this->connection->getDriver()->inTransaction());
  570. $this->connection->delete('things', ['id' => 1]);
  571. $result = $this->connection->execute('SELECT * FROM things');
  572. $this->assertCount(1, $result->fetchAll());
  573. $this->connection->commit();
  574. $this->assertTrue($this->connection->getDriver()->inTransaction());
  575. $this->connection->rollback();
  576. $this->assertFalse($this->connection->getDriver()->inTransaction());
  577. $result = $this->connection->execute('SELECT * FROM things');
  578. $this->assertCount(2, $result->fetchAll());
  579. }
  580. /**
  581. * Tests that it is possible to use virtualized nested transaction
  582. * with early rollback algorithm
  583. */
  584. public function testVirtualNestedTransaction2(): void
  585. {
  586. //starting 3 virtual transaction
  587. $this->connection->begin();
  588. $this->connection->begin();
  589. $this->connection->begin();
  590. $this->connection->delete('things', ['id' => 1]);
  591. $result = $this->connection->execute('SELECT * FROM things');
  592. $this->assertCount(1, $result->fetchAll());
  593. $this->connection->rollback();
  594. $result = $this->connection->execute('SELECT * FROM things');
  595. $this->assertCount(2, $result->fetchAll());
  596. }
  597. /**
  598. * Tests that it is possible to use virtualized nested transaction
  599. * with early rollback algorithm
  600. */
  601. public function testVirtualNestedTransaction3(): void
  602. {
  603. //starting 3 virtual transaction
  604. $this->connection->begin();
  605. $this->connection->begin();
  606. $this->connection->begin();
  607. $this->connection->delete('things', ['id' => 1]);
  608. $result = $this->connection->execute('SELECT * FROM things');
  609. $this->assertCount(1, $result->fetchAll());
  610. $this->connection->commit();
  611. $this->connection->commit();
  612. $this->connection->commit();
  613. $result = $this->connection->execute('SELECT * FROM things');
  614. $this->assertCount(1, $result->fetchAll());
  615. }
  616. /**
  617. * Tests that it is possible to real use nested transactions
  618. */
  619. public function testSavePoints(): void
  620. {
  621. $this->connection->enableSavePoints(true);
  622. $this->skipIf(!$this->connection->isSavePointsEnabled(), 'Database driver doesn\'t support save points');
  623. $this->connection->begin();
  624. $this->connection->delete('things', ['id' => 1]);
  625. $result = $this->connection->execute('SELECT * FROM things');
  626. $this->assertCount(1, $result->fetchAll());
  627. $this->connection->begin();
  628. $this->connection->delete('things', ['id' => 2]);
  629. $result = $this->connection->execute('SELECT * FROM things');
  630. $this->assertCount(0, $result->fetchAll());
  631. $this->connection->rollback();
  632. $result = $this->connection->execute('SELECT * FROM things');
  633. $this->assertCount(1, $result->fetchAll());
  634. $this->connection->rollback();
  635. $result = $this->connection->execute('SELECT * FROM things');
  636. $this->assertCount(2, $result->fetchAll());
  637. }
  638. /**
  639. * Tests that it is possible to real use nested transactions
  640. */
  641. public function testSavePoints2(): void
  642. {
  643. $this->connection->enableSavePoints(true);
  644. $this->skipIf(!$this->connection->isSavePointsEnabled(), 'Database driver doesn\'t support save points');
  645. $this->connection->begin();
  646. $this->connection->delete('things', ['id' => 1]);
  647. $result = $this->connection->execute('SELECT * FROM things');
  648. $this->assertCount(1, $result->fetchAll());
  649. $this->connection->begin();
  650. $this->connection->delete('things', ['id' => 2]);
  651. $result = $this->connection->execute('SELECT * FROM things');
  652. $this->assertCount(0, $result->fetchAll());
  653. $this->connection->rollback();
  654. $result = $this->connection->execute('SELECT * FROM things');
  655. $this->assertCount(1, $result->fetchAll());
  656. $this->connection->commit();
  657. $result = $this->connection->execute('SELECT * FROM things');
  658. $this->assertCount(1, $result->fetchAll());
  659. }
  660. /**
  661. * Tests inTransaction()
  662. */
  663. public function testInTransaction(): void
  664. {
  665. $this->connection->begin();
  666. $this->assertTrue($this->connection->inTransaction());
  667. $this->connection->begin();
  668. $this->assertTrue($this->connection->inTransaction());
  669. $this->connection->commit();
  670. $this->assertTrue($this->connection->inTransaction());
  671. $this->connection->commit();
  672. $this->assertFalse($this->connection->inTransaction());
  673. $this->connection->begin();
  674. $this->assertTrue($this->connection->inTransaction());
  675. $this->connection->begin();
  676. $this->connection->rollback();
  677. $this->assertFalse($this->connection->inTransaction());
  678. }
  679. /**
  680. * Tests inTransaction() with save points
  681. */
  682. public function testInTransactionWithSavePoints(): void
  683. {
  684. $this->skipIf(
  685. $this->connection->getDriver() instanceof Sqlserver,
  686. 'SQLServer fails when this test is included.'
  687. );
  688. $this->connection->enableSavePoints(true);
  689. $this->skipIf(!$this->connection->isSavePointsEnabled(), 'Database driver doesn\'t support save points');
  690. $this->connection->begin();
  691. $this->assertTrue($this->connection->inTransaction());
  692. $this->connection->begin();
  693. $this->assertTrue($this->connection->inTransaction());
  694. $this->connection->commit();
  695. $this->assertTrue($this->connection->inTransaction());
  696. $this->connection->commit();
  697. $this->assertFalse($this->connection->inTransaction());
  698. $this->connection->begin();
  699. $this->assertTrue($this->connection->inTransaction());
  700. $this->connection->begin();
  701. $this->connection->rollback();
  702. $this->assertTrue($this->connection->inTransaction());
  703. $this->connection->rollback();
  704. $this->assertFalse($this->connection->inTransaction());
  705. }
  706. /**
  707. * Tests setting and getting the cacher object
  708. */
  709. public function testGetAndSetCacher(): void
  710. {
  711. $cacher = new NullEngine();
  712. $this->connection->setCacher($cacher);
  713. $this->assertSame($cacher, $this->connection->getCacher());
  714. }
  715. /**
  716. * Tests that the transactional method will start and commit a transaction
  717. * around some arbitrary function passed as argument
  718. */
  719. public function testTransactionalSuccess(): void
  720. {
  721. $driver = $this->getMockFormDriver();
  722. $connection = $this->getMockBuilder(Connection::class)
  723. ->onlyMethods(['commit', 'begin'])
  724. ->setConstructorArgs([['driver' => $driver]])
  725. ->getMock();
  726. $connection->expects($this->once())->method('begin');
  727. $connection->expects($this->once())->method('commit');
  728. $result = $connection->transactional(function ($conn) use ($connection) {
  729. $this->assertSame($connection, $conn);
  730. return 'thing';
  731. });
  732. $this->assertSame('thing', $result);
  733. }
  734. /**
  735. * Tests that the transactional method will rollback the transaction if false
  736. * is returned from the callback
  737. */
  738. public function testTransactionalFail(): void
  739. {
  740. $driver = $this->getMockFormDriver();
  741. $connection = $this->getMockBuilder(Connection::class)
  742. ->onlyMethods(['commit', 'begin', 'rollback'])
  743. ->setConstructorArgs([['driver' => $driver]])
  744. ->getMock();
  745. $connection->expects($this->once())->method('begin');
  746. $connection->expects($this->once())->method('rollback');
  747. $connection->expects($this->never())->method('commit');
  748. $result = $connection->transactional(function ($conn) use ($connection) {
  749. $this->assertSame($connection, $conn);
  750. return false;
  751. });
  752. $this->assertFalse($result);
  753. }
  754. /**
  755. * Tests that the transactional method will rollback the transaction
  756. * and throw the same exception if the callback raises one
  757. *
  758. * @throws \InvalidArgumentException
  759. */
  760. public function testTransactionalWithException(): void
  761. {
  762. $this->expectException(InvalidArgumentException::class);
  763. $driver = $this->getMockFormDriver();
  764. $connection = $this->getMockBuilder(Connection::class)
  765. ->onlyMethods(['commit', 'begin', 'rollback'])
  766. ->setConstructorArgs([['driver' => $driver]])
  767. ->getMock();
  768. $connection->expects($this->once())->method('begin');
  769. $connection->expects($this->once())->method('rollback');
  770. $connection->expects($this->never())->method('commit');
  771. $connection->transactional(function ($conn) use ($connection): void {
  772. $this->assertSame($connection, $conn);
  773. throw new InvalidArgumentException();
  774. });
  775. }
  776. /**
  777. * Tests it is possible to set a schema collection object
  778. */
  779. public function testSetSchemaCollection(): void
  780. {
  781. $driver = $this->getMockFormDriver();
  782. $connection = new Connection(['driver' => $driver]);
  783. $schema = $connection->getSchemaCollection();
  784. $this->assertInstanceOf('Cake\Database\Schema\Collection', $schema);
  785. $schema = $this->getMockBuilder('Cake\Database\Schema\Collection')
  786. ->setConstructorArgs([$connection])
  787. ->getMock();
  788. $connection->setSchemaCollection($schema);
  789. $this->assertSame($schema, $connection->getSchemaCollection());
  790. }
  791. /**
  792. * Test CachedCollection creation with default and custom cache key prefix.
  793. */
  794. public function testGetCachedCollection(): void
  795. {
  796. $driver = $this->getMockFormDriver();
  797. $connection = new Connection([
  798. 'driver' => $driver,
  799. 'name' => 'default',
  800. 'cacheMetadata' => true,
  801. ]);
  802. $schema = $connection->getSchemaCollection();
  803. $this->assertInstanceOf(CachedCollection::class, $schema);
  804. $this->assertSame('default_key', $schema->cacheKey('key'));
  805. $driver = $this->getMockFormDriver();
  806. $connection = new Connection([
  807. 'driver' => $driver,
  808. 'name' => 'default',
  809. 'cacheMetadata' => true,
  810. 'cacheKeyPrefix' => 'foo',
  811. ]);
  812. $schema = $connection->getSchemaCollection();
  813. $this->assertInstanceOf(CachedCollection::class, $schema);
  814. $this->assertSame('foo_key', $schema->cacheKey('key'));
  815. }
  816. /**
  817. * Tests that allowed nesting of commit/rollback operations doesn't
  818. * throw any exceptions.
  819. */
  820. public function testNestedTransactionRollbackExceptionNotThrown(): void
  821. {
  822. $this->connection->transactional(function () {
  823. $this->connection->transactional(function () {
  824. return true;
  825. });
  826. return true;
  827. });
  828. $this->assertFalse($this->connection->inTransaction());
  829. $this->connection->transactional(function () {
  830. $this->connection->transactional(function () {
  831. return true;
  832. });
  833. return false;
  834. });
  835. $this->assertFalse($this->connection->inTransaction());
  836. $this->connection->transactional(function () {
  837. $this->connection->transactional(function () {
  838. return false;
  839. });
  840. return false;
  841. });
  842. $this->assertFalse($this->connection->inTransaction());
  843. }
  844. /**
  845. * Tests that not allowed nesting of commit/rollback operations throws
  846. * a NestedTransactionRollbackException.
  847. */
  848. public function testNestedTransactionRollbackExceptionThrown(): void
  849. {
  850. $this->rollbackSourceLine = -1;
  851. $e = null;
  852. try {
  853. $this->connection->transactional(function () {
  854. $this->connection->transactional(function () {
  855. return false;
  856. });
  857. $this->rollbackSourceLine = __LINE__ - 1;
  858. if (PHP_VERSION_ID >= 80200) {
  859. $this->rollbackSourceLine -= 2;
  860. }
  861. return true;
  862. });
  863. $this->fail('NestedTransactionRollbackException should be thrown');
  864. } catch (NestedTransactionRollbackException $e) {
  865. }
  866. $trace = $e->getTrace();
  867. $this->assertEquals(__FILE__, $trace[1]['file']);
  868. $this->assertEquals($this->rollbackSourceLine, $trace[1]['line']);
  869. }
  870. /**
  871. * Tests more detail about that not allowed nesting of rollback/commit
  872. * operations throws a NestedTransactionRollbackException.
  873. */
  874. public function testNestedTransactionStates(): void
  875. {
  876. $this->rollbackSourceLine = -1;
  877. $this->nestedTransactionStates = [];
  878. $e = null;
  879. try {
  880. $this->connection->transactional(function () {
  881. $this->pushNestedTransactionState();
  882. $this->connection->transactional(function () {
  883. return true;
  884. });
  885. $this->connection->transactional(function () {
  886. $this->pushNestedTransactionState();
  887. $this->connection->transactional(function () {
  888. return false;
  889. });
  890. $this->rollbackSourceLine = __LINE__ - 1;
  891. if (PHP_VERSION_ID >= 80200) {
  892. $this->rollbackSourceLine -= 2;
  893. }
  894. $this->pushNestedTransactionState();
  895. return true;
  896. });
  897. $this->connection->transactional(function () {
  898. return false;
  899. });
  900. $this->pushNestedTransactionState();
  901. return true;
  902. });
  903. $this->fail('NestedTransactionRollbackException should be thrown');
  904. } catch (NestedTransactionRollbackException $e) {
  905. }
  906. $this->pushNestedTransactionState();
  907. $this->assertSame([false, false, true, true, false], $this->nestedTransactionStates);
  908. $this->assertFalse($this->connection->inTransaction());
  909. $trace = $e->getTrace();
  910. $this->assertEquals(__FILE__, $trace[1]['file']);
  911. $this->assertEquals($this->rollbackSourceLine, $trace[1]['line']);
  912. }
  913. /**
  914. * Helper method to trace nested transaction states.
  915. */
  916. public function pushNestedTransactionState(): void
  917. {
  918. $method = new ReflectionMethod($this->connection, 'wasNestedTransactionRolledback');
  919. $this->nestedTransactionStates[] = $method->invoke($this->connection);
  920. }
  921. /**
  922. * Tests that the connection is restablished whenever it is interrupted
  923. * after having used the connection at least once.
  924. */
  925. public function testAutomaticReconnect2(): void
  926. {
  927. $conn = clone $this->connection;
  928. $statement = $conn->execute('SELECT 1');
  929. $statement->execute();
  930. $statement->closeCursor();
  931. $newDriver = $this->getMockBuilder(Driver::class)->getMock();
  932. $prop = new ReflectionProperty($conn, 'readDriver');
  933. $prop->setAccessible(true);
  934. $prop->setValue($conn, $newDriver);
  935. $prop = new ReflectionProperty($conn, 'writeDriver');
  936. $prop->setAccessible(true);
  937. $prop->setValue($conn, $newDriver);
  938. $newDriver->expects($this->exactly(2))
  939. ->method('execute')
  940. ->will($this->onConsecutiveCalls(
  941. $this->throwException(new Exception('server gone away')),
  942. $this->returnValue($statement)
  943. ));
  944. $res = $conn->execute('SELECT 1');
  945. $this->assertInstanceOf(StatementInterface::class, $res);
  946. }
  947. /**
  948. * Tests that the connection is not restablished whenever it is interrupted
  949. * inside a transaction.
  950. */
  951. public function testNoAutomaticReconnect(): void
  952. {
  953. $conn = clone $this->connection;
  954. $statement = $conn->execute('SELECT 1');
  955. $statement->execute();
  956. $statement->closeCursor();
  957. $conn->begin();
  958. $newDriver = $this->getMockBuilder(Driver::class)->getMock();
  959. $prop = new ReflectionProperty($conn, 'readDriver');
  960. $prop->setAccessible(true);
  961. $prop->setValue($conn, $newDriver);
  962. $prop = new ReflectionProperty($conn, 'writeDriver');
  963. $prop->setAccessible(true);
  964. $oldDriver = $prop->getValue($conn);
  965. $prop->setValue($conn, $newDriver);
  966. $newDriver->expects($this->once())
  967. ->method('execute')
  968. ->will($this->throwException(new Exception('server gone away')));
  969. try {
  970. $conn->execute('SELECT 1');
  971. } catch (Exception $e) {
  972. $this->assertInstanceOf(Exception::class, $e);
  973. $prop->setValue($conn, $oldDriver);
  974. $conn->rollback();
  975. }
  976. }
  977. }