ConnectionTest.php 43 KB

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