QueryRegressionTest.php 40 KB

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