ConnectionTest.php 44 KB

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