DatabaseConnectionTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <?php
  2. namespace Illuminate\Tests\Database;
  3. use DateTime;
  4. use ErrorException;
  5. use Exception;
  6. use Illuminate\Contracts\Events\Dispatcher;
  7. use Illuminate\Database\Connection;
  8. use Illuminate\Database\Events\QueryExecuted;
  9. use Illuminate\Database\Events\TransactionBeginning;
  10. use Illuminate\Database\Events\TransactionCommitted;
  11. use Illuminate\Database\Events\TransactionRolledBack;
  12. use Illuminate\Database\Query\Builder as BaseBuilder;
  13. use Illuminate\Database\Query\Grammars\Grammar;
  14. use Illuminate\Database\Query\Processors\Processor;
  15. use Illuminate\Database\QueryException;
  16. use Illuminate\Database\Schema\Builder;
  17. use Mockery as m;
  18. use PDO;
  19. use PDOException;
  20. use PDOStatement;
  21. use PHPUnit\Framework\TestCase;
  22. use ReflectionClass;
  23. use stdClass;
  24. class DatabaseConnectionTest extends TestCase
  25. {
  26. protected function tearDown(): void
  27. {
  28. m::close();
  29. }
  30. public function testSettingDefaultCallsGetDefaultGrammar()
  31. {
  32. $connection = $this->getMockConnection();
  33. $mock = m::mock(stdClass::class);
  34. $connection->expects($this->once())->method('getDefaultQueryGrammar')->willReturn($mock);
  35. $connection->useDefaultQueryGrammar();
  36. $this->assertEquals($mock, $connection->getQueryGrammar());
  37. }
  38. public function testSettingDefaultCallsGetDefaultPostProcessor()
  39. {
  40. $connection = $this->getMockConnection();
  41. $mock = m::mock(stdClass::class);
  42. $connection->expects($this->once())->method('getDefaultPostProcessor')->willReturn($mock);
  43. $connection->useDefaultPostProcessor();
  44. $this->assertEquals($mock, $connection->getPostProcessor());
  45. }
  46. public function testSelectOneCallsSelectAndReturnsSingleResult()
  47. {
  48. $connection = $this->getMockConnection(['select']);
  49. $connection->expects($this->once())->method('select')->with('foo', ['bar' => 'baz'])->willReturn(['foo']);
  50. $this->assertSame('foo', $connection->selectOne('foo', ['bar' => 'baz']));
  51. }
  52. public function testSelectProperlyCallsPDO()
  53. {
  54. $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['prepare'])->getMock();
  55. $writePdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['prepare'])->getMock();
  56. $writePdo->expects($this->never())->method('prepare');
  57. $statement = $this->getMockBuilder('PDOStatement')
  58. ->onlyMethods(['setFetchMode', 'execute', 'fetchAll', 'bindValue'])
  59. ->getMock();
  60. $statement->expects($this->once())->method('setFetchMode');
  61. $statement->expects($this->once())->method('bindValue')->with('foo', 'bar', 2);
  62. $statement->expects($this->once())->method('execute');
  63. $statement->expects($this->once())->method('fetchAll')->willReturn(['boom']);
  64. $pdo->expects($this->once())->method('prepare')->with('foo')->willReturn($statement);
  65. $mock = $this->getMockConnection(['prepareBindings'], $writePdo);
  66. $mock->setReadPdo($pdo);
  67. $mock->expects($this->once())->method('prepareBindings')->with($this->equalTo(['foo' => 'bar']))->willReturn(['foo' => 'bar']);
  68. $results = $mock->select('foo', ['foo' => 'bar']);
  69. $this->assertEquals(['boom'], $results);
  70. $log = $mock->getQueryLog();
  71. $this->assertSame('foo', $log[0]['query']);
  72. $this->assertEquals(['foo' => 'bar'], $log[0]['bindings']);
  73. $this->assertIsNumeric($log[0]['time']);
  74. }
  75. public function testInsertCallsTheStatementMethod()
  76. {
  77. $connection = $this->getMockConnection(['statement']);
  78. $connection->expects($this->once())->method('statement')->with($this->equalTo('foo'), $this->equalTo(['bar']))->willReturn('baz');
  79. $results = $connection->insert('foo', ['bar']);
  80. $this->assertSame('baz', $results);
  81. }
  82. public function testUpdateCallsTheAffectingStatementMethod()
  83. {
  84. $connection = $this->getMockConnection(['affectingStatement']);
  85. $connection->expects($this->once())->method('affectingStatement')->with($this->equalTo('foo'), $this->equalTo(['bar']))->willReturn('baz');
  86. $results = $connection->update('foo', ['bar']);
  87. $this->assertSame('baz', $results);
  88. }
  89. public function testDeleteCallsTheAffectingStatementMethod()
  90. {
  91. $connection = $this->getMockConnection(['affectingStatement']);
  92. $connection->expects($this->once())->method('affectingStatement')->with($this->equalTo('foo'), $this->equalTo(['bar']))->willReturn(true);
  93. $results = $connection->delete('foo', ['bar']);
  94. $this->assertTrue($results);
  95. }
  96. public function testStatementProperlyCallsPDO()
  97. {
  98. $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['prepare'])->getMock();
  99. $statement = $this->getMockBuilder('PDOStatement')->onlyMethods(['execute', 'bindValue'])->getMock();
  100. $statement->expects($this->once())->method('bindValue')->with(1, 'bar', 2);
  101. $statement->expects($this->once())->method('execute')->willReturn(true);
  102. $pdo->expects($this->once())->method('prepare')->with($this->equalTo('foo'))->willReturn($statement);
  103. $mock = $this->getMockConnection(['prepareBindings'], $pdo);
  104. $mock->expects($this->once())->method('prepareBindings')->with($this->equalTo(['bar']))->willReturn(['bar']);
  105. $results = $mock->statement('foo', ['bar']);
  106. $this->assertTrue($results);
  107. $log = $mock->getQueryLog();
  108. $this->assertSame('foo', $log[0]['query']);
  109. $this->assertEquals(['bar'], $log[0]['bindings']);
  110. $this->assertIsNumeric($log[0]['time']);
  111. }
  112. public function testAffectingStatementProperlyCallsPDO()
  113. {
  114. $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['prepare'])->getMock();
  115. $statement = $this->getMockBuilder('PDOStatement')->onlyMethods(['execute', 'rowCount', 'bindValue'])->getMock();
  116. $statement->expects($this->once())->method('bindValue')->with('foo', 'bar', 2);
  117. $statement->expects($this->once())->method('execute');
  118. $statement->expects($this->once())->method('rowCount')->willReturn(42);
  119. $pdo->expects($this->once())->method('prepare')->with('foo')->willReturn($statement);
  120. $mock = $this->getMockConnection(['prepareBindings'], $pdo);
  121. $mock->expects($this->once())->method('prepareBindings')->with($this->equalTo(['foo' => 'bar']))->willReturn(['foo' => 'bar']);
  122. $results = $mock->update('foo', ['foo' => 'bar']);
  123. $this->assertSame(42, $results);
  124. $log = $mock->getQueryLog();
  125. $this->assertSame('foo', $log[0]['query']);
  126. $this->assertEquals(['foo' => 'bar'], $log[0]['bindings']);
  127. $this->assertIsNumeric($log[0]['time']);
  128. }
  129. public function testTransactionLevelNotIncrementedOnTransactionException()
  130. {
  131. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  132. $pdo->expects($this->once())->method('beginTransaction')->will($this->throwException(new Exception));
  133. $connection = $this->getMockConnection([], $pdo);
  134. try {
  135. $connection->beginTransaction();
  136. } catch (Exception $e) {
  137. $this->assertEquals(0, $connection->transactionLevel());
  138. }
  139. }
  140. public function testBeginTransactionMethodRetriesOnFailure()
  141. {
  142. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  143. $pdo->method('beginTransaction')
  144. ->willReturnOnConsecutiveCalls($this->throwException(new ErrorException('server has gone away')), true);
  145. $connection = $this->getMockConnection(['reconnect'], $pdo);
  146. $connection->expects($this->once())->method('reconnect');
  147. $connection->beginTransaction();
  148. $this->assertEquals(1, $connection->transactionLevel());
  149. }
  150. public function testBeginTransactionMethodReconnectsMissingConnection()
  151. {
  152. $connection = $this->getMockConnection();
  153. $connection->setReconnector(function ($connection) {
  154. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  155. $connection->setPdo($pdo);
  156. });
  157. $connection->disconnect();
  158. $connection->beginTransaction();
  159. $this->assertEquals(1, $connection->transactionLevel());
  160. }
  161. public function testBeginTransactionMethodNeverRetriesIfWithinTransaction()
  162. {
  163. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  164. $pdo->expects($this->once())->method('beginTransaction');
  165. $pdo->expects($this->once())->method('exec')->will($this->throwException(new Exception));
  166. $connection = $this->getMockConnection(['reconnect'], $pdo);
  167. $queryGrammar = $this->createMock(Grammar::class);
  168. $queryGrammar->expects($this->once())->method('compileSavepoint')->willReturn('trans1');
  169. $queryGrammar->expects($this->once())->method('supportsSavepoints')->willReturn(true);
  170. $connection->setQueryGrammar($queryGrammar);
  171. $connection->expects($this->never())->method('reconnect');
  172. $connection->beginTransaction();
  173. $this->assertEquals(1, $connection->transactionLevel());
  174. try {
  175. $connection->beginTransaction();
  176. } catch (Exception $e) {
  177. $this->assertEquals(1, $connection->transactionLevel());
  178. }
  179. }
  180. public function testSwapPDOWithOpenTransactionResetsTransactionLevel()
  181. {
  182. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  183. $pdo->expects($this->once())->method('beginTransaction')->willReturn(true);
  184. $connection = $this->getMockConnection([], $pdo);
  185. $connection->beginTransaction();
  186. $connection->disconnect();
  187. $this->assertEquals(0, $connection->transactionLevel());
  188. }
  189. public function testBeganTransactionFiresEventsIfSet()
  190. {
  191. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  192. $connection = $this->getMockConnection(['getName'], $pdo);
  193. $connection->expects($this->any())->method('getName')->willReturn('name');
  194. $connection->setEventDispatcher($events = m::mock(Dispatcher::class));
  195. $events->shouldReceive('dispatch')->once()->with(m::type(TransactionBeginning::class));
  196. $connection->beginTransaction();
  197. }
  198. public function testCommittedFiresEventsIfSet()
  199. {
  200. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  201. $connection = $this->getMockConnection(['getName'], $pdo);
  202. $connection->expects($this->any())->method('getName')->willReturn('name');
  203. $connection->setEventDispatcher($events = m::mock(Dispatcher::class));
  204. $events->shouldReceive('dispatch')->once()->with(m::type(TransactionCommitted::class));
  205. $connection->commit();
  206. }
  207. public function testRollBackedFiresEventsIfSet()
  208. {
  209. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  210. $connection = $this->getMockConnection(['getName'], $pdo);
  211. $connection->expects($this->any())->method('getName')->willReturn('name');
  212. $connection->beginTransaction();
  213. $connection->setEventDispatcher($events = m::mock(Dispatcher::class));
  214. $events->shouldReceive('dispatch')->once()->with(m::type(TransactionRolledBack::class));
  215. $connection->rollBack();
  216. }
  217. public function testRedundantRollBackFiresNoEvent()
  218. {
  219. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  220. $connection = $this->getMockConnection(['getName'], $pdo);
  221. $connection->expects($this->any())->method('getName')->willReturn('name');
  222. $connection->setEventDispatcher($events = m::mock(Dispatcher::class));
  223. $events->shouldNotReceive('dispatch');
  224. $connection->rollBack();
  225. }
  226. public function testTransactionMethodRunsSuccessfully()
  227. {
  228. $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction', 'commit'])->getMock();
  229. $mock = $this->getMockConnection([], $pdo);
  230. $pdo->expects($this->once())->method('beginTransaction');
  231. $pdo->expects($this->once())->method('commit');
  232. $result = $mock->transaction(function ($db) {
  233. return $db;
  234. });
  235. $this->assertEquals($mock, $result);
  236. }
  237. public function testTransactionRetriesOnSerializationFailure()
  238. {
  239. $this->expectException(PDOException::class);
  240. $this->expectExceptionMessage('Serialization failure');
  241. $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction', 'commit', 'rollBack'])->getMock();
  242. $mock = $this->getMockConnection([], $pdo);
  243. $pdo->expects($this->exactly(3))->method('commit')->will($this->throwException(new DatabaseConnectionTestMockPDOException('Serialization failure', '40001')));
  244. $pdo->expects($this->exactly(3))->method('beginTransaction');
  245. $pdo->expects($this->never())->method('rollBack');
  246. $mock->transaction(function () {
  247. }, 3);
  248. }
  249. public function testTransactionMethodRetriesOnDeadlock()
  250. {
  251. $this->expectException(QueryException::class);
  252. $this->expectExceptionMessage('Deadlock found when trying to get lock (SQL: )');
  253. $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction', 'commit', 'rollBack'])->getMock();
  254. $mock = $this->getMockConnection([], $pdo);
  255. $pdo->expects($this->exactly(3))->method('beginTransaction');
  256. $pdo->expects($this->exactly(3))->method('rollBack');
  257. $pdo->expects($this->never())->method('commit');
  258. $mock->transaction(function () {
  259. throw new QueryException('', [], new Exception('Deadlock found when trying to get lock'));
  260. }, 3);
  261. }
  262. public function testTransactionMethodRollsbackAndThrows()
  263. {
  264. $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction', 'commit', 'rollBack'])->getMock();
  265. $mock = $this->getMockConnection([], $pdo);
  266. $pdo->expects($this->once())->method('beginTransaction');
  267. $pdo->expects($this->once())->method('rollBack');
  268. $pdo->expects($this->never())->method('commit');
  269. try {
  270. $mock->transaction(function () {
  271. throw new Exception('foo');
  272. });
  273. } catch (Exception $e) {
  274. $this->assertSame('foo', $e->getMessage());
  275. }
  276. }
  277. public function testOnLostConnectionPDOIsNotSwappedWithinATransaction()
  278. {
  279. $this->expectException(QueryException::class);
  280. $this->expectExceptionMessage('server has gone away (SQL: foo)');
  281. $pdo = m::mock(PDO::class);
  282. $pdo->shouldReceive('beginTransaction')->once();
  283. $statement = m::mock(PDOStatement::class);
  284. $pdo->shouldReceive('prepare')->once()->andReturn($statement);
  285. $statement->shouldReceive('execute')->once()->andThrow(new PDOException('server has gone away'));
  286. $connection = new Connection($pdo);
  287. $connection->beginTransaction();
  288. $connection->statement('foo');
  289. }
  290. public function testOnLostConnectionPDOIsSwappedOutsideTransaction()
  291. {
  292. $pdo = m::mock(PDO::class);
  293. $statement = m::mock(PDOStatement::class);
  294. $statement->shouldReceive('execute')->once()->andThrow(new PDOException('server has gone away'));
  295. $statement->shouldReceive('execute')->once()->andReturn(true);
  296. $pdo->shouldReceive('prepare')->twice()->andReturn($statement);
  297. $connection = new Connection($pdo);
  298. $called = false;
  299. $connection->setReconnector(function ($connection) use (&$called) {
  300. $called = true;
  301. });
  302. $this->assertTrue($connection->statement('foo'));
  303. $this->assertTrue($called);
  304. }
  305. public function testRunMethodRetriesOnFailure()
  306. {
  307. $method = (new ReflectionClass(Connection::class))->getMethod('run');
  308. $method->setAccessible(true);
  309. $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class);
  310. $mock = $this->getMockConnection(['tryAgainIfCausedByLostConnection'], $pdo);
  311. $mock->expects($this->once())->method('tryAgainIfCausedByLostConnection');
  312. $method->invokeArgs($mock, ['', [], function () {
  313. throw new QueryException('', [], new Exception);
  314. }]);
  315. }
  316. public function testRunMethodNeverRetriesIfWithinTransaction()
  317. {
  318. $this->expectException(QueryException::class);
  319. $this->expectExceptionMessage('(SQL: ) (SQL: )');
  320. $method = (new ReflectionClass(Connection::class))->getMethod('run');
  321. $method->setAccessible(true);
  322. $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction'])->getMock();
  323. $mock = $this->getMockConnection(['tryAgainIfCausedByLostConnection'], $pdo);
  324. $pdo->expects($this->once())->method('beginTransaction');
  325. $mock->expects($this->never())->method('tryAgainIfCausedByLostConnection');
  326. $mock->beginTransaction();
  327. $method->invokeArgs($mock, ['', [], function () {
  328. throw new QueryException('', [], new Exception);
  329. }]);
  330. }
  331. public function testFromCreatesNewQueryBuilder()
  332. {
  333. $conn = $this->getMockConnection();
  334. $conn->setQueryGrammar(m::mock(Grammar::class));
  335. $conn->setPostProcessor(m::mock(Processor::class));
  336. $builder = $conn->table('users');
  337. $this->assertInstanceOf(BaseBuilder::class, $builder);
  338. $this->assertSame('users', $builder->from);
  339. }
  340. public function testPrepareBindings()
  341. {
  342. $date = m::mock(DateTime::class);
  343. $date->shouldReceive('format')->once()->with('foo')->andReturn('bar');
  344. $bindings = ['test' => $date];
  345. $conn = $this->getMockConnection();
  346. $grammar = m::mock(Grammar::class);
  347. $grammar->shouldReceive('getDateFormat')->once()->andReturn('foo');
  348. $conn->setQueryGrammar($grammar);
  349. $result = $conn->prepareBindings($bindings);
  350. $this->assertEquals(['test' => 'bar'], $result);
  351. }
  352. public function testLogQueryFiresEventsIfSet()
  353. {
  354. $connection = $this->getMockConnection();
  355. $connection->logQuery('foo', [], time());
  356. $connection->setEventDispatcher($events = m::mock(Dispatcher::class));
  357. $events->shouldReceive('dispatch')->once()->with(m::type(QueryExecuted::class));
  358. $connection->logQuery('foo', [], null);
  359. }
  360. public function testBeforeExecutingHooksCanBeRegistered()
  361. {
  362. $this->expectException(Exception::class);
  363. $this->expectExceptionMessage('The callback was fired');
  364. $connection = $this->getMockConnection();
  365. $connection->beforeExecuting(function () {
  366. throw new Exception('The callback was fired');
  367. });
  368. $connection->select('foo bar', ['baz']);
  369. }
  370. public function testPretendOnlyLogsQueries()
  371. {
  372. $connection = $this->getMockConnection();
  373. $queries = $connection->pretend(function ($connection) {
  374. $connection->select('foo bar', ['baz']);
  375. });
  376. $this->assertSame('foo bar', $queries[0]['query']);
  377. $this->assertEquals(['baz'], $queries[0]['bindings']);
  378. }
  379. public function testSchemaBuilderCanBeCreated()
  380. {
  381. $connection = $this->getMockConnection();
  382. $schema = $connection->getSchemaBuilder();
  383. $this->assertInstanceOf(Builder::class, $schema);
  384. $this->assertSame($connection, $schema->getConnection());
  385. }
  386. protected function getMockConnection($methods = [], $pdo = null)
  387. {
  388. $pdo = $pdo ?: new DatabaseConnectionTestMockPDO;
  389. $defaults = ['getDefaultQueryGrammar', 'getDefaultPostProcessor', 'getDefaultSchemaGrammar'];
  390. $connection = $this->getMockBuilder(Connection::class)->onlyMethods(array_merge($defaults, $methods))->setConstructorArgs([$pdo])->getMock();
  391. $connection->enableQueryLog();
  392. return $connection;
  393. }
  394. }
  395. class DatabaseConnectionTestMockPDO extends PDO
  396. {
  397. public function __construct()
  398. {
  399. //
  400. }
  401. }
  402. class DatabaseConnectionTestMockPDOException extends PDOException
  403. {
  404. /**
  405. * Overrides Exception::__construct, which casts $code to integer, so that we can create
  406. * an exception with a string $code consistent with the real PDOException behavior.
  407. *
  408. * @param string|null $message
  409. * @param string|null $code
  410. * @return void
  411. */
  412. public function __construct($message = null, $code = null)
  413. {
  414. $this->message = $message;
  415. $this->code = $code;
  416. }
  417. }