ConnectionTest.php 39 KB

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