QueryRegressionTest.php 16 KB

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