ConnectionTest.php 42 KB

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