QueryRegressionTest.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209
  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.articles',
  35. 'core.articles_tags',
  36. 'core.authors',
  37. 'core.authors_tags',
  38. 'core.comments',
  39. 'core.featured_tags',
  40. 'core.special_tags',
  41. 'core.tags',
  42. 'core.tags_translations',
  43. 'core.translates',
  44. 'core.users'
  45. ];
  46. /**
  47. * Tear down
  48. *
  49. * @return void
  50. */
  51. public function tearDown()
  52. {
  53. parent::tearDown();
  54. TableRegistry::clear();
  55. }
  56. /**
  57. * Test for https://github.com/cakephp/cakephp/issues/3087
  58. *
  59. * @return void
  60. */
  61. public function testSelectTimestampColumn()
  62. {
  63. $table = TableRegistry::get('users');
  64. $user = $table->find()->where(['id' => 1])->first();
  65. $this->assertEquals(new Time('2007-03-17 01:16:23'), $user->created);
  66. $this->assertEquals(new Time('2007-03-17 01:18:31'), $user->updated);
  67. }
  68. /**
  69. * Tests that EagerLoader does not try to create queries for associations having no
  70. * keys to compare against
  71. *
  72. * @return void
  73. */
  74. public function testEagerLoadingFromEmptyResults()
  75. {
  76. $table = TableRegistry::get('Articles');
  77. $table->belongsToMany('ArticlesTags');
  78. $results = $table->find()->where(['id >' => 100])->contain('ArticlesTags')->toArray();
  79. $this->assertEmpty($results);
  80. }
  81. /**
  82. * Tests that eagerloading belongsToMany with find list fails with a helpful message.
  83. *
  84. * @expectedException \RuntimeException
  85. * @return void
  86. */
  87. public function testEagerLoadingBelongsToManyList()
  88. {
  89. $table = TableRegistry::get('Articles');
  90. $table->belongsToMany('Tags', [
  91. 'finder' => 'list'
  92. ]);
  93. $table->find()->contain('Tags')->toArray();
  94. }
  95. /**
  96. * Test that association proxy find() applies joins when conditions are involved.
  97. *
  98. * @return void
  99. */
  100. public function testBelongsToManyAssociationProxyFindWithConditions()
  101. {
  102. $table = TableRegistry::get('Articles');
  103. $table->belongsToMany('Tags', [
  104. 'foreignKey' => 'article_id',
  105. 'associationForeignKey' => 'tag_id',
  106. 'conditions' => ['SpecialTags.highlighted' => true],
  107. 'through' => 'SpecialTags'
  108. ]);
  109. $query = $table->Tags->find();
  110. $result = $query->toArray();
  111. $this->assertCount(1, $result);
  112. }
  113. /**
  114. * Test that association proxy find() with matching resolves joins correctly
  115. *
  116. * @return void
  117. */
  118. public function testBelongsToManyAssociationProxyFindWithConditionsMatching()
  119. {
  120. $table = TableRegistry::get('Articles');
  121. $table->belongsToMany('Tags', [
  122. 'foreignKey' => 'article_id',
  123. 'associationForeignKey' => 'tag_id',
  124. 'conditions' => ['SpecialTags.highlighted' => true],
  125. 'through' => 'SpecialTags'
  126. ]);
  127. $query = $table->Tags->find()->matching('Articles', function ($query) {
  128. return $query->where(['Articles.id' => 1]);
  129. });
  130. // The inner join on special_tags excludes the results.
  131. $this->assertEquals(0, $query->count());
  132. }
  133. /**
  134. * Tests that duplicate aliases in contain() can be used, even when they would
  135. * naturally be attached to the query instead of eagerly loaded. What should
  136. * happen here is that One of the duplicates will be changed to be loaded using
  137. * an extra query, but yielding the same results
  138. *
  139. * @return void
  140. */
  141. public function testDuplicateAttachableAliases()
  142. {
  143. TableRegistry::get('Stuff', ['table' => 'tags']);
  144. TableRegistry::get('Things', ['table' => 'articles_tags']);
  145. $table = TableRegistry::get('Articles');
  146. $table->belongsTo('Authors');
  147. $table->hasOne('Things', ['propertyName' => 'articles_tag']);
  148. $table->Authors->target()->hasOne('Stuff', [
  149. 'foreignKey' => 'id',
  150. 'propertyName' => 'favorite_tag'
  151. ]);
  152. $table->Things->target()->belongsTo('Stuff', [
  153. 'foreignKey' => 'tag_id',
  154. 'propertyName' => 'foo'
  155. ]);
  156. $results = $table->find()
  157. ->contain(['Authors.Stuff', 'Things.Stuff'])
  158. ->order(['Articles.id' => 'ASC'])
  159. ->toArray();
  160. $this->assertEquals(1, $results[0]->articles_tag->foo->id);
  161. $this->assertEquals(1, $results[0]->author->favorite_tag->id);
  162. $this->assertEquals(2, $results[1]->articles_tag->foo->id);
  163. $this->assertEquals(1, $results[0]->author->favorite_tag->id);
  164. $this->assertEquals(1, $results[2]->articles_tag->foo->id);
  165. $this->assertEquals(3, $results[2]->author->favorite_tag->id);
  166. $this->assertEquals(3, $results[3]->articles_tag->foo->id);
  167. $this->assertEquals(3, $results[3]->author->favorite_tag->id);
  168. }
  169. /**
  170. * Test for https://github.com/cakephp/cakephp/issues/3410
  171. *
  172. * @return void
  173. */
  174. public function testNullableTimeColumn()
  175. {
  176. $table = TableRegistry::get('users');
  177. $entity = $table->newEntity(['username' => 'derp', 'created' => null]);
  178. $this->assertSame($entity, $table->save($entity));
  179. $this->assertNull($entity->created);
  180. }
  181. /**
  182. * Test for https://github.com/cakephp/cakephp/issues/3626
  183. *
  184. * Checks that join data is actually created and not tried to be updated every time
  185. * @return void
  186. */
  187. public function testCreateJointData()
  188. {
  189. $articles = TableRegistry::get('Articles');
  190. $articles->belongsToMany('Highlights', [
  191. 'className' => 'TestApp\Model\Table\TagsTable',
  192. 'foreignKey' => 'article_id',
  193. 'targetForeignKey' => 'tag_id',
  194. 'through' => 'SpecialTags'
  195. ]);
  196. $entity = $articles->get(2);
  197. $data = [
  198. 'id' => 2,
  199. 'highlights' => [
  200. [
  201. 'name' => 'New Special Tag',
  202. '_joinData' => ['highlighted' => true, 'highlighted_time' => '2014-06-01 10:10:00']
  203. ]
  204. ]
  205. ];
  206. $entity = $articles->patchEntity($entity, $data, ['Highlights._joinData']);
  207. $articles->save($entity);
  208. $entity = $articles->get(2, ['contain' => ['Highlights']]);
  209. $this->assertEquals(4, $entity->highlights[0]->_joinData->tag_id);
  210. $this->assertEquals('2014-06-01', $entity->highlights[0]->_joinData->highlighted_time->format('Y-m-d'));
  211. }
  212. /**
  213. * Tests that the junction table instance taken from both sides of a belongsToMany
  214. * relationship is actually the same object.
  215. *
  216. * @return void
  217. */
  218. public function testReciprocalBelongsToMany()
  219. {
  220. $articles = TableRegistry::get('Articles');
  221. $tags = TableRegistry::get('Tags');
  222. $articles->belongsToMany('Tags');
  223. $tags->belongsToMany('Articles');
  224. $left = $articles->Tags->junction();
  225. $right = $tags->Articles->junction();
  226. $this->assertSame($left, $right);
  227. }
  228. /**
  229. * Test for https://github.com/cakephp/cakephp/issues/4253
  230. *
  231. * Makes sure that the belongsToMany association is not overwritten with conflicting information
  232. * by any of the sides when the junction() function is invoked
  233. *
  234. * @return void
  235. */
  236. public function testReciprocalBelongsToManyNoOverwrite()
  237. {
  238. $articles = TableRegistry::get('Articles');
  239. $tags = TableRegistry::get('Tags');
  240. $articles->belongsToMany('Tags');
  241. $tags->belongsToMany('Articles');
  242. $sub = $articles->Tags->find()->select(['Tags.id'])->matching('Articles', function ($q) {
  243. return $q->where(['Articles.id' => 1]);
  244. });
  245. $query = $articles->Tags->find()->where(['Tags.id NOT IN' => $sub]);
  246. $this->assertEquals(1, $query->count());
  247. }
  248. /**
  249. * Returns an array with the saving strategies for a belongsTo association
  250. *
  251. * @return array
  252. */
  253. public function strategyProvider()
  254. {
  255. return [['append', 'replace']];
  256. }
  257. /**
  258. * Test for https://github.com/cakephp/cakephp/issues/3677 and
  259. * https://github.com/cakephp/cakephp/issues/3714
  260. *
  261. * Checks that only relevant associations are passed when saving _joinData
  262. * Tests that _joinData can also save deeper associations
  263. *
  264. * @dataProvider strategyProvider
  265. * @param string $strategy
  266. * @return void
  267. */
  268. public function testBelongsToManyDeepSave($strategy)
  269. {
  270. $articles = TableRegistry::get('Articles');
  271. $articles->belongsToMany('Highlights', [
  272. 'className' => 'TestApp\Model\Table\TagsTable',
  273. 'foreignKey' => 'article_id',
  274. 'targetForeignKey' => 'tag_id',
  275. 'through' => 'SpecialTags',
  276. 'saveStrategy' => $strategy
  277. ]);
  278. $articles->Highlights->junction()->belongsTo('Authors');
  279. $articles->Highlights->hasOne('Authors', [
  280. 'foreignKey' => 'id'
  281. ]);
  282. $entity = $articles->get(2, ['contain' => ['Highlights']]);
  283. $data = [
  284. 'highlights' => [
  285. [
  286. 'name' => 'New Special Tag',
  287. '_joinData' => [
  288. 'highlighted' => true,
  289. 'highlighted_time' => '2014-06-01 10:10:00',
  290. 'author' => [
  291. 'name' => 'mariano'
  292. ]
  293. ],
  294. 'author' => ['name' => 'mark']
  295. ]
  296. ]
  297. ];
  298. $options = [
  299. 'associated' => [
  300. 'Highlights._joinData.Authors', 'Highlights.Authors'
  301. ]
  302. ];
  303. $entity = $articles->patchEntity($entity, $data, $options);
  304. $articles->save($entity, $options);
  305. $entity = $articles->get(2, [
  306. 'contain' => [
  307. 'SpecialTags' => ['sort' => ['SpecialTags.id' => 'ASC']],
  308. 'SpecialTags.Authors',
  309. 'Highlights.Authors'
  310. ]
  311. ]);
  312. $this->assertEquals('mark', end($entity->highlights)->author->name);
  313. $lastTag = end($entity->special_tags);
  314. $this->assertTrue($lastTag->highlighted);
  315. $this->assertEquals('2014-06-01 10:10:00', $lastTag->highlighted_time->format('Y-m-d H:i:s'));
  316. $this->assertEquals('mariano', $lastTag->author->name);
  317. }
  318. /**
  319. * Tests that no exceptions are generated becuase of ambiguous column names in queries
  320. * during a save operation
  321. *
  322. * @see https://github.com/cakephp/cakephp/issues/3803
  323. * @return void
  324. */
  325. public function testSaveWithCallbacks()
  326. {
  327. $articles = TableRegistry::get('Articles');
  328. $articles->belongsTo('Authors');
  329. $articles->eventManager()->attach(function ($event, $query) {
  330. return $query->contain('Authors');
  331. }, 'Model.beforeFind');
  332. $article = $articles->newEntity();
  333. $article->title = 'Foo';
  334. $article->body = 'Bar';
  335. $this->assertSame($article, $articles->save($article));
  336. }
  337. /**
  338. * Test that save() works with entities containing expressions
  339. * as properties.
  340. *
  341. * @return void
  342. */
  343. public function testSaveWithExpressionProperty()
  344. {
  345. $articles = TableRegistry::get('Articles');
  346. $article = $articles->newEntity();
  347. $article->title = new \Cake\Database\Expression\QueryExpression("SELECT 'jose'");
  348. $this->assertSame($article, $articles->save($article));
  349. }
  350. /**
  351. * Tests that whe saving deep associations for a belongsToMany property,
  352. * data is not removed becuase of excesive associations filtering.
  353. *
  354. * @see https://github.com/cakephp/cakephp/issues/4009
  355. * @return void
  356. */
  357. public function testBelongsToManyDeepSave2()
  358. {
  359. $articles = TableRegistry::get('Articles');
  360. $articles->belongsToMany('Highlights', [
  361. 'className' => 'TestApp\Model\Table\TagsTable',
  362. 'foreignKey' => 'article_id',
  363. 'targetForeignKey' => 'tag_id',
  364. 'through' => 'SpecialTags',
  365. ]);
  366. $articles->Highlights->hasMany('TopArticles', [
  367. 'className' => 'TestApp\Model\Table\ArticlesTable',
  368. 'foreignKey' => 'author_id',
  369. ]);
  370. $entity = $articles->get(2, ['contain' => ['Highlights']]);
  371. $data = [
  372. 'highlights' => [
  373. [
  374. 'name' => 'New Special Tag',
  375. '_joinData' => [
  376. 'highlighted' => true,
  377. 'highlighted_time' => '2014-06-01 10:10:00',
  378. ],
  379. 'top_articles' => [
  380. ['title' => 'First top article'],
  381. ['title' => 'Second top article'],
  382. ]
  383. ]
  384. ]
  385. ];
  386. $options = [
  387. 'associated' => [
  388. 'Highlights._joinData', 'Highlights.TopArticles'
  389. ]
  390. ];
  391. $entity = $articles->patchEntity($entity, $data, $options);
  392. $articles->save($entity, $options);
  393. $entity = $articles->get(2, [
  394. 'contain' => [
  395. 'Highlights.TopArticles'
  396. ]
  397. ]);
  398. $highlights = $entity->highlights[0];
  399. $this->assertEquals('First top article', $highlights->top_articles[0]->title);
  400. $this->assertEquals('Second top article', $highlights->top_articles[1]->title);
  401. $this->assertEquals(
  402. new Time('2014-06-01 10:10:00'),
  403. $highlights->_joinData->highlighted_time
  404. );
  405. }
  406. /**
  407. * An integration test that spot checks that associations use the
  408. * correct alias names to generate queries.
  409. *
  410. * @return void
  411. */
  412. public function testPluginAssociationQueryGeneration()
  413. {
  414. Plugin::load('TestPlugin');
  415. $articles = TableRegistry::get('Articles');
  416. $articles->hasMany('TestPlugin.Comments');
  417. $articles->belongsTo('TestPlugin.Authors');
  418. $result = $articles->find()
  419. ->where(['Articles.id' => 2])
  420. ->contain(['Comments', 'Authors'])
  421. ->first();
  422. $this->assertNotEmpty(
  423. $result->comments[0]->id,
  424. 'No SQL error and comment exists.'
  425. );
  426. $this->assertNotEmpty(
  427. $result->author->id,
  428. 'No SQL error and author exists.'
  429. );
  430. }
  431. /**
  432. * Tests that loading associations having the same alias in the
  433. * joinable associations chain is not sensitive to the order in which
  434. * the associations are selected.
  435. *
  436. * @see https://github.com/cakephp/cakephp/issues/4454
  437. * @return void
  438. */
  439. public function testAssociationChainOrder()
  440. {
  441. $articles = TableRegistry::get('Articles');
  442. $articles->belongsTo('Authors');
  443. $articles->hasOne('ArticlesTags');
  444. $articlesTags = TableRegistry::get('ArticlesTags');
  445. $articlesTags->belongsTo('Authors', [
  446. 'foreignKey' => 'tag_id'
  447. ]);
  448. $resultA = $articles->find()
  449. ->contain(['ArticlesTags.Authors', 'Authors'])
  450. ->first();
  451. $resultB = $articles->find()
  452. ->contain(['Authors', 'ArticlesTags.Authors'])
  453. ->first();
  454. $this->assertEquals($resultA, $resultB);
  455. $this->assertNotEmpty($resultA->author);
  456. $this->assertNotEmpty($resultA->articles_tag->author);
  457. }
  458. /**
  459. * Test that offset/limit are elided from subquery loads.
  460. *
  461. * @return void
  462. */
  463. public function testAssociationSubQueryNoOffset()
  464. {
  465. $table = TableRegistry::get('Articles');
  466. $table->addBehavior('Translate', ['fields' => ['title', 'body']]);
  467. $table->locale('eng');
  468. $query = $table->find('translations')
  469. ->order(['Articles.id' => 'ASC'])
  470. ->limit(10)
  471. ->offset(1);
  472. $result = $query->toArray();
  473. $this->assertCount(2, $result);
  474. }
  475. /**
  476. * Tests that using the subquery strategy in a deep association returns the right results
  477. *
  478. * @see https://github.com/cakephp/cakephp/issues/4484
  479. * @return void
  480. */
  481. public function testDeepBelongsToManySubqueryStrategy()
  482. {
  483. $table = TableRegistry::get('Authors');
  484. $table->hasMany('Articles');
  485. $table->Articles->belongsToMany('Tags', [
  486. 'strategy' => 'subquery'
  487. ]);
  488. $result = $table->find()->contain(['Articles.Tags'])->toArray();
  489. $this->assertEquals(
  490. ['tag1', 'tag3'],
  491. collection($result[2]->articles[0]->tags)->extract('name')->toArray()
  492. );
  493. }
  494. /**
  495. * Tests that using the subquery strategy in a deep association returns the right results
  496. *
  497. * @see https://github.com/cakephp/cakephp/issues/5769
  498. * @return void
  499. */
  500. public function testDeepBelongsToManySubqueryStrategy2()
  501. {
  502. $table = TableRegistry::get('Authors');
  503. $table->hasMany('Articles');
  504. $table->Articles->belongsToMany('Tags', [
  505. 'strategy' => 'subquery'
  506. ]);
  507. $table->belongsToMany('Tags', [
  508. 'strategy' => 'subquery',
  509. ]);
  510. $table->Articles->belongsTo('Authors');
  511. $result = $table->Articles->find()
  512. ->where(['Authors.id >' => 1])
  513. ->contain(['Authors.Tags'])
  514. ->toArray();
  515. $this->assertEquals(
  516. ['tag1', 'tag2'],
  517. collection($result[0]->author->tags)->extract('name')->toArray()
  518. );
  519. $this->assertEquals(3, $result[0]->author->id);
  520. }
  521. /**
  522. * Tests that finding on a table with a primary key other than `id` will work
  523. * seamlessly with either select or subquery.
  524. *
  525. * @see https://github.com/cakephp/cakephp/issues/6781
  526. * @return void
  527. */
  528. public function testDeepHasManyEitherStrategy()
  529. {
  530. $tags = TableRegistry::get('Tags');
  531. $featuredTags = TableRegistry::get('FeaturedTags');
  532. $featuredTags->belongsTo('Tags');
  533. $tags->hasMany('TagsTranslations', [
  534. 'foreignKey' => 'id',
  535. 'strategy' => 'select'
  536. ]);
  537. $findViaSelect = $featuredTags
  538. ->find()
  539. ->where(['FeaturedTags.tag_id' => 2])
  540. ->contain('Tags.TagsTranslations');
  541. $tags->hasMany('TagsTranslations', [
  542. 'foreignKey' => 'id',
  543. 'strategy' => 'subquery'
  544. ]);
  545. $findViaSubquery = $featuredTags
  546. ->find()
  547. ->where(['FeaturedTags.tag_id' => 2])
  548. ->contain('Tags.TagsTranslations');
  549. $expected = [2 => 'tag 2 translated into en_us'];
  550. $this->assertEquals($expected, $findViaSelect->combine('tag_id', 'tag.tags_translations.0.name')->toArray());
  551. $this->assertEquals($expected, $findViaSubquery->combine('tag_id', 'tag.tags_translations.0.name')->toArray());
  552. }
  553. /**
  554. * Tests that getting the count of a query having containments return
  555. * the correct results
  556. *
  557. * @see https://github.com/cakephp/cakephp/issues/4511
  558. * @return void
  559. */
  560. public function testCountWithContain()
  561. {
  562. $table = TableRegistry::get('Articles');
  563. $table->belongsTo('Authors', ['joinType' => 'inner']);
  564. $count = $table
  565. ->find()
  566. ->contain(['Authors' => function ($q) {
  567. return $q->where(['Authors.id' => 1]);
  568. }])
  569. ->count();
  570. $this->assertEquals(2, $count);
  571. }
  572. /**
  573. * Test that deep containments don't generate empty entities for
  574. * intermediary relations.
  575. *
  576. * @return void
  577. */
  578. public function testContainNoEmptyAssociatedObjects()
  579. {
  580. $comments = TableRegistry::get('Comments');
  581. $comments->belongsTo('Users');
  582. $users = TableRegistry::get('Users');
  583. $users->hasMany('Articles', [
  584. 'foreignKey' => 'author_id'
  585. ]);
  586. $comments->updateAll(['user_id' => 99], ['id' => 1]);
  587. $result = $comments->find()
  588. ->contain(['Users'])
  589. ->where(['Comments.id' => 1])
  590. ->first();
  591. $this->assertNull($result->user, 'No record should be null.');
  592. $result = $comments->find()
  593. ->contain(['Users', 'Users.Articles'])
  594. ->where(['Comments.id' => 1])
  595. ->first();
  596. $this->assertNull($result->user, 'No record should be null.');
  597. }
  598. /**
  599. * Tests that using a comparison expression inside an OR condition works
  600. *
  601. * @see https://github.com/cakephp/cakephp/issues/5081
  602. * @return void
  603. */
  604. public function testOrConditionsWithExpression()
  605. {
  606. $table = TableRegistry::get('Articles');
  607. $query = $table->find();
  608. $query->where([
  609. 'OR' => [
  610. new \Cake\Database\Expression\Comparison('id', 1, 'integer', '>'),
  611. new \Cake\Database\Expression\Comparison('id', 3, 'integer', '<')
  612. ]
  613. ]);
  614. $results = $query->toArray();
  615. $this->assertCount(3, $results);
  616. }
  617. /**
  618. * Tests that calling count on a query having a union works correctly
  619. *
  620. * @see https://github.com/cakephp/cakephp/issues/5107
  621. * @return void
  622. */
  623. public function testCountWithUnionQuery()
  624. {
  625. $table = TableRegistry::get('Articles');
  626. $query = $table->find()->where(['id' => 1]);
  627. $query2 = $table->find()->where(['id' => 2]);
  628. $query->union($query2);
  629. $this->assertEquals(2, $query->count());
  630. }
  631. /**
  632. * Integration test when selecting no fields on the primary table.
  633. *
  634. * @return void
  635. */
  636. public function testSelectNoFieldsOnPrimaryAlias()
  637. {
  638. $table = TableRegistry::get('Articles');
  639. $table->belongsTo('Users');
  640. $query = $table->find()
  641. ->select(['Users__id' => 'id']);
  642. $results = $query->toArray();
  643. $this->assertCount(3, $results);
  644. }
  645. /**
  646. * Tests that calling first on the query results will not remove all other results
  647. * from the set.
  648. *
  649. * @return void
  650. */
  651. public function testFirstOnResultSet()
  652. {
  653. $results = TableRegistry::get('Articles')->find()->all();
  654. $this->assertEquals(3, $results->count());
  655. $this->assertNotNull($results->first());
  656. $this->assertCount(3, $results->toArray());
  657. }
  658. /**
  659. * Checks that matching and contain can be called for the same belongsTo association
  660. *
  661. * @see https://github.com/cakephp/cakephp/issues/5463
  662. * @return void
  663. */
  664. public function testFindMatchingAndContain()
  665. {
  666. $table = TableRegistry::get('Articles');
  667. $table->belongsTo('Authors');
  668. $article = $table->find()
  669. ->contain('Authors')
  670. ->matching('Authors', function ($q) {
  671. return $q->where(['Authors.id' => 1]);
  672. })
  673. ->first();
  674. $this->assertNotNull($article->author);
  675. $this->assertEquals($article->author, $article->_matchingData['Authors']);
  676. }
  677. /**
  678. * Checks that matching and contain can be called for the same belongsTo association
  679. *
  680. * @see https://github.com/cakephp/cakephp/issues/5463
  681. * @return void
  682. */
  683. public function testFindMatchingAndContainWithSubquery()
  684. {
  685. $table = TableRegistry::get('authors');
  686. $table->hasMany('articles', ['strategy' => 'subquery']);
  687. $table->articles->belongsToMany('tags');
  688. $result = $table->find()
  689. ->matching('articles.tags', function ($q) {
  690. return $q->where(['tags.id' => 2]);
  691. })
  692. ->contain('articles');
  693. $this->assertCount(2, $result->first()->articles);
  694. }
  695. /**
  696. * Tests that matching does not overwrite associations in contain
  697. *
  698. * @see https://github.com/cakephp/cakephp/issues/5584
  699. * @return void
  700. */
  701. public function testFindMatchingOverwrite()
  702. {
  703. $comments = TableRegistry::get('Comments');
  704. $comments->belongsTo('Articles');
  705. $articles = TableRegistry::get('Articles');
  706. $articles->belongsToMany('Tags');
  707. $result = $comments
  708. ->find()
  709. ->matching('Articles.Tags', function ($q) {
  710. return $q->where(['Tags.id' => 2]);
  711. })
  712. ->contain('Articles')
  713. ->first();
  714. $this->assertEquals(1, $result->id);
  715. $this->assertEquals(1, $result->_matchingData['Articles']->id);
  716. $this->assertEquals(2, $result->_matchingData['Tags']->id);
  717. $this->assertNotNull($result->article);
  718. $this->assertEquals($result->article, $result->_matchingData['Articles']);
  719. }
  720. /**
  721. * Tests that matching does not overwrite associations in contain
  722. *
  723. * @see https://github.com/cakephp/cakephp/issues/5584
  724. * @return void
  725. */
  726. public function testFindMatchingOverwrite2()
  727. {
  728. $comments = TableRegistry::get('Comments');
  729. $comments->belongsTo('Articles');
  730. $articles = TableRegistry::get('Articles');
  731. $articles->belongsTo('Authors');
  732. $articles->belongsToMany('Tags');
  733. $result = $comments
  734. ->find()
  735. ->matching('Articles.Tags', function ($q) {
  736. return $q->where(['Tags.id' => 2]);
  737. })
  738. ->contain('Articles.Authors')
  739. ->first();
  740. $this->assertNotNull($result->article->author);
  741. }
  742. /**
  743. * Tests that trying to contain an inexistent association
  744. * throws an exception and not a fatal error.
  745. *
  746. * @expectedException InvalidArgumentException
  747. * @return void
  748. */
  749. public function testQueryNotFatalError()
  750. {
  751. $comments = TableRegistry::get('Comments');
  752. $comments->find()->contain('Deprs')->all();
  753. }
  754. /**
  755. * Tests that using matching and contain on belongsTo associations
  756. * works correctly.
  757. *
  758. * @see https://github.com/cakephp/cakephp/issues/5721
  759. * @return void
  760. */
  761. public function testFindMatchingWithContain()
  762. {
  763. $comments = TableRegistry::get('Comments');
  764. $comments->belongsTo('Articles');
  765. $comments->belongsTo('Users');
  766. $result = $comments->find()
  767. ->contain(['Articles', 'Users'])
  768. ->matching('Articles', function ($q) {
  769. return $q->where(['Articles.id >=' => 1]);
  770. })
  771. ->matching('Users', function ($q) {
  772. return $q->where(['Users.id >=' => 1]);
  773. })
  774. ->order(['Comments.id' => 'ASC'])
  775. ->first();
  776. $this->assertInstanceOf('Cake\ORM\Entity', $result->article);
  777. $this->assertInstanceOf('Cake\ORM\Entity', $result->user);
  778. $this->assertEquals(2, $result->user->id);
  779. $this->assertEquals(1, $result->article->id);
  780. }
  781. /**
  782. * Tests that HasMany associations don't use duplicate PK values.
  783. *
  784. * @return void
  785. */
  786. public function testHasManyEagerLoadingUniqueKey()
  787. {
  788. $table = TableRegistry::get('ArticlesTags');
  789. $table->belongsTo('Articles', [
  790. 'strategy' => 'select'
  791. ]);
  792. $result = $table->find()
  793. ->contain(['Articles' => function ($q) {
  794. $result = $q->sql();
  795. $this->assertNotContains(':c2', $result, 'Only 2 bindings as there are only 2 rows.');
  796. $this->assertNotContains(':c3', $result, 'Only 2 bindings as there are only 2 rows.');
  797. return $q;
  798. }])
  799. ->toArray();
  800. $this->assertNotEmpty($result[0]->article);
  801. }
  802. /**
  803. * Tests that using contain but selecting no fields from the association
  804. * does not trigger any errors and fetches the right results.
  805. *
  806. * @see https://github.com/cakephp/cakephp/issues/6214
  807. * @return void
  808. */
  809. public function testContainWithNoFields()
  810. {
  811. $table = TableRegistry::get('Comments');
  812. $table->belongsTo('Users');
  813. $results = $table->find()
  814. ->select(['Comments.id', 'Comments.user_id'])
  815. ->contain(['Users'])
  816. ->where(['Users.id' => 1])
  817. ->combine('id', 'user_id');
  818. $this->assertEquals([3 => 1, 4 => 1, 5 => 1], $results->toArray());
  819. }
  820. /**
  821. * Tests that using matching and selecting no fields for that association
  822. * will no trigger any errors and fetch the right results
  823. *
  824. * @see https://github.com/cakephp/cakephp/issues/6223
  825. * @return void
  826. */
  827. public function testMatchingWithNoFields()
  828. {
  829. $table = TableRegistry::get('Users');
  830. $table->hasMany('Comments');
  831. $results = $table->find()
  832. ->select(['Users.id'])
  833. ->matching('Comments', function ($q) {
  834. return $q->where(['Comments.id' => 1]);
  835. })
  836. ->extract('id')
  837. ->toList();
  838. $this->assertEquals([2], $results);
  839. }
  840. /**
  841. * Test that empty conditions in a matching clause don't cause errors.
  842. *
  843. * @return void
  844. */
  845. public function testMatchingEmptyQuery()
  846. {
  847. $table = TableRegistry::get('Articles');
  848. $table->belongsToMany('Tags');
  849. $rows = $table->find()
  850. ->matching('Tags', function ($q) {
  851. return $q->where([]);
  852. })
  853. ->all();
  854. $this->assertNotEmpty($rows);
  855. $rows = $table->find()
  856. ->matching('Tags', function ($q) {
  857. return $q->where(null);
  858. })
  859. ->all();
  860. $this->assertNotEmpty($rows);
  861. }
  862. /**
  863. * Tests that using a subquery as part of an expression will not make invalid SQL
  864. *
  865. * @return void
  866. */
  867. public function testSubqueryInSelectExpression()
  868. {
  869. $table = TableRegistry::get('Comments');
  870. $ratio = $table->find()
  871. ->select(function ($query) use ($table) {
  872. $allCommentsCount = $table->find()->select($query->func()->count('*'));
  873. $countToFloat = $query->newExpr([$query->func()->count('*'), '1.0'])->type('*');
  874. return [
  875. 'ratio' => $query
  876. ->newExpr($countToFloat)
  877. ->add($allCommentsCount)
  878. ->type('/')
  879. ];
  880. })
  881. ->where(['user_id' => 1])
  882. ->first()
  883. ->ratio;
  884. $this->assertEquals(0.5, $ratio);
  885. }
  886. /**
  887. * Tests calling last on an empty table
  888. *
  889. * @see https://github.com/cakephp/cakephp/issues/6683
  890. * @return void
  891. */
  892. public function testFindLastOnEmptyTable()
  893. {
  894. $table = TableRegistry::get('Comments');
  895. $table->deleteAll(['1 = 1']);
  896. $this->assertEquals(0, $table->find()->count());
  897. $this->assertNull($table->find()->last());
  898. }
  899. /**
  900. * Tests calling contain in a nested closure
  901. *
  902. * @see https://github.com/cakephp/cakephp/issues/7591
  903. * @return void
  904. */
  905. public function testContainInNestedClosure()
  906. {
  907. $table = TableRegistry::get('Comments');
  908. $table->belongsTo('Articles');
  909. $table->Articles->belongsTo('Authors');
  910. $table->Articles->Authors->belongsToMany('Tags');
  911. $query = $table->find()->where(['Comments.id' => 5])->contain(['Articles' => function ($q) {
  912. return $q->contain(['Authors' => function ($q) {
  913. return $q->contain('Tags');
  914. }]);
  915. }]);
  916. $this->assertCount(2, $query->first()->article->author->tags);
  917. }
  918. /**
  919. * Test that the typemaps used in function expressions
  920. * create the correct results.
  921. *
  922. * @return void
  923. */
  924. public function testTypemapInFunctions()
  925. {
  926. $table = TableRegistry::get('Comments');
  927. $table->updateAll(['published' => null], ['1 = 1']);
  928. $query = $table->find();
  929. $query->select([
  930. 'id',
  931. 'coalesced' => $query->func()->coalesce(
  932. ['published' => 'literal', -1],
  933. ['integer']
  934. )
  935. ]);
  936. $result = $query->all()->first();
  937. $this->assertSame(
  938. '-1',
  939. $result['coalesced'],
  940. 'Output values for functions are not cast yet.'
  941. );
  942. }
  943. /**
  944. * Test that contain queries map types correctly.
  945. *
  946. * @return void
  947. */
  948. public function testBooleanConditionsInContain()
  949. {
  950. $table = TableRegistry::get('Articles');
  951. $table->belongsToMany('Tags', [
  952. 'foreignKey' => 'article_id',
  953. 'associationForeignKey' => 'tag_id',
  954. 'through' => 'SpecialTags'
  955. ]);
  956. $query = $table->find()
  957. ->contain(['Tags' => function ($q) {
  958. return $q->where(['SpecialTags.highlighted_time >' => new Time('2014-06-01 00:00:00')]);
  959. }])
  960. ->where(['Articles.id' => 2]);
  961. $result = $query->first();
  962. $this->assertEquals(2, $result->id);
  963. $this->assertNotEmpty($result->tags, 'Missing tags');
  964. $this->assertNotEmpty($result->tags[0]->_joinData, 'Missing join data');
  965. }
  966. /**
  967. * Test that contain queries map types correctly.
  968. *
  969. * @return void
  970. */
  971. public function testComplexTypesInJoinedWhere()
  972. {
  973. $table = TableRegistry::get('Users');
  974. $table->hasOne('Comments', [
  975. 'foreignKey' => 'user_id',
  976. ]);
  977. $query = $table->find()
  978. ->contain('Comments')
  979. ->where([
  980. 'Comments.updated >' => new \DateTime('2007-03-18 10:55:00')
  981. ]);
  982. $result = $query->first();
  983. $this->assertNotEmpty($result);
  984. $this->assertInstanceOf('Cake\I18n\Time', $result->comment->updated);
  985. }
  986. /**
  987. * Test that nested contain queries map types correctly.
  988. *
  989. * @return void
  990. */
  991. public function testComplexNestedTypesInJoinedWhere()
  992. {
  993. $table = TableRegistry::get('Users');
  994. $table->hasOne('Comments', [
  995. 'foreignKey' => 'user_id',
  996. ]);
  997. $table->Comments->belongsTo('Articles');
  998. $table->Comments->Articles->belongsTo('Authors', [
  999. 'className' => 'Users',
  1000. 'foreignKey' => 'author_id'
  1001. ]);
  1002. $query = $table->find()
  1003. ->contain('Comments.Articles.Authors')
  1004. ->where([
  1005. 'Authors.created >' => new \DateTime('2007-03-17 01:16:00')
  1006. ]);
  1007. $result = $query->first();
  1008. $this->assertNotEmpty($result);
  1009. $this->assertInstanceOf('Cake\I18n\Time', $result->comment->article->author->updated);
  1010. }
  1011. /**
  1012. * Tests that it is possible to use matching with dot notation
  1013. * even when part of the part of the path in the dot notation is
  1014. * shared for two different calls
  1015. *
  1016. * @return void
  1017. */
  1018. public function testDotNotationNotOverride()
  1019. {
  1020. $table = TableRegistry::get('Comments');
  1021. $articles = $table->belongsTo('Articles');
  1022. $specialTags = $articles->hasMany('SpecialTags');
  1023. $specialTags->belongsTo('Authors');
  1024. $specialTags->belongsTo('Tags');
  1025. $results = $table
  1026. ->find()
  1027. ->select(['name' => 'Authors.name', 'tag' => 'Tags.name'])
  1028. ->matching('Articles.SpecialTags.Tags')
  1029. ->matching('Articles.SpecialTags.Authors', function ($q) {
  1030. return $q->where(['Authors.id' => 2]);
  1031. })
  1032. ->distinct()
  1033. ->hydrate(false)
  1034. ->toArray();
  1035. $this->assertEquals([['name' => 'nate', 'tag' => 'tag1']], $results);
  1036. }
  1037. /**
  1038. * Test expression based ordering with unions.
  1039. *
  1040. * @return void
  1041. */
  1042. public function testComplexOrderWithUnion()
  1043. {
  1044. $table = TableRegistry::get('Comments');
  1045. $query = $table->find();
  1046. $inner = $table->find()
  1047. ->select(['content' => 'comment'])
  1048. ->where(['id >' => 3]);
  1049. $inner2 = $table->find()
  1050. ->select(['content' => 'comment'])
  1051. ->where(['id <' => 3]);
  1052. $order = $query->func()->concat(['content' => 'literal', 'test']);
  1053. $query->select(['inside.content'])
  1054. ->from(['inside' => $inner->unionAll($inner2)])
  1055. ->orderAsc($order);
  1056. $results = $query->toArray();
  1057. $this->assertCount(5, $results);
  1058. }
  1059. /**
  1060. * Test that associations that are loaded with subqueries
  1061. * do not cause errors when the subquery has a limit & order clause.
  1062. *
  1063. * @return void
  1064. */
  1065. public function testEagerLoadOrderAndSubquery()
  1066. {
  1067. $table = TableRegistry::get('Articles');
  1068. $table->hasMany('Comments', [
  1069. 'strategy' => 'subquery'
  1070. ]);
  1071. $query = $table->find()
  1072. ->select(['score' => 100])
  1073. ->autoFields(true)
  1074. ->contain(['Comments'])
  1075. ->limit(5)
  1076. ->order(['score' => 'desc']);
  1077. $result = $query->all();
  1078. $this->assertCount(3, $result);
  1079. }
  1080. /**
  1081. * Tests that decorating the results does not result in a memory leak
  1082. *
  1083. * @return void
  1084. */
  1085. public function testFormatResultsMemory()
  1086. {
  1087. $table = TableRegistry::get('Articles');
  1088. $table->belongsTo('Authors');
  1089. $table->belongsToMany('Tags');
  1090. gc_collect_cycles();
  1091. $memory = memory_get_usage() / 1024 / 1024;
  1092. foreach (range(1, 3) as $time) {
  1093. $table->find()
  1094. ->contain(['Authors', 'Tags'])
  1095. ->formatResults(function ($results) {
  1096. return $results;
  1097. })
  1098. ->all();
  1099. }
  1100. gc_collect_cycles();
  1101. $endMemory = memory_get_usage() / 1024 / 1024;
  1102. $this->assertWithinRange($endMemory, $memory, 1.25, 'Memory leak in ResultSet');
  1103. }
  1104. }