ConnectionTest.php 43 KB

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