UpdateQueryTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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 5.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Database\Query;
  17. use Cake\Database\Driver\Mysql;
  18. use Cake\Database\Exception\DatabaseException;
  19. use Cake\Database\Expression\IdentifierExpression;
  20. use Cake\Database\ExpressionInterface;
  21. use Cake\Database\Query\SelectQuery;
  22. use Cake\Database\Query\UpdateQuery;
  23. use Cake\Database\StatementInterface;
  24. use Cake\Database\ValueBinder;
  25. use Cake\Datasource\ConnectionManager;
  26. use Cake\Test\TestCase\Database\QueryAssertsTrait;
  27. use Cake\TestSuite\TestCase;
  28. use DateTime;
  29. /**
  30. * Tests UpdateQuery class
  31. */
  32. class UpdateQueryTest extends TestCase
  33. {
  34. use QueryAssertsTrait;
  35. protected array $fixtures = [
  36. 'core.Articles',
  37. 'core.Authors',
  38. 'core.Comments',
  39. ];
  40. /**
  41. * @var \Cake\Database\Connection
  42. */
  43. protected $connection;
  44. /**
  45. * @var bool
  46. */
  47. protected $autoQuote;
  48. public function setUp(): void
  49. {
  50. parent::setUp();
  51. $this->connection = ConnectionManager::get('test');
  52. $this->autoQuote = $this->connection->getDriver()->isAutoQuotingEnabled();
  53. }
  54. public function tearDown(): void
  55. {
  56. parent::tearDown();
  57. $this->connection->getDriver()->enableAutoQuoting($this->autoQuote);
  58. unset($this->connection);
  59. }
  60. /**
  61. * Test a simple update.
  62. */
  63. public function testUpdateSimple(): void
  64. {
  65. $query = new UpdateQuery($this->connection);
  66. $query->update('authors')
  67. ->set('name', 'mark')
  68. ->where(['id' => 1]);
  69. $result = $query->sql();
  70. $this->assertQuotedQuery('UPDATE <authors> SET <name> = :', $result, !$this->autoQuote);
  71. $result = $query->execute();
  72. $this->assertSame(1, $result->rowCount());
  73. $result->closeCursor();
  74. }
  75. /**
  76. * Test update with multiple fields.
  77. */
  78. public function testUpdateMultipleFields(): void
  79. {
  80. $query = new UpdateQuery($this->connection);
  81. $query->update('articles')
  82. ->set('title', 'mark', 'string')
  83. ->set('body', 'some text', 'string')
  84. ->where(['id' => 1]);
  85. $result = $query->sql();
  86. $this->assertQuotedQuery(
  87. 'UPDATE <articles> SET <title> = :c0 , <body> = :c1',
  88. $result,
  89. !$this->autoQuote
  90. );
  91. $this->assertQuotedQuery(' WHERE <id> = :c2$', $result, !$this->autoQuote);
  92. $result = $query->execute();
  93. $this->assertSame(1, $result->rowCount());
  94. $result->closeCursor();
  95. }
  96. /**
  97. * Test updating multiple fields with an array.
  98. */
  99. public function testUpdateMultipleFieldsArray(): void
  100. {
  101. $query = new UpdateQuery($this->connection);
  102. $query->update('articles')
  103. ->set([
  104. 'title' => 'mark',
  105. 'body' => 'some text',
  106. ], ['title' => 'string', 'body' => 'string'])
  107. ->where(['id' => 1]);
  108. $result = $query->sql();
  109. $this->assertQuotedQuery(
  110. 'UPDATE <articles> SET <title> = :c0 , <body> = :c1',
  111. $result,
  112. !$this->autoQuote
  113. );
  114. $this->assertQuotedQuery('WHERE <id> = :', $result, !$this->autoQuote);
  115. $result = $query->execute();
  116. $this->assertSame(1, $result->rowCount());
  117. $result->closeCursor();
  118. }
  119. /**
  120. * Test updates with an expression.
  121. */
  122. public function testUpdateWithExpression(): void
  123. {
  124. $query = new UpdateQuery($this->connection);
  125. $expr = $query->newExpr()->equalFields('article_id', 'user_id');
  126. $query->update('comments')
  127. ->set($expr)
  128. ->where(['id' => 1]);
  129. $result = $query->sql();
  130. $this->assertQuotedQuery(
  131. 'UPDATE <comments> SET <article_id> = <user_id> WHERE <id> = :',
  132. $result,
  133. !$this->autoQuote
  134. );
  135. $result = $query->execute();
  136. $this->assertSame(1, $result->rowCount());
  137. $result->closeCursor();
  138. }
  139. /**
  140. * Tests update with subquery that references itself
  141. */
  142. public function testUpdateSubquery(): void
  143. {
  144. $this->skipIf($this->connection->getDriver() instanceof Mysql);
  145. $subquery = new SelectQuery($this->connection);
  146. $subquery
  147. ->select('created')
  148. ->from(['c' => 'comments'])
  149. ->where(['c.id' => new IdentifierExpression('comments.id')]);
  150. $query = new UpdateQuery($this->connection);
  151. $query->update('comments')
  152. ->set('updated', $subquery);
  153. $this->assertEqualsSql(
  154. 'UPDATE comments SET updated = (SELECT created FROM comments c WHERE c.id = comments.id)',
  155. $query->sql(new ValueBinder())
  156. );
  157. $result = $query->execute();
  158. $this->assertSame(6, $result->rowCount());
  159. $result->closeCursor();
  160. $result = (new SelectQuery($this->connection))->select(['created', 'updated'])->from('comments')->execute();
  161. foreach ($result->fetchAll('assoc') as $row) {
  162. $this->assertSame($row['created'], $row['updated']);
  163. }
  164. $result->closeCursor();
  165. }
  166. /**
  167. * Test update with array fields and types.
  168. */
  169. public function testUpdateArrayFields(): void
  170. {
  171. $query = new UpdateQuery($this->connection);
  172. $date = new DateTime();
  173. $query->update('comments')
  174. ->set(['comment' => 'mark', 'created' => $date], ['created' => 'date'])
  175. ->where(['id' => 1]);
  176. $result = $query->sql();
  177. $this->assertQuotedQuery(
  178. 'UPDATE <comments> SET <comment> = :c0 , <created> = :c1',
  179. $result,
  180. !$this->autoQuote
  181. );
  182. $this->assertQuotedQuery(' WHERE <id> = :c2$', $result, !$this->autoQuote);
  183. $result = $query->execute();
  184. $this->assertSame(1, $result->rowCount());
  185. $query = new SelectQuery($this->connection);
  186. $result = $query->select('created')->from('comments')->where(['id' => 1])->execute();
  187. $result = $result->fetchAll('assoc')[0]['created'];
  188. $this->assertStringStartsWith($date->format('Y-m-d'), $result);
  189. }
  190. /**
  191. * Test update with callable in set
  192. */
  193. public function testUpdateSetCallable(): void
  194. {
  195. $query = new UpdateQuery($this->connection);
  196. $date = new DateTime();
  197. $query->update('comments')
  198. ->set(function ($exp) use ($date) {
  199. return $exp
  200. ->eq('comment', 'mark')
  201. ->eq('created', $date, 'date');
  202. })
  203. ->where(['id' => 1]);
  204. $result = $query->sql();
  205. $this->assertQuotedQuery(
  206. 'UPDATE <comments> SET <comment> = :c0 , <created> = :c1',
  207. $result,
  208. !$this->autoQuote
  209. );
  210. $this->assertQuotedQuery(' WHERE <id> = :c2$', $result, !$this->autoQuote);
  211. $result = $query->execute();
  212. $this->assertSame(1, $result->rowCount());
  213. }
  214. /**
  215. * Tests that aliases are stripped from update query conditions
  216. * where possible.
  217. */
  218. public function testUpdateStripAliasesFromConditions(): void
  219. {
  220. $query = new UpdateQuery($this->connection);
  221. $query
  222. ->update('authors')
  223. ->set(['name' => 'name'])
  224. ->where([
  225. 'OR' => [
  226. 'a.id' => 1,
  227. 'a.name IS' => null,
  228. 'a.email IS NOT' => null,
  229. 'AND' => [
  230. 'b.name NOT IN' => ['foo', 'bar'],
  231. 'OR' => [
  232. $query->newExpr()->eq(new IdentifierExpression('c.name'), 'zap'),
  233. 'd.name' => 'baz',
  234. (new SelectQuery($this->connection))->select(['e.name'])->where(['e.name' => 'oof']),
  235. ],
  236. ],
  237. ],
  238. ]);
  239. $this->assertQuotedQuery(
  240. 'UPDATE <authors> SET <name> = :c0 WHERE \(' .
  241. '<id> = :c1 OR \(<name>\) IS NULL OR \(<email>\) IS NOT NULL OR \(' .
  242. '<name> NOT IN \(:c2,:c3\) AND \(' .
  243. '<name> = :c4 OR <name> = :c5 OR \(SELECT <e>\.<name> WHERE <e>\.<name> = :c6\)' .
  244. '\)' .
  245. '\)' .
  246. '\)',
  247. $query->sql(),
  248. !$this->autoQuote
  249. );
  250. }
  251. /**
  252. * Tests that update queries that contain joins do trigger a notice,
  253. * warning about possible incompatibilities with aliases being removed
  254. * from the conditions.
  255. */
  256. public function testUpdateRemovingAliasesCanBreakJoins(): void
  257. {
  258. $this->expectException(DatabaseException::class);
  259. $this->expectExceptionMessage('Aliases are being removed from conditions for UPDATE/DELETE queries, this can break references to joined tables.');
  260. $query = new UpdateQuery($this->connection);
  261. $query
  262. ->update('authors')
  263. ->set(['name' => 'name'])
  264. ->innerJoin('articles')
  265. ->where(['a.id' => 1]);
  266. $query->sql();
  267. }
  268. /**
  269. * Test that epilog() will actually append a string to an update query
  270. */
  271. public function testAppendUpdate(): void
  272. {
  273. $query = new UpdateQuery($this->connection);
  274. $sql = $query
  275. ->update('articles')
  276. ->set(['title' => 'foo'])
  277. ->where(['id' => 1])
  278. ->epilog('RETURNING id')
  279. ->sql();
  280. $this->assertStringContainsString('UPDATE', $sql);
  281. $this->assertStringContainsString('SET', $sql);
  282. $this->assertStringContainsString('WHERE', $sql);
  283. $this->assertSame(' RETURNING id', substr($sql, -13));
  284. }
  285. /**
  286. * Performs the simple update query and verifies the row count.
  287. */
  288. public function testRowCountAndClose(): void
  289. {
  290. $statementMock = $this->getMockBuilder(StatementInterface::class)
  291. ->onlyMethods(['rowCount', 'closeCursor'])
  292. ->getMockForAbstractClass();
  293. $statementMock->expects($this->once())
  294. ->method('rowCount')
  295. ->willReturn(500);
  296. $statementMock->expects($this->once())
  297. ->method('closeCursor');
  298. $queryMock = $this->getMockBuilder(UpdateQuery::class)
  299. ->onlyMethods(['execute'])
  300. ->setConstructorArgs([$this->connection])
  301. ->getMock();
  302. $queryMock->expects($this->once())
  303. ->method('execute')
  304. ->willReturn($statementMock);
  305. $rowCount = $queryMock->update('authors')
  306. ->set('name', 'mark')
  307. ->where(['id' => 1])
  308. ->rowCountAndClose();
  309. $this->assertEquals(500, $rowCount);
  310. }
  311. public function testCloneUpdateExpression(): void
  312. {
  313. $query = new UpdateQuery($this->connection);
  314. $query->update($query->newExpr('update'));
  315. $clause = $query->clause('update');
  316. $clauseClone = (clone $query)->clause('update');
  317. $this->assertIsArray($clause);
  318. foreach ($clause as $key => $value) {
  319. $this->assertEquals($value, $clauseClone[$key]);
  320. $this->assertNotSame($value, $clauseClone[$key]);
  321. }
  322. }
  323. public function testCloneSetExpression(): void
  324. {
  325. $query = new UpdateQuery($this->connection);
  326. $query
  327. ->update('table')
  328. ->set(['column' => $query->newExpr('value')]);
  329. $clause = $query->clause('set');
  330. $clauseClone = (clone $query)->clause('set');
  331. $this->assertInstanceOf(ExpressionInterface::class, $clause);
  332. $this->assertEquals($clause, $clauseClone);
  333. $this->assertNotSame($clause, $clauseClone);
  334. }
  335. /**
  336. * Test use of modifiers in a UPDATE query
  337. *
  338. * Testing the generated SQL since the modifiers are usually different per driver
  339. */
  340. public function testUpdateModifiers(): void
  341. {
  342. $query = new UpdateQuery($this->connection);
  343. $result = $query
  344. ->update('authors')
  345. ->set('name', 'mark')
  346. ->modifier('TOP 10 PERCENT');
  347. $this->assertQuotedQuery(
  348. 'UPDATE TOP 10 PERCENT <authors> SET <name> = :c0',
  349. $result->sql(),
  350. !$this->autoQuote
  351. );
  352. $query = new UpdateQuery($this->connection);
  353. $result = $query
  354. ->update('authors')
  355. ->set('name', 'mark')
  356. ->modifier(['TOP 10 PERCENT', 'FOO']);
  357. $this->assertQuotedQuery(
  358. 'UPDATE TOP 10 PERCENT FOO <authors> SET <name> = :c0',
  359. $result->sql(),
  360. !$this->autoQuote
  361. );
  362. $query = new UpdateQuery($this->connection);
  363. $result = $query
  364. ->update('authors')
  365. ->set('name', 'mark')
  366. ->modifier([$query->newExpr('TOP 10 PERCENT')]);
  367. $this->assertQuotedQuery(
  368. 'UPDATE TOP 10 PERCENT <authors> SET <name> = :c0',
  369. $result->sql(),
  370. !$this->autoQuote
  371. );
  372. }
  373. /**
  374. * Tests that the JSON type can save and get data symmetrically
  375. */
  376. public function testSymmetricJsonType(): void
  377. {
  378. $query = new UpdateQuery($this->connection);
  379. $query
  380. ->update('comments')
  381. ->set('comment', ['a' => 'b', 'c' => true], 'json')
  382. ->where(['id' => 1]);
  383. $query->execute()->closeCursor();
  384. $query = new SelectQuery($this->connection);
  385. $query
  386. ->select(['comment'])
  387. ->from('comments')
  388. ->where(['id' => 1])
  389. ->getSelectTypeMap()->setTypes(['comment' => 'json']);
  390. $result = $query->execute();
  391. $comment = $result->fetchAll('assoc')[0]['comment'];
  392. $result->closeCursor();
  393. $this->assertSame(['a' => 'b', 'c' => true], $comment);
  394. }
  395. }