UpdateQueryTest.php 16 KB

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