ConnectionTest.php 33 KB

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