QueryRegressionTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\ORM;
  16. use Cake\Core\Plugin;
  17. use Cake\I18n\Time;
  18. use Cake\ORM\Query;
  19. use Cake\ORM\Table;
  20. use Cake\ORM\TableRegistry;
  21. use Cake\TestSuite\TestCase;
  22. /**
  23. * Contains regression test for the Query builder
  24. *
  25. */
  26. class QueryRegressionTest extends TestCase
  27. {
  28. /**
  29. * Fixture to be used
  30. *
  31. * @var array
  32. */
  33. public $fixtures = [
  34. 'core.users',
  35. 'core.articles',
  36. 'core.comments',
  37. 'core.tags',
  38. 'core.articles_tags',
  39. 'core.authors',
  40. 'core.special_tags',
  41. 'core.translates',
  42. ];
  43. /**
  44. * Tear down
  45. *
  46. * @return void
  47. */
  48. public function tearDown()
  49. {
  50. parent::tearDown();
  51. TableRegistry::clear();
  52. }
  53. /**
  54. * Test for https://github.com/cakephp/cakephp/issues/3087
  55. *
  56. * @return void
  57. */
  58. public function testSelectTimestampColumn()
  59. {
  60. $table = TableRegistry::get('users');
  61. $user = $table->find()->where(['id' => 1])->first();
  62. $this->assertEquals(new Time('2007-03-17 01:16:23'), $user->created);
  63. $this->assertEquals(new Time('2007-03-17 01:18:31'), $user->updated);
  64. }
  65. /**
  66. * Tests that EagerLoader does not try to create queries for associations having no
  67. * keys to compare against
  68. *
  69. * @return void
  70. */
  71. public function testEagerLoadingFromEmptyResults()
  72. {
  73. $table = TableRegistry::get('Articles');
  74. $table->belongsToMany('ArticlesTags');
  75. $results = $table->find()->where(['id >' => 100])->contain('ArticlesTags')->toArray();
  76. $this->assertEmpty($results);
  77. }
  78. /**
  79. * Tests that eagerloading belongsToMany with find list fails with a helpful message.
  80. *
  81. * @expectedException \RuntimeException
  82. * @return void
  83. */
  84. public function testEagerLoadingBelongsToManyList()
  85. {
  86. $table = TableRegistry::get('Articles');
  87. $table->belongsToMany('Tags', [
  88. 'finder' => 'list'
  89. ]);
  90. $table->find()->contain('Tags')->toArray();
  91. }
  92. /**
  93. * Tests that duplicate aliases in contain() can be used, even when they would
  94. * naturally be attached to the query instead of eagerly loaded. What should
  95. * happen here is that One of the duplicates will be changed to be loaded using
  96. * an extra query, but yielding the same results
  97. *
  98. * @return void
  99. */
  100. public function testDuplicateAttachableAliases()
  101. {
  102. TableRegistry::get('Stuff', ['table' => 'tags']);
  103. TableRegistry::get('Things', ['table' => 'articles_tags']);
  104. $table = TableRegistry::get('Articles');
  105. $table->belongsTo('Authors');
  106. $table->hasOne('Things', ['propertyName' => 'articles_tag']);
  107. $table->Authors->target()->hasOne('Stuff', [
  108. 'foreignKey' => 'id',
  109. 'propertyName' => 'favorite_tag'
  110. ]);
  111. $table->Things->target()->belongsTo('Stuff', [
  112. 'foreignKey' => 'tag_id',
  113. 'propertyName' => 'foo'
  114. ]);
  115. $results = $table->find()
  116. ->contain(['Authors.Stuff', 'Things.Stuff'])
  117. ->order(['Articles.id' => 'ASC'])
  118. ->toArray();
  119. $this->assertEquals(1, $results[0]->articles_tag->foo->id);
  120. $this->assertEquals(1, $results[0]->author->favorite_tag->id);
  121. $this->assertEquals(2, $results[1]->articles_tag->foo->id);
  122. $this->assertEquals(1, $results[0]->author->favorite_tag->id);
  123. $this->assertEquals(1, $results[2]->articles_tag->foo->id);
  124. $this->assertEquals(3, $results[2]->author->favorite_tag->id);
  125. $this->assertEquals(3, $results[3]->articles_tag->foo->id);
  126. $this->assertEquals(3, $results[3]->author->favorite_tag->id);
  127. }
  128. /**
  129. * Test for https://github.com/cakephp/cakephp/issues/3410
  130. *
  131. * @return void
  132. */
  133. public function testNullableTimeColumn()
  134. {
  135. $table = TableRegistry::get('users');
  136. $entity = $table->newEntity(['username' => 'derp', 'created' => null]);
  137. $this->assertSame($entity, $table->save($entity));
  138. $this->assertNull($entity->created);
  139. }
  140. /**
  141. * Test for https://github.com/cakephp/cakephp/issues/3626
  142. *
  143. * Checks that join data is actually created and not tried to be updated every time
  144. * @return void
  145. */
  146. public function testCreateJointData()
  147. {
  148. $articles = TableRegistry::get('Articles');
  149. $articles->belongsToMany('Highlights', [
  150. 'className' => 'TestApp\Model\Table\TagsTable',
  151. 'foreignKey' => 'article_id',
  152. 'targetForeignKey' => 'tag_id',
  153. 'through' => 'SpecialTags'
  154. ]);
  155. $entity = $articles->get(2);
  156. $data = [
  157. 'id' => 2,
  158. 'highlights' => [
  159. [
  160. 'name' => 'New Special Tag',
  161. '_joinData' => ['highlighted' => true, 'highlighted_time' => '2014-06-01 10:10:00']
  162. ]
  163. ]
  164. ];
  165. $entity = $articles->patchEntity($entity, $data, ['Highlights._joinData']);
  166. $articles->save($entity);
  167. $entity = $articles->get(2, ['contain' => ['Highlights']]);
  168. $this->assertEquals(4, $entity->highlights[0]->_joinData->tag_id);
  169. $this->assertEquals('2014-06-01', $entity->highlights[0]->_joinData->highlighted_time->format('Y-m-d'));
  170. }
  171. /**
  172. * Tests that the junction table instance taken from both sides of a belongsToMany
  173. * relationship is actually the same object.
  174. *
  175. * @return void
  176. */
  177. public function testReciprocalBelongsToMany()
  178. {
  179. $articles = TableRegistry::get('Articles');
  180. $tags = TableRegistry::get('Tags');
  181. $articles->belongsToMany('Tags');
  182. $tags->belongsToMany('Articles');
  183. $left = $articles->Tags->junction();
  184. $right = $tags->Articles->junction();
  185. $this->assertSame($left, $right);
  186. }
  187. /**
  188. * Test for https://github.com/cakephp/cakephp/issues/4253
  189. *
  190. * Makes sure that the belongsToMany association is not overwritten with conflicting information
  191. * by any of the sides when the junction() function is invoked
  192. *
  193. * @return void
  194. */
  195. public function testReciprocalBelongsToMany2()
  196. {
  197. $articles = TableRegistry::get('Articles');
  198. $tags = TableRegistry::get('Tags');
  199. $articles->belongsToMany('Tags');
  200. $tags->belongsToMany('Articles');
  201. $sub = $articles->Tags->find()->select(['id'])->matching('Articles', function ($q) {
  202. return $q->where(['Articles.id' => 1]);
  203. });
  204. $query = $articles->Tags->find()->where(['id NOT IN' => $sub]);
  205. $this->assertEquals(1, $query->count());
  206. }
  207. /**
  208. * Returns an array with the saving strategies for a belongsTo association
  209. *
  210. * @return array
  211. */
  212. public function strategyProvider()
  213. {
  214. return [['append', 'replace']];
  215. }
  216. /**
  217. * Test for https://github.com/cakephp/cakephp/issues/3677 and
  218. * https://github.com/cakephp/cakephp/issues/3714
  219. *
  220. * Checks that only relevant associations are passed when saving _joinData
  221. * Tests that _joinData can also save deeper associations
  222. *
  223. * @dataProvider strategyProvider
  224. * @param string $strategy
  225. * @return void
  226. */
  227. public function testBelongsToManyDeepSave($strategy)
  228. {
  229. $articles = TableRegistry::get('Articles');
  230. $articles->belongsToMany('Highlights', [
  231. 'className' => 'TestApp\Model\Table\TagsTable',
  232. 'foreignKey' => 'article_id',
  233. 'targetForeignKey' => 'tag_id',
  234. 'through' => 'SpecialTags',
  235. 'saveStrategy' => $strategy
  236. ]);
  237. $articles->Highlights->junction()->belongsTo('Authors');
  238. $articles->Highlights->hasOne('Authors', [
  239. 'foreignKey' => 'id'
  240. ]);
  241. $entity = $articles->get(2, ['contain' => ['Highlights']]);
  242. $data = [
  243. 'highlights' => [
  244. [
  245. 'name' => 'New Special Tag',
  246. '_joinData' => [
  247. 'highlighted' => true,
  248. 'highlighted_time' => '2014-06-01 10:10:00',
  249. 'author' => [
  250. 'name' => 'mariano'
  251. ]
  252. ],
  253. 'author' => ['name' => 'mark']
  254. ]
  255. ]
  256. ];
  257. $options = [
  258. 'associated' => [
  259. 'Highlights._joinData.Authors', 'Highlights.Authors'
  260. ]
  261. ];
  262. $entity = $articles->patchEntity($entity, $data, $options);
  263. $articles->save($entity, $options);
  264. $entity = $articles->get(2, [
  265. 'contain' => [
  266. 'SpecialTags' => ['sort' => ['SpecialTags.id' => 'ASC']],
  267. 'SpecialTags.Authors',
  268. 'Highlights.Authors'
  269. ]
  270. ]);
  271. $this->assertEquals('mariano', end($entity->special_tags)->author->name);
  272. $this->assertEquals('mark', end($entity->highlights)->author->name);
  273. }
  274. /**
  275. * Tests that no exceptions are generated becuase of ambiguous column names in queries
  276. * during a save operation
  277. *
  278. * @see https://github.com/cakephp/cakephp/issues/3803
  279. * @return void
  280. */
  281. public function testSaveWithCallbacks()
  282. {
  283. $articles = TableRegistry::get('Articles');
  284. $articles->belongsTo('Authors');
  285. $articles->eventManager()->attach(function ($event, $query) {
  286. return $query->contain('Authors');
  287. }, 'Model.beforeFind');
  288. $article = $articles->newEntity();
  289. $article->title = 'Foo';
  290. $article->body = 'Bar';
  291. $this->assertSame($article, $articles->save($article));
  292. }
  293. /**
  294. * Test that save() works with entities containing expressions
  295. * as properties.
  296. *
  297. * @return void
  298. */
  299. public function testSaveWithExpressionProperty()
  300. {
  301. $articles = TableRegistry::get('Articles');
  302. $article = $articles->newEntity();
  303. $article->title = new \Cake\Database\Expression\QueryExpression("SELECT 'jose'");
  304. $this->assertSame($article, $articles->save($article));
  305. }
  306. /**
  307. * Tests that whe saving deep associations for a belongsToMany property,
  308. * data is not removed becuase of excesive associations filtering.
  309. *
  310. * @see https://github.com/cakephp/cakephp/issues/4009
  311. * @return void
  312. */
  313. public function testBelongsToManyDeepSave2()
  314. {
  315. $articles = TableRegistry::get('Articles');
  316. $articles->belongsToMany('Highlights', [
  317. 'className' => 'TestApp\Model\Table\TagsTable',
  318. 'foreignKey' => 'article_id',
  319. 'targetForeignKey' => 'tag_id',
  320. 'through' => 'SpecialTags',
  321. ]);
  322. $articles->Highlights->hasMany('TopArticles', [
  323. 'className' => 'TestApp\Model\Table\ArticlesTable',
  324. 'foreignKey' => 'author_id',
  325. ]);
  326. $entity = $articles->get(2, ['contain' => ['Highlights']]);
  327. $data = [
  328. 'highlights' => [
  329. [
  330. 'name' => 'New Special Tag',
  331. '_joinData' => [
  332. 'highlighted' => true,
  333. 'highlighted_time' => '2014-06-01 10:10:00',
  334. ],
  335. 'top_articles' => [
  336. ['title' => 'First top article'],
  337. ['title' => 'Second top article'],
  338. ]
  339. ]
  340. ]
  341. ];
  342. $options = [
  343. 'associated' => [
  344. 'Highlights._joinData', 'Highlights.TopArticles'
  345. ]
  346. ];
  347. $entity = $articles->patchEntity($entity, $data, $options);
  348. $articles->save($entity, $options);
  349. $entity = $articles->get(2, [
  350. 'contain' => [
  351. 'Highlights.TopArticles'
  352. ]
  353. ]);
  354. $highlights = $entity->highlights[0];
  355. $this->assertEquals('First top article', $highlights->top_articles[0]->title);
  356. $this->assertEquals('Second top article', $highlights->top_articles[1]->title);
  357. $this->assertEquals(
  358. new Time('2014-06-01 10:10:00'),
  359. $highlights->_joinData->highlighted_time
  360. );
  361. }
  362. /**
  363. * An integration test that spot checks that associations use the
  364. * correct alias names to generate queries.
  365. *
  366. * @return void
  367. */
  368. public function testPluginAssociationQueryGeneration()
  369. {
  370. Plugin::load('TestPlugin');
  371. $articles = TableRegistry::get('Articles');
  372. $articles->hasMany('TestPlugin.Comments');
  373. $articles->belongsTo('TestPlugin.Authors');
  374. $result = $articles->find()
  375. ->where(['Articles.id' => 2])
  376. ->contain(['Comments', 'Authors'])
  377. ->first();
  378. $this->assertNotEmpty(
  379. $result->comments[0]->id,
  380. 'No SQL error and comment exists.'
  381. );
  382. $this->assertNotEmpty(
  383. $result->author->id,
  384. 'No SQL error and author exists.'
  385. );
  386. }
  387. /**
  388. * Tests that loading associations having the same alias in the
  389. * joinable associations chain is not sensitive to the order in which
  390. * the associations are selected.
  391. *
  392. * @see https://github.com/cakephp/cakephp/issues/4454
  393. * @return void
  394. */
  395. public function testAssociationChainOrder()
  396. {
  397. $articles = TableRegistry::get('Articles');
  398. $articles->belongsTo('Authors');
  399. $articles->hasOne('ArticlesTags');
  400. $articlesTags = TableRegistry::get('ArticlesTags');
  401. $articlesTags->belongsTo('Authors', [
  402. 'foreignKey' => 'tag_id'
  403. ]);
  404. $resultA = $articles->find()
  405. ->contain(['ArticlesTags.Authors', 'Authors'])
  406. ->first();
  407. $resultB = $articles->find()
  408. ->contain(['Authors', 'ArticlesTags.Authors'])
  409. ->first();
  410. $this->assertEquals($resultA, $resultB);
  411. $this->assertNotEmpty($resultA->author);
  412. $this->assertNotEmpty($resultA->articles_tag->author);
  413. }
  414. /**
  415. * Test that offset/limit are elided from subquery loads.
  416. *
  417. * @return void
  418. */
  419. public function testAssociationSubQueryNoOffset()
  420. {
  421. $table = TableRegistry::get('Articles');
  422. $table->addBehavior('Translate', ['fields' => ['title', 'body']]);
  423. $table->locale('eng');
  424. $query = $table->find('translations')->limit(10)->offset(1);
  425. $result = $query->toArray();
  426. $this->assertCount(2, $result);
  427. }
  428. /**
  429. * Tests that using the subquery strategy in a deep assotiatin returns the right results
  430. *
  431. * @see https://github.com/cakephp/cakephp/issues/4484
  432. * @return void
  433. */
  434. public function testDeepBelongsToManySubqueryStrategy()
  435. {
  436. $table = TableRegistry::get('Authors');
  437. $table->hasMany('Articles');
  438. $table->Articles->belongsToMany('Tags', [
  439. 'strategy' => 'subquery'
  440. ]);
  441. $table->Articles->Tags->junction();
  442. $result = $table->find()->contain(['Articles.Tags'])->toArray();
  443. $this->assertEquals(
  444. ['tag1', 'tag3'],
  445. collection($result[2]->articles[0]->tags)->extract('name')->toArray()
  446. );
  447. }
  448. /**
  449. * Tests that getting the count of a query having containments return
  450. * the correct results
  451. *
  452. * @see https://github.com/cakephp/cakephp/issues/4511
  453. * @return void
  454. */
  455. public function testCountWithContain()
  456. {
  457. $table = TableRegistry::get('Articles');
  458. $table->belongsTo('Authors', ['joinType' => 'inner']);
  459. $count = $table
  460. ->find()
  461. ->contain(['Authors' => function ($q) {
  462. return $q->where(['Authors.id' => 1]);
  463. }])
  464. ->count();
  465. $this->assertEquals(2, $count);
  466. }
  467. /**
  468. * Test that deep containments don't generate empty entities for
  469. * intermediary relations.
  470. *
  471. * @return void
  472. */
  473. public function testContainNoEmptyAssociatedObjects()
  474. {
  475. $comments = TableRegistry::get('Comments');
  476. $comments->belongsTo('Users');
  477. $users = TableRegistry::get('Users');
  478. $users->hasMany('Articles', [
  479. 'foreignKey' => 'author_id'
  480. ]);
  481. $comments->updateAll(['user_id' => 99], ['id' => 1]);
  482. $result = $comments->find()
  483. ->contain(['Users'])
  484. ->where(['Comments.id' => 1])
  485. ->first();
  486. $this->assertNull($result->user, 'No record should be null.');
  487. $result = $comments->find()
  488. ->contain(['Users', 'Users.Articles'])
  489. ->where(['Comments.id' => 1])
  490. ->first();
  491. $this->assertNull($result->user, 'No record should be null.');
  492. }
  493. /**
  494. * Tests that using a comparison expression inside an OR condition works
  495. *
  496. * @see https://github.com/cakephp/cakephp/issues/5081
  497. * @return void
  498. */
  499. public function testOrConditionsWithExpression()
  500. {
  501. $table = TableRegistry::get('Articles');
  502. $query = $table->find();
  503. $query->where([
  504. 'OR' => [
  505. new \Cake\Database\Expression\Comparison('id', 1, 'integer', '>'),
  506. new \Cake\Database\Expression\Comparison('id', 3, 'integer', '<')
  507. ]
  508. ]);
  509. $results = $query->toArray();
  510. $this->assertCount(3, $results);
  511. }
  512. /**
  513. * Tests that calling count on a query having a union works correctly
  514. *
  515. * @see https://github.com/cakephp/cakephp/issues/5107
  516. * @return void
  517. */
  518. public function testCountWithUnionQuery()
  519. {
  520. $table = TableRegistry::get('Articles');
  521. $query = $table->find()->where(['id' => 1]);
  522. $query2 = $table->find()->where(['id' => 2]);
  523. $query->union($query2);
  524. $this->assertEquals(2, $query->count());
  525. }
  526. /**
  527. * Integration test when selecting no fields on the primary table.
  528. *
  529. * @return void
  530. */
  531. public function testSelectNoFieldsOnPrimaryAlias()
  532. {
  533. $table = TableRegistry::get('Articles');
  534. $table->belongsTo('Users');
  535. $query = $table->find()
  536. ->select(['Users__id' => 'id']);
  537. $results = $query->toArray();
  538. $this->assertCount(3, $results);
  539. }
  540. /**
  541. * Tests that calling first on the query results will not remove all other results
  542. * from the set.
  543. *
  544. * @return void
  545. */
  546. public function testFirstOnResultSet()
  547. {
  548. $results = TableRegistry::get('Articles')->find()->all();
  549. $this->assertEquals(3, $results->count());
  550. $this->assertNotNull($results->first());
  551. $this->assertCount(3, $results->toArray());
  552. }
  553. /**
  554. * Checks that matching and contain can be called for the same belongsTo association
  555. *
  556. * @see https://github.com/cakephp/cakephp/issues/5463
  557. * @return void
  558. */
  559. public function testFindMatchingAndContain()
  560. {
  561. $table = TableRegistry::get('Articles');
  562. $table->belongsTo('Authors');
  563. $article = $table->find()
  564. ->contain('Authors')
  565. ->matching('Authors', function ($q) {
  566. return $q->where(['Authors.id' => 1]);
  567. })
  568. ->first();
  569. $this->assertNotNull($article->author);
  570. $this->assertEquals($article->author, $article->_matchingData['Authors']);
  571. }
  572. /**
  573. * Checks that matching and contain can be called for the same belongsTo association
  574. *
  575. * @see https://github.com/cakephp/cakephp/issues/5463
  576. * @return void
  577. */
  578. public function testFindMatchingAndContainWithSubquery()
  579. {
  580. $table = TableRegistry::get('authors');
  581. $table->hasMany('articles', ['strategy' => 'subquery']);
  582. $table->articles->belongsToMany('tags');
  583. $result = $table->find()
  584. ->matching('articles.tags', function ($q) {
  585. return $q->where(['tags.id' => 2]);
  586. })
  587. ->contain('articles');
  588. $this->assertCount(2, $result->first()->articles);
  589. }
  590. }