ConnectionTest.php 48 KB

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