QueryRegressionTest.php 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\ORM;
  17. use Cake\Database\Expression\ComparisonExpression;
  18. use Cake\Database\Expression\QueryExpression;
  19. use Cake\Datasource\EntityInterface;
  20. use Cake\Event\EventInterface;
  21. use Cake\I18n\FrozenTime;
  22. use Cake\I18n\Time;
  23. use Cake\ORM\Query;
  24. use Cake\TestSuite\TestCase;
  25. /**
  26. * Contains regression test for the Query builder
  27. */
  28. class QueryRegressionTest extends TestCase
  29. {
  30. /**
  31. * Fixture to be used
  32. *
  33. * @var array
  34. */
  35. protected $fixtures = [
  36. 'core.Articles',
  37. 'core.Tags',
  38. 'core.ArticlesTags',
  39. 'core.Authors',
  40. 'core.AuthorsTags',
  41. 'core.Comments',
  42. 'core.FeaturedTags',
  43. 'core.SpecialTags',
  44. 'core.TagsTranslations',
  45. 'core.Translates',
  46. 'core.Users',
  47. ];
  48. public $autoFixtures = false;
  49. /**
  50. * Test for https://github.com/cakephp/cakephp/issues/3087
  51. *
  52. * @return void
  53. */
  54. public function testSelectTimestampColumn()
  55. {
  56. $this->loadFixtures('Users');
  57. $table = $this->getTableLocator()->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. {
  70. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags');
  71. $table = $this->getTableLocator()->get('Articles');
  72. $table->belongsToMany('ArticlesTags');
  73. $results = $table->find()->where(['id >' => 100])->contain('ArticlesTags')->toArray();
  74. $this->assertEmpty($results);
  75. }
  76. /**
  77. * Tests that eagerloading associations with aliased fields works.
  78. *
  79. * @return void
  80. */
  81. public function testEagerLoadingAliasedAssociationFields()
  82. {
  83. $this->loadFixtures('Articles', 'Authors');
  84. $table = $this->getTableLocator()->get('Articles');
  85. $table->belongsTo('Authors', [
  86. 'foreignKey' => 'author_id',
  87. ]);
  88. $result = $table->find()
  89. ->contain(['Authors' => [
  90. 'fields' => [
  91. 'id',
  92. 'Authors__aliased_name' => 'name',
  93. ],
  94. ]])
  95. ->where(['Articles.id' => 1])
  96. ->first();
  97. $this->assertInstanceOf(EntityInterface::class, $result);
  98. $this->assertInstanceOf(EntityInterface::class, $result->author);
  99. $this->assertSame('mariano', $result->author->aliased_name);
  100. }
  101. /**
  102. * Tests that eagerloading and hydration works for associations that have
  103. * different aliases in the association and targetTable
  104. *
  105. * @return void
  106. */
  107. public function testEagerLoadingMismatchingAliasInBelongsTo()
  108. {
  109. $this->loadFixtures('Articles', 'Users');
  110. $table = $this->getTableLocator()->get('Articles');
  111. $users = $this->getTableLocator()->get('Users');
  112. $table->belongsTo('Authors', [
  113. 'targetTable' => $users,
  114. 'foreignKey' => 'author_id',
  115. ]);
  116. $result = $table->find()->where(['Articles.id' => 1])->contain('Authors')->first();
  117. $this->assertInstanceOf(EntityInterface::class, $result);
  118. $this->assertInstanceOf(EntityInterface::class, $result->author);
  119. $this->assertSame('mariano', $result->author->username);
  120. }
  121. /**
  122. * Tests that eagerloading and hydration works for associations that have
  123. * different aliases in the association and targetTable
  124. *
  125. * @return void
  126. */
  127. public function testEagerLoadingMismatchingAliasInHasOne()
  128. {
  129. $this->loadFixtures('Articles', 'Users');
  130. $articles = $this->getTableLocator()->get('Articles');
  131. $users = $this->getTableLocator()->get('Users');
  132. $users->hasOne('Posts', [
  133. 'targetTable' => $articles,
  134. 'foreignKey' => 'author_id',
  135. ]);
  136. $result = $users->find()->where(['Users.id' => 1])->contain('Posts')->first();
  137. $this->assertInstanceOf(EntityInterface::class, $result);
  138. $this->assertInstanceOf(EntityInterface::class, $result->post);
  139. $this->assertSame('First Article', $result->post->title);
  140. }
  141. /**
  142. * Tests that eagerloading belongsToMany with find list fails with a helpful message.
  143. *
  144. * @return void
  145. */
  146. public function testEagerLoadingBelongsToManyList()
  147. {
  148. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags');
  149. $table = $this->getTableLocator()->get('Articles');
  150. $table->belongsToMany('Tags', [
  151. 'finder' => 'list',
  152. ]);
  153. $this->expectException(\RuntimeException::class);
  154. $this->expectExceptionMessage('"_joinData" is missing from the belongsToMany results');
  155. $table->find()->contain('Tags')->toArray();
  156. }
  157. /**
  158. * Tests that eagerloading and hydration works for associations that have
  159. * different aliases in the association and targetTable
  160. *
  161. * @return void
  162. */
  163. public function testEagerLoadingNestedMatchingCalls()
  164. {
  165. $this->loadFixtures('Articles', 'Authors', 'Tags', 'ArticlesTags', 'AuthorsTags');
  166. $articles = $this->getTableLocator()->get('Articles');
  167. $articles->belongsToMany('Tags', [
  168. 'foreignKey' => 'article_id',
  169. 'targetForeignKey' => 'tag_id',
  170. 'joinTable' => 'articles_tags',
  171. ]);
  172. $tags = $this->getTableLocator()->get('Tags');
  173. $tags->belongsToMany('Authors', [
  174. 'foreignKey' => 'tag_id',
  175. 'targetForeignKey' => 'author_id',
  176. 'joinTable' => 'authors_tags',
  177. ]);
  178. $query = $articles->find()
  179. ->matching('Tags', function ($q) {
  180. return $q->matching('Authors', function ($q) {
  181. return $q->where(['Authors.name' => 'larry']);
  182. });
  183. });
  184. $this->assertEquals(3, $query->count());
  185. $result = $query->first();
  186. $this->assertInstanceOf(EntityInterface::class, $result);
  187. $this->assertInstanceOf(EntityInterface::class, $result->_matchingData['Tags']);
  188. $this->assertInstanceOf(EntityInterface::class, $result->_matchingData['Authors']);
  189. }
  190. /**
  191. * Tests that duplicate aliases in contain() can be used, even when they would
  192. * naturally be attached to the query instead of eagerly loaded. What should
  193. * happen here is that One of the duplicates will be changed to be loaded using
  194. * an extra query, but yielding the same results
  195. *
  196. * @return void
  197. */
  198. public function testDuplicateAttachableAliases()
  199. {
  200. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags', 'Authors');
  201. $this->getTableLocator()->get('Stuff', ['table' => 'tags']);
  202. $this->getTableLocator()->get('Things', ['table' => 'articles_tags']);
  203. $table = $this->getTableLocator()->get('Articles');
  204. $table->belongsTo('Authors');
  205. $table->hasOne('Things', ['propertyName' => 'articles_tag']);
  206. $table->Authors->getTarget()->hasOne('Stuff', [
  207. 'foreignKey' => 'id',
  208. 'propertyName' => 'favorite_tag',
  209. ]);
  210. $table->Things->getTarget()->belongsTo('Stuff', [
  211. 'foreignKey' => 'tag_id',
  212. 'propertyName' => 'foo',
  213. ]);
  214. $results = $table->find()
  215. ->contain(['Authors.Stuff', 'Things.Stuff'])
  216. ->order(['Articles.id' => 'ASC'])
  217. ->toArray();
  218. $this->assertCount(5, $results);
  219. $this->assertEquals(1, $results[0]->articles_tag->foo->id);
  220. $this->assertEquals(1, $results[0]->author->favorite_tag->id);
  221. $this->assertEquals(2, $results[1]->articles_tag->foo->id);
  222. $this->assertEquals(1, $results[2]->articles_tag->foo->id);
  223. $this->assertEquals(3, $results[2]->author->favorite_tag->id);
  224. $this->assertEquals(3, $results[3]->articles_tag->foo->id);
  225. $this->assertEquals(3, $results[3]->author->favorite_tag->id);
  226. }
  227. /**
  228. * Test for https://github.com/cakephp/cakephp/issues/3410
  229. *
  230. * @return void
  231. */
  232. public function testNullableTimeColumn()
  233. {
  234. $this->loadFixtures('Users');
  235. $table = $this->getTableLocator()->get('users');
  236. $entity = $table->newEntity(['username' => 'derp', 'created' => null]);
  237. $this->assertSame($entity, $table->save($entity));
  238. $this->assertNull($entity->created);
  239. }
  240. /**
  241. * Test for https://github.com/cakephp/cakephp/issues/3626
  242. *
  243. * Checks that join data is actually created and not tried to be updated every time
  244. *
  245. * @return void
  246. */
  247. public function testCreateJointData()
  248. {
  249. $this->loadFixtures('Articles', 'Tags', 'SpecialTags');
  250. $articles = $this->getTableLocator()->get('Articles');
  251. $articles->belongsToMany('Highlights', [
  252. 'className' => 'TestApp\Model\Table\TagsTable',
  253. 'foreignKey' => 'article_id',
  254. 'targetForeignKey' => 'tag_id',
  255. 'through' => 'SpecialTags',
  256. ]);
  257. $entity = $articles->get(2);
  258. $data = [
  259. 'id' => 2,
  260. 'highlights' => [
  261. [
  262. 'name' => 'New Special Tag',
  263. '_joinData' => ['highlighted' => true, 'highlighted_time' => '2014-06-01 10:10:00'],
  264. ],
  265. ],
  266. ];
  267. $entity = $articles->patchEntity($entity, $data, ['Highlights._joinData']);
  268. $articles->save($entity);
  269. $entity = $articles->get(2, ['contain' => ['Highlights']]);
  270. $this->assertEquals(4, $entity->highlights[0]->_joinData->tag_id);
  271. $this->assertSame('2014-06-01', $entity->highlights[0]->_joinData->highlighted_time->format('Y-m-d'));
  272. }
  273. /**
  274. * Tests that the junction table instance taken from both sides of a belongsToMany
  275. * relationship is actually the same object.
  276. *
  277. * @return void
  278. */
  279. public function testReciprocalBelongsToMany()
  280. {
  281. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags');
  282. $articles = $this->getTableLocator()->get('Articles');
  283. $tags = $this->getTableLocator()->get('Tags');
  284. $articles->belongsToMany('Tags');
  285. $tags->belongsToMany('Articles');
  286. $left = $articles->Tags->junction();
  287. $right = $tags->Articles->junction();
  288. $this->assertSame($left, $right);
  289. }
  290. /**
  291. * Test for https://github.com/cakephp/cakephp/issues/4253
  292. *
  293. * Makes sure that the belongsToMany association is not overwritten with conflicting information
  294. * by any of the sides when the junction() function is invoked
  295. *
  296. * @return void
  297. */
  298. public function testReciprocalBelongsToManyNoOverwrite()
  299. {
  300. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags');
  301. $articles = $this->getTableLocator()->get('Articles');
  302. $tags = $this->getTableLocator()->get('Tags');
  303. $articles->belongsToMany('Tags');
  304. $tags->belongsToMany('Articles');
  305. $sub = $articles->Tags->find()->select(['Tags.id'])->matching('Articles', function ($q) {
  306. return $q->where(['Articles.id' => 1]);
  307. });
  308. $query = $articles->Tags->find()->where(['Tags.id NOT IN' => $sub]);
  309. $this->assertEquals(1, $query->count());
  310. }
  311. /**
  312. * Returns an array with the saving strategies for a belongsTo association
  313. *
  314. * @return array
  315. */
  316. public function strategyProvider()
  317. {
  318. return [
  319. ['append'],
  320. ['replace'],
  321. ];
  322. }
  323. /**
  324. * Test for https://github.com/cakephp/cakephp/issues/3677 and
  325. * https://github.com/cakephp/cakephp/issues/3714
  326. *
  327. * Checks that only relevant associations are passed when saving _joinData
  328. * Tests that _joinData can also save deeper associations
  329. *
  330. * @dataProvider strategyProvider
  331. * @param string $strategy
  332. * @return void
  333. */
  334. public function testBelongsToManyDeepSave($strategy)
  335. {
  336. $this->loadFixtures('Articles', 'Tags', 'SpecialTags', 'Authors');
  337. $articles = $this->getTableLocator()->get('Articles');
  338. $articles->belongsToMany('Highlights', [
  339. 'className' => 'TestApp\Model\Table\TagsTable',
  340. 'foreignKey' => 'article_id',
  341. 'targetForeignKey' => 'tag_id',
  342. 'through' => 'SpecialTags',
  343. 'saveStrategy' => $strategy,
  344. ]);
  345. $articles->Highlights->junction()->belongsTo('Authors');
  346. $articles->Highlights->hasOne('Authors', [
  347. 'foreignKey' => 'id',
  348. ]);
  349. $entity = $articles->get(2, ['contain' => ['Highlights']]);
  350. $data = [
  351. 'highlights' => [
  352. [
  353. 'name' => 'New Special Tag',
  354. '_joinData' => [
  355. 'highlighted' => true,
  356. 'highlighted_time' => '2014-06-01 10:10:00',
  357. 'author' => [
  358. 'name' => 'mariano',
  359. ],
  360. ],
  361. 'author' => ['name' => 'mark'],
  362. ],
  363. ],
  364. ];
  365. $options = [
  366. 'associated' => [
  367. 'Highlights._joinData.Authors', 'Highlights.Authors',
  368. ],
  369. ];
  370. $entity = $articles->patchEntity($entity, $data, $options);
  371. $articles->save($entity, $options);
  372. $entity = $articles->get(2, [
  373. 'contain' => [
  374. 'SpecialTags' => ['sort' => ['SpecialTags.id' => 'ASC']],
  375. 'SpecialTags.Authors',
  376. 'Highlights.Authors',
  377. ],
  378. ]);
  379. $this->assertSame('mark', end($entity->highlights)->author->name);
  380. $lastTag = end($entity->special_tags);
  381. $this->assertTrue($lastTag->highlighted);
  382. $this->assertSame('2014-06-01 10:10:00', $lastTag->highlighted_time->format('Y-m-d H:i:s'));
  383. $this->assertSame('mariano', $lastTag->author->name);
  384. }
  385. /**
  386. * Tests that no exceptions are generated because of ambiguous column names in queries
  387. * during a save operation
  388. *
  389. * @see https://github.com/cakephp/cakephp/issues/3803
  390. * @return void
  391. */
  392. public function testSaveWithCallbacks()
  393. {
  394. $this->loadFixtures('Articles', 'Authors');
  395. $articles = $this->getTableLocator()->get('Articles');
  396. $articles->belongsTo('Authors');
  397. $articles->getEventManager()->on('Model.beforeFind', function (EventInterface $event, $query) {
  398. return $query->contain('Authors');
  399. });
  400. $article = $articles->newEmptyEntity();
  401. $article->title = 'Foo';
  402. $article->body = 'Bar';
  403. $this->assertSame($article, $articles->save($article));
  404. }
  405. /**
  406. * Test that save() works with entities containing expressions
  407. * as properties.
  408. *
  409. * @return void
  410. */
  411. public function testSaveWithExpressionProperty()
  412. {
  413. $this->loadFixtures('Articles');
  414. $articles = $this->getTableLocator()->get('Articles');
  415. $article = $articles->newEmptyEntity();
  416. $article->title = new QueryExpression("SELECT 'jose'");
  417. $this->assertSame($article, $articles->save($article));
  418. }
  419. /**
  420. * Tests that whe saving deep associations for a belongsToMany property,
  421. * data is not removed because of excessive associations filtering.
  422. *
  423. * @see https://github.com/cakephp/cakephp/issues/4009
  424. * @return void
  425. */
  426. public function testBelongsToManyDeepSave2()
  427. {
  428. $this->loadFixtures('Articles', 'Tags', 'SpecialTags');
  429. $articles = $this->getTableLocator()->get('Articles');
  430. $articles->belongsToMany('Highlights', [
  431. 'className' => 'TestApp\Model\Table\TagsTable',
  432. 'foreignKey' => 'article_id',
  433. 'targetForeignKey' => 'tag_id',
  434. 'through' => 'SpecialTags',
  435. ]);
  436. $articles->Highlights->hasMany('TopArticles', [
  437. 'className' => 'TestApp\Model\Table\ArticlesTable',
  438. 'foreignKey' => 'author_id',
  439. ]);
  440. $entity = $articles->get(2, ['contain' => ['Highlights']]);
  441. $data = [
  442. 'highlights' => [
  443. [
  444. 'name' => 'New Special Tag',
  445. '_joinData' => [
  446. 'highlighted' => true,
  447. 'highlighted_time' => '2014-06-01 10:10:00',
  448. ],
  449. 'top_articles' => [
  450. ['title' => 'First top article'],
  451. ['title' => 'Second top article'],
  452. ],
  453. ],
  454. ],
  455. ];
  456. $options = [
  457. 'associated' => [
  458. 'Highlights._joinData', 'Highlights.TopArticles',
  459. ],
  460. ];
  461. $entity = $articles->patchEntity($entity, $data, $options);
  462. $articles->save($entity, $options);
  463. $entity = $articles->get(2, [
  464. 'contain' => [
  465. 'Highlights.TopArticles',
  466. ],
  467. ]);
  468. $highlights = $entity->highlights[0];
  469. $this->assertSame('First top article', $highlights->top_articles[0]->title);
  470. $this->assertSame('Second top article', $highlights->top_articles[1]->title);
  471. $this->assertEquals(
  472. new Time('2014-06-01 10:10:00'),
  473. $highlights->_joinData->highlighted_time
  474. );
  475. }
  476. /**
  477. * An integration test that spot checks that associations use the
  478. * correct alias names to generate queries.
  479. *
  480. * @return void
  481. */
  482. public function testPluginAssociationQueryGeneration()
  483. {
  484. $this->loadFixtures('Articles', 'Comments', 'Authors');
  485. $this->loadPlugins(['TestPlugin']);
  486. $articles = $this->getTableLocator()->get('Articles');
  487. $articles->hasMany('TestPlugin.Comments');
  488. $articles->belongsTo('TestPlugin.Authors');
  489. $result = $articles->find()
  490. ->where(['Articles.id' => 2])
  491. ->contain(['Comments', 'Authors'])
  492. ->first();
  493. $this->assertNotEmpty(
  494. $result->comments[0]->id,
  495. 'No SQL error and comment exists.'
  496. );
  497. $this->assertNotEmpty(
  498. $result->author->id,
  499. 'No SQL error and author exists.'
  500. );
  501. $this->clearPlugins();
  502. }
  503. /**
  504. * Tests that loading associations having the same alias in the
  505. * joinable associations chain is not sensitive to the order in which
  506. * the associations are selected.
  507. *
  508. * @see https://github.com/cakephp/cakephp/issues/4454
  509. * @return void
  510. */
  511. public function testAssociationChainOrder()
  512. {
  513. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags', 'Authors');
  514. $articles = $this->getTableLocator()->get('Articles');
  515. $articles->belongsTo('Authors');
  516. $articles->hasOne('ArticlesTags');
  517. $articlesTags = $this->getTableLocator()->get('ArticlesTags');
  518. $articlesTags->belongsTo('Authors', [
  519. 'foreignKey' => 'tag_id',
  520. ]);
  521. $resultA = $articles->find()
  522. ->contain(['ArticlesTags.Authors', 'Authors'])
  523. ->first();
  524. $resultB = $articles->find()
  525. ->contain(['Authors', 'ArticlesTags.Authors'])
  526. ->first();
  527. $this->assertEquals($resultA, $resultB);
  528. $this->assertNotEmpty($resultA->author);
  529. $this->assertNotEmpty($resultA->articles_tag->author);
  530. }
  531. /**
  532. * Test that offset/limit are elided from subquery loads.
  533. *
  534. * @return void
  535. */
  536. public function testAssociationSubQueryNoOffset()
  537. {
  538. $this->loadFixtures('Articles', 'Translates');
  539. $table = $this->getTableLocator()->get('Articles');
  540. $table->addBehavior('Translate', ['fields' => ['title', 'body']]);
  541. $table->setLocale('eng');
  542. $query = $table->find('translations')
  543. ->order(['Articles.id' => 'ASC'])
  544. ->limit(10)
  545. ->offset(1);
  546. $result = $query->toArray();
  547. $this->assertCount(2, $result);
  548. }
  549. /**
  550. * Tests that using the subquery strategy in a deep association returns the right results
  551. *
  552. * @see https://github.com/cakephp/cakephp/issues/4484
  553. * @return void
  554. */
  555. public function testDeepBelongsToManySubqueryStrategy()
  556. {
  557. $this->loadFixtures('Authors', 'Tags', 'Articles', 'ArticlesTags');
  558. $table = $this->getTableLocator()->get('Authors');
  559. $table->hasMany('Articles');
  560. $table->Articles->belongsToMany('Tags', [
  561. 'strategy' => 'subquery',
  562. ]);
  563. $result = $table->find()->contain(['Articles.Tags'])->toArray();
  564. $this->skipIf(count($result) === 0, 'No results, this test sometimes acts up on PHP 5.6');
  565. $this->assertEquals(
  566. ['tag1', 'tag3'],
  567. collection($result[2]->articles[0]->tags)->extract('name')->toArray()
  568. );
  569. }
  570. /**
  571. * Tests that using the subquery strategy in a deep association returns the right results
  572. *
  573. * @see https://github.com/cakephp/cakephp/issues/5769
  574. * @return void
  575. */
  576. public function testDeepBelongsToManySubqueryStrategy2()
  577. {
  578. $this->loadFixtures('Articles', 'Authors', 'Tags', 'Authors', 'AuthorsTags');
  579. $table = $this->getTableLocator()->get('Authors');
  580. $table->hasMany('Articles');
  581. $table->Articles->belongsToMany('Tags', [
  582. 'strategy' => 'subquery',
  583. ]);
  584. $table->belongsToMany('Tags', [
  585. 'strategy' => 'subquery',
  586. ]);
  587. $table->Articles->belongsTo('Authors');
  588. $result = $table->Articles->find()
  589. ->where(['Authors.id >' => 1])
  590. ->contain(['Authors.Tags'])
  591. ->toArray();
  592. $this->assertEquals(
  593. ['tag1', 'tag2'],
  594. collection($result[0]->author->tags)->extract('name')->toArray()
  595. );
  596. $this->assertEquals(3, $result[0]->author->id);
  597. }
  598. /**
  599. * Tests that finding on a table with a primary key other than `id` will work
  600. * seamlessly with either select or subquery.
  601. *
  602. * @see https://github.com/cakephp/cakephp/issues/6781
  603. * @return void
  604. */
  605. public function testDeepHasManyEitherStrategy()
  606. {
  607. $this->loadFixtures('Tags', 'FeaturedTags', 'TagsTranslations');
  608. $tags = $this->getTableLocator()->get('Tags');
  609. $this->skipIf(
  610. $tags->getConnection()->getDriver() instanceof \Cake\Database\Driver\Sqlserver,
  611. 'SQL server is temporarily weird in this test, will investigate later'
  612. );
  613. $tags = $this->getTableLocator()->get('Tags');
  614. $featuredTags = $this->getTableLocator()->get('FeaturedTags');
  615. $featuredTags->belongsTo('Tags');
  616. $tags->hasMany('TagsTranslations', [
  617. 'foreignKey' => 'id',
  618. 'strategy' => 'select',
  619. ]);
  620. $findViaSelect = $featuredTags
  621. ->find()
  622. ->where(['FeaturedTags.tag_id' => 2])
  623. ->contain('Tags.TagsTranslations')
  624. ->all();
  625. $tags->hasMany('TagsTranslations', [
  626. 'foreignKey' => 'id',
  627. 'strategy' => 'subquery',
  628. ]);
  629. $findViaSubquery = $featuredTags
  630. ->find()
  631. ->where(['FeaturedTags.tag_id' => 2])
  632. ->contain('Tags.TagsTranslations')
  633. ->all();
  634. $expected = [2 => 'tag 2 translated into en_us'];
  635. $this->assertEquals($expected, $findViaSelect->combine('tag_id', 'tag.tags_translations.0.name')->toArray());
  636. $this->assertEquals($expected, $findViaSubquery->combine('tag_id', 'tag.tags_translations.0.name')->toArray());
  637. }
  638. /**
  639. * Tests that getting the count of a query having containments return
  640. * the correct results
  641. *
  642. * @see https://github.com/cakephp/cakephp/issues/4511
  643. * @return void
  644. */
  645. public function testCountWithContain()
  646. {
  647. $this->loadFixtures('Articles', 'Authors');
  648. $table = $this->getTableLocator()->get('Articles');
  649. $table->belongsTo('Authors', ['joinType' => 'inner']);
  650. $count = $table
  651. ->find()
  652. ->contain(['Authors' => function ($q) {
  653. return $q->where(['Authors.id' => 1]);
  654. }])
  655. ->count();
  656. $this->assertEquals(2, $count);
  657. }
  658. /**
  659. * Tests that getting the count of a query with bind is correct
  660. *
  661. * @see https://github.com/cakephp/cakephp/issues/8466
  662. * @return void
  663. */
  664. public function testCountWithBind()
  665. {
  666. $this->loadFixtures('Articles');
  667. $table = $this->getTableLocator()->get('Articles');
  668. $query = $table
  669. ->find()
  670. ->select(['title', 'id'])
  671. ->where('title LIKE :val')
  672. ->group(['id', 'title'])
  673. ->bind(':val', '%Second%');
  674. $count = $query->count();
  675. $this->assertEquals(1, $count);
  676. }
  677. /**
  678. * Test count() with inner join containments.
  679. *
  680. * @return void
  681. */
  682. public function testCountWithInnerJoinContain()
  683. {
  684. $this->loadFixtures('Articles', 'Authors');
  685. $table = $this->getTableLocator()->get('Articles');
  686. $table->belongsTo('Authors')->setJoinType('INNER');
  687. $result = $table->save($table->newEntity([
  688. 'author_id' => null,
  689. 'title' => 'title',
  690. 'body' => 'body',
  691. 'published' => 'Y',
  692. ]));
  693. $this->assertNotFalse($result);
  694. $table->getEventManager()
  695. ->on('Model.beforeFind', function (EventInterface $event, $query) {
  696. $query->contain(['Authors']);
  697. });
  698. $count = $table->find()->count();
  699. $this->assertEquals(3, $count);
  700. }
  701. /**
  702. * Tests that bind in subqueries works.
  703. *
  704. * @return void
  705. */
  706. public function testSubqueryBind()
  707. {
  708. $this->loadFixtures('Articles');
  709. $table = $this->getTableLocator()->get('Articles');
  710. $sub = $table->find()
  711. ->select(['id'])
  712. ->where('title LIKE :val')
  713. ->bind(':val', 'Second %');
  714. $query = $table
  715. ->find()
  716. ->select(['title'])
  717. ->where(['id NOT IN' => $sub]);
  718. $result = $query->toArray();
  719. $this->assertCount(2, $result);
  720. $this->assertSame('First Article', $result[0]->title);
  721. $this->assertSame('Third Article', $result[1]->title);
  722. }
  723. /**
  724. * Test that deep containments don't generate empty entities for
  725. * intermediary relations.
  726. *
  727. * @return void
  728. */
  729. public function testContainNoEmptyAssociatedObjects()
  730. {
  731. $this->loadFixtures('Comments', 'Users', 'Articles');
  732. $comments = $this->getTableLocator()->get('Comments');
  733. $comments->belongsTo('Users');
  734. $users = $this->getTableLocator()->get('Users');
  735. $users->hasMany('Articles', [
  736. 'foreignKey' => 'author_id',
  737. ]);
  738. $comments->updateAll(['user_id' => 99], ['id' => 1]);
  739. $result = $comments->find()
  740. ->contain(['Users'])
  741. ->where(['Comments.id' => 1])
  742. ->first();
  743. $this->assertNull($result->user, 'No record should be null.');
  744. $result = $comments->find()
  745. ->contain(['Users', 'Users.Articles'])
  746. ->where(['Comments.id' => 1])
  747. ->first();
  748. $this->assertNull($result->user, 'No record should be null.');
  749. }
  750. /**
  751. * Tests that using a comparison expression inside an OR condition works
  752. *
  753. * @see https://github.com/cakephp/cakephp/issues/5081
  754. * @return void
  755. */
  756. public function testOrConditionsWithExpression()
  757. {
  758. $this->loadFixtures('Articles');
  759. $table = $this->getTableLocator()->get('Articles');
  760. $query = $table->find();
  761. $query->where([
  762. 'OR' => [
  763. new ComparisonExpression('id', 1, 'integer', '>'),
  764. new ComparisonExpression('id', 3, 'integer', '<'),
  765. ],
  766. ]);
  767. $results = $query->toArray();
  768. $this->assertCount(3, $results);
  769. }
  770. /**
  771. * Tests that calling count on a query having a union works correctly
  772. *
  773. * @see https://github.com/cakephp/cakephp/issues/5107
  774. * @return void
  775. */
  776. public function testCountWithUnionQuery()
  777. {
  778. $this->loadFixtures('Articles');
  779. $table = $this->getTableLocator()->get('Articles');
  780. $query = $table->find()->where(['id' => 1]);
  781. $query2 = $table->find()->where(['id' => 2]);
  782. $query->union($query2);
  783. $this->assertEquals(2, $query->count());
  784. }
  785. /**
  786. * Integration test when selecting no fields on the primary table.
  787. *
  788. * @return void
  789. */
  790. public function testSelectNoFieldsOnPrimaryAlias()
  791. {
  792. $this->loadFixtures('Articles', 'Users');
  793. $table = $this->getTableLocator()->get('Articles');
  794. $table->belongsTo('Users');
  795. $query = $table->find()
  796. ->select(['Users__id' => 'id']);
  797. $results = $query->toArray();
  798. $this->assertCount(3, $results);
  799. }
  800. /**
  801. * Test selecting with aliased aggregates and identifier quoting
  802. * does not emit notice errors.
  803. *
  804. * @see https://github.com/cakephp/cakephp/issues/12766
  805. * @return void
  806. */
  807. public function testAliasedAggregateFieldTypeConversionSafe()
  808. {
  809. $this->loadFixtures('Articles');
  810. $articles = $this->getTableLocator()->get('Articles');
  811. $driver = $articles->getConnection()->getDriver();
  812. $restore = $driver->isAutoQuotingEnabled();
  813. $driver->enableAutoQuoting(true);
  814. $query = $articles->find();
  815. $query->select([
  816. 'sumUsers' => $articles->find()->func()->sum('author_id'),
  817. ]);
  818. $driver->enableAutoQuoting($restore);
  819. $result = $query->execute()->fetchAll('assoc');
  820. $this->assertArrayHasKey('sumUsers', $result[0]);
  821. }
  822. /**
  823. * Tests that calling first on the query results will not remove all other results
  824. * from the set.
  825. *
  826. * @return void
  827. */
  828. public function testFirstOnResultSet()
  829. {
  830. $this->loadFixtures('Articles');
  831. $results = $this->getTableLocator()->get('Articles')->find()->all();
  832. $this->assertEquals(3, $results->count());
  833. $this->assertNotNull($results->first());
  834. $this->assertCount(3, $results->toArray());
  835. }
  836. /**
  837. * Checks that matching and contain can be called for the same belongsTo association
  838. *
  839. * @see https://github.com/cakephp/cakephp/issues/5463
  840. * @return void
  841. */
  842. public function testFindMatchingAndContain()
  843. {
  844. $this->loadFixtures('Articles', 'Authors');
  845. $table = $this->getTableLocator()->get('Articles');
  846. $table->belongsTo('Authors');
  847. $article = $table->find()
  848. ->contain('Authors')
  849. ->matching('Authors', function ($q) {
  850. return $q->where(['Authors.id' => 1]);
  851. })
  852. ->first();
  853. $this->assertNotNull($article->author);
  854. $this->assertEquals($article->author, $article->_matchingData['Authors']);
  855. }
  856. /**
  857. * Checks that matching and contain can be called for the same belongsTo association
  858. *
  859. * @see https://github.com/cakephp/cakephp/issues/5463
  860. * @return void
  861. */
  862. public function testFindMatchingAndContainWithSubquery()
  863. {
  864. $this->loadFixtures('Articles', 'Authors', 'Tags', 'ArticlesTags');
  865. $table = $this->getTableLocator()->get('authors');
  866. $table->hasMany('articles', ['strategy' => 'subquery']);
  867. $table->articles->belongsToMany('tags');
  868. $result = $table->find()
  869. ->matching('articles.tags', function ($q) {
  870. return $q->where(['tags.id' => 2]);
  871. })
  872. ->contain('articles');
  873. $this->assertCount(2, $result->first()->articles);
  874. }
  875. /**
  876. * Tests that matching does not overwrite associations in contain
  877. *
  878. * @see https://github.com/cakephp/cakephp/issues/5584
  879. * @return void
  880. */
  881. public function testFindMatchingOverwrite()
  882. {
  883. $this->loadFixtures('Articles', 'Comments', 'Tags', 'ArticlesTags');
  884. $comments = $this->getTableLocator()->get('Comments');
  885. $comments->belongsTo('Articles');
  886. $articles = $this->getTableLocator()->get('Articles');
  887. $articles->belongsToMany('Tags');
  888. $result = $comments
  889. ->find()
  890. ->matching('Articles.Tags', function ($q) {
  891. return $q->where(['Tags.id' => 2]);
  892. })
  893. ->contain('Articles')
  894. ->first();
  895. $this->assertEquals(1, $result->id);
  896. $this->assertEquals(1, $result->_matchingData['Articles']->id);
  897. $this->assertEquals(2, $result->_matchingData['Tags']->id);
  898. $this->assertNotNull($result->article);
  899. $this->assertEquals($result->article, $result->_matchingData['Articles']);
  900. }
  901. /**
  902. * Tests that matching does not overwrite associations in contain
  903. *
  904. * @see https://github.com/cakephp/cakephp/issues/5584
  905. * @return void
  906. */
  907. public function testFindMatchingOverwrite2()
  908. {
  909. $this->loadFixtures('Articles', 'Comments', 'Tags', 'ArticlesTags', 'Authors');
  910. $comments = $this->getTableLocator()->get('Comments');
  911. $comments->belongsTo('Articles');
  912. $articles = $this->getTableLocator()->get('Articles');
  913. $articles->belongsTo('Authors');
  914. $articles->belongsToMany('Tags');
  915. $result = $comments
  916. ->find()
  917. ->matching('Articles.Tags', function ($q) {
  918. return $q->where(['Tags.id' => 2]);
  919. })
  920. ->contain('Articles.Authors')
  921. ->first();
  922. $this->assertNotNull($result->article->author);
  923. }
  924. /**
  925. * Tests that trying to contain an inexistent association
  926. * throws an exception and not a fatal error.
  927. *
  928. * @return void
  929. */
  930. public function testQueryNotFatalError()
  931. {
  932. $this->expectException(\InvalidArgumentException::class);
  933. $this->loadFixtures('Comments');
  934. $comments = $this->getTableLocator()->get('Comments');
  935. $comments->find()->contain('Deprs')->all();
  936. }
  937. /**
  938. * Tests that using matching and contain on belongsTo associations
  939. * works correctly.
  940. *
  941. * @see https://github.com/cakephp/cakephp/issues/5721
  942. * @return void
  943. */
  944. public function testFindMatchingWithContain()
  945. {
  946. $this->loadFixtures('Articles', 'Comments', 'Users');
  947. $comments = $this->getTableLocator()->get('Comments');
  948. $comments->belongsTo('Articles');
  949. $comments->belongsTo('Users');
  950. $result = $comments->find()
  951. ->contain(['Articles', 'Users'])
  952. ->matching('Articles', function ($q) {
  953. return $q->where(['Articles.id >=' => 1]);
  954. })
  955. ->matching('Users', function ($q) {
  956. return $q->where(['Users.id >=' => 1]);
  957. })
  958. ->order(['Comments.id' => 'ASC'])
  959. ->first();
  960. $this->assertInstanceOf('Cake\ORM\Entity', $result->article);
  961. $this->assertInstanceOf('Cake\ORM\Entity', $result->user);
  962. $this->assertEquals(2, $result->user->id);
  963. $this->assertEquals(1, $result->article->id);
  964. }
  965. /**
  966. * Tests that HasMany associations don't use duplicate PK values.
  967. *
  968. * @return void
  969. */
  970. public function testHasManyEagerLoadingUniqueKey()
  971. {
  972. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags');
  973. $table = $this->getTableLocator()->get('ArticlesTags');
  974. $table->belongsTo('Articles', [
  975. 'strategy' => 'select',
  976. ]);
  977. $result = $table->find()
  978. ->contain(['Articles' => function ($q) {
  979. $result = $q->sql();
  980. $this->assertStringNotContainsString(':c2', $result, 'Only 2 bindings as there are only 2 rows.');
  981. $this->assertStringNotContainsString(':c3', $result, 'Only 2 bindings as there are only 2 rows.');
  982. return $q;
  983. }])
  984. ->toArray();
  985. $this->assertNotEmpty($result[0]->article);
  986. }
  987. /**
  988. * Tests that using contain but selecting no fields from the association
  989. * does not trigger any errors and fetches the right results.
  990. *
  991. * @see https://github.com/cakephp/cakephp/issues/6214
  992. * @return void
  993. */
  994. public function testContainWithNoFields()
  995. {
  996. $this->loadFixtures('Comments', 'Users');
  997. $table = $this->getTableLocator()->get('Comments');
  998. $table->belongsTo('Users');
  999. $results = $table->find()
  1000. ->select(['Comments.id', 'Comments.user_id'])
  1001. ->contain(['Users'])
  1002. ->where(['Users.id' => 1])
  1003. ->combine('id', 'user_id');
  1004. $this->assertEquals([3 => 1, 4 => 1, 5 => 1], $results->toArray());
  1005. }
  1006. /**
  1007. * Tests that find() and contained associations using computed fields doesn't error out.
  1008. *
  1009. * @see https://github.com/cakephp/cakephp/issues/9326
  1010. * @return void
  1011. */
  1012. public function testContainWithComputedField()
  1013. {
  1014. $this->loadFixtures('Comments', 'Users');
  1015. $table = $this->getTableLocator()->get('Users');
  1016. $table->hasMany('Comments');
  1017. $query = $table->find()->contain([
  1018. 'Comments' => function ($q) {
  1019. return $q->select([
  1020. 'concat' => $q->func()->concat(['red', 'blue']),
  1021. 'user_id',
  1022. ]);
  1023. }])
  1024. ->where(['Users.id' => 2]);
  1025. $results = $query->toArray();
  1026. $this->assertCount(1, $results);
  1027. $this->assertSame('redblue', $results[0]->comments[0]->concat);
  1028. }
  1029. /**
  1030. * Tests that using matching and selecting no fields for that association
  1031. * will no trigger any errors and fetch the right results
  1032. *
  1033. * @see https://github.com/cakephp/cakephp/issues/6223
  1034. * @return void
  1035. */
  1036. public function testMatchingWithNoFields()
  1037. {
  1038. $this->loadFixtures('Comments', 'Users');
  1039. $table = $this->getTableLocator()->get('Users');
  1040. $table->hasMany('Comments');
  1041. $results = $table->find()
  1042. ->select(['Users.id'])
  1043. ->matching('Comments', function ($q) {
  1044. return $q->where(['Comments.id' => 1]);
  1045. })
  1046. ->extract('id')
  1047. ->toList();
  1048. $this->assertEquals([2], $results);
  1049. }
  1050. /**
  1051. * Test that empty conditions in a matching clause don't cause errors.
  1052. *
  1053. * @return void
  1054. */
  1055. public function testMatchingEmptyQuery()
  1056. {
  1057. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags');
  1058. $table = $this->getTableLocator()->get('Articles');
  1059. $table->belongsToMany('Tags');
  1060. $rows = $table->find()
  1061. ->matching('Tags', function ($q) {
  1062. return $q->where([]);
  1063. })
  1064. ->all();
  1065. $this->assertNotEmpty($rows);
  1066. $rows = $table->find()
  1067. ->matching('Tags', function ($q) {
  1068. return $q->where(null);
  1069. })
  1070. ->all();
  1071. $this->assertNotEmpty($rows);
  1072. }
  1073. /**
  1074. * Tests that using a subquery as part of an expression will not make invalid SQL
  1075. *
  1076. * @return void
  1077. */
  1078. public function testSubqueryInSelectExpression()
  1079. {
  1080. $this->loadFixtures('Comments');
  1081. $table = $this->getTableLocator()->get('Comments');
  1082. $ratio = $table->find()
  1083. ->select(function ($query) use ($table) {
  1084. $allCommentsCount = $table->find()->select($query->func()->count('*'));
  1085. $countToFloat = $query->newExpr([$query->func()->count('*'), '1.0'])->setConjunction('*');
  1086. return [
  1087. 'ratio' => $query
  1088. ->newExpr($countToFloat)
  1089. ->add($allCommentsCount)
  1090. ->setConjunction('/'),
  1091. ];
  1092. })
  1093. ->where(['user_id' => 1])
  1094. ->first()
  1095. ->ratio;
  1096. $this->assertEquals(0.5, $ratio);
  1097. }
  1098. /**
  1099. * Tests calling last on an empty table
  1100. *
  1101. * @see https://github.com/cakephp/cakephp/issues/6683
  1102. * @return void
  1103. */
  1104. public function testFindLastOnEmptyTable()
  1105. {
  1106. $this->loadFixtures('Comments');
  1107. $table = $this->getTableLocator()->get('Comments');
  1108. $table->deleteAll(['1 = 1']);
  1109. $this->assertSame(0, $table->find()->count());
  1110. $this->assertNull($table->find()->last());
  1111. }
  1112. /**
  1113. * Tests calling contain in a nested closure
  1114. *
  1115. * @see https://github.com/cakephp/cakephp/issues/7591
  1116. * @return void
  1117. */
  1118. public function testContainInNestedClosure()
  1119. {
  1120. $this->loadFixtures('Comments', 'Articles', 'Authors', 'Tags', 'AuthorsTags');
  1121. $table = $this->getTableLocator()->get('Comments');
  1122. $table->belongsTo('Articles');
  1123. $table->Articles->belongsTo('Authors');
  1124. $table->Articles->Authors->belongsToMany('Tags');
  1125. $query = $table->find()->where(['Comments.id' => 5])->contain(['Articles' => function ($q) {
  1126. return $q->contain(['Authors' => function ($q) {
  1127. return $q->contain('Tags');
  1128. }]);
  1129. }]);
  1130. $this->assertCount(2, $query->first()->article->author->tags);
  1131. }
  1132. /**
  1133. * Test that the typemaps used in function expressions
  1134. * create the correct results.
  1135. *
  1136. * @return void
  1137. */
  1138. public function testTypemapInFunctions()
  1139. {
  1140. $this->loadFixtures('Comments');
  1141. $table = $this->getTableLocator()->get('Comments');
  1142. $table->updateAll(['published' => null], ['1 = 1']);
  1143. $query = $table->find();
  1144. $query->select([
  1145. 'id',
  1146. 'coalesced' => $query->func()->coalesce(
  1147. ['published' => 'identifier', -1],
  1148. ['integer']
  1149. ),
  1150. ]);
  1151. $result = $query->all()->first();
  1152. $this->assertSame(
  1153. -1,
  1154. $result['coalesced'],
  1155. 'Output values for functions should be casted'
  1156. );
  1157. }
  1158. /**
  1159. * Test that the typemaps used in function expressions
  1160. * create the correct results.
  1161. *
  1162. * @return void
  1163. */
  1164. public function testTypemapInFunctions2()
  1165. {
  1166. $this->loadFixtures('Comments');
  1167. $table = $this->getTableLocator()->get('Comments');
  1168. $query = $table->find();
  1169. $query->select([
  1170. 'max' => $query->func()->max('created', ['datetime']),
  1171. ]);
  1172. $result = $query->all()->first();
  1173. $this->assertEquals(new Time('2007-03-18 10:55:23'), $result['max']);
  1174. }
  1175. /**
  1176. * Test that contain queries map types correctly.
  1177. *
  1178. * @return void
  1179. */
  1180. public function testBooleanConditionsInContain()
  1181. {
  1182. $this->loadFixtures('Articles', 'Tags', 'SpecialTags');
  1183. $table = $this->getTableLocator()->get('Articles');
  1184. $table->belongsToMany('Tags', [
  1185. 'foreignKey' => 'article_id',
  1186. 'associationForeignKey' => 'tag_id',
  1187. 'through' => 'SpecialTags',
  1188. ]);
  1189. $query = $table->find()
  1190. ->contain(['Tags' => function ($q) {
  1191. return $q->where(['SpecialTags.highlighted_time >' => new Time('2014-06-01 00:00:00')]);
  1192. }])
  1193. ->where(['Articles.id' => 2]);
  1194. $result = $query->first();
  1195. $this->assertEquals(2, $result->id);
  1196. $this->assertNotEmpty($result->tags, 'Missing tags');
  1197. $this->assertNotEmpty($result->tags[0]->_joinData, 'Missing join data');
  1198. }
  1199. /**
  1200. * Test that contain queries map types correctly.
  1201. *
  1202. * @return void
  1203. */
  1204. public function testComplexTypesInJoinedWhere()
  1205. {
  1206. $this->loadFixtures('Comments', 'Users');
  1207. $table = $this->getTableLocator()->get('Users');
  1208. $table->hasOne('Comments', [
  1209. 'foreignKey' => 'user_id',
  1210. ]);
  1211. $query = $table->find()
  1212. ->contain('Comments')
  1213. ->where([
  1214. 'Comments.updated >' => new \DateTime('2007-03-18 10:55:00'),
  1215. ]);
  1216. $result = $query->first();
  1217. $this->assertNotEmpty($result);
  1218. $this->assertInstanceOf(FrozenTime::class, $result->comment->updated);
  1219. }
  1220. /**
  1221. * Test that nested contain queries map types correctly.
  1222. *
  1223. * @return void
  1224. */
  1225. public function testComplexNestedTypesInJoinedWhere()
  1226. {
  1227. $this->loadFixtures('Comments', 'Users', 'Articles');
  1228. $table = $this->getTableLocator()->get('Users');
  1229. $table->hasOne('Comments', [
  1230. 'foreignKey' => 'user_id',
  1231. ]);
  1232. $table->Comments->belongsTo('Articles');
  1233. $table->Comments->Articles->belongsTo('Authors', [
  1234. 'className' => 'Users',
  1235. 'foreignKey' => 'author_id',
  1236. ]);
  1237. $query = $table->find()
  1238. ->contain('Comments.Articles.Authors')
  1239. ->where([
  1240. 'Authors.created >' => new \DateTime('2007-03-17 01:16:00'),
  1241. ]);
  1242. $result = $query->first();
  1243. $this->assertNotEmpty($result);
  1244. $this->assertInstanceOf(FrozenTime::class, $result->comment->article->author->updated);
  1245. }
  1246. /**
  1247. * Test that matching queries map types correctly.
  1248. *
  1249. * @return void
  1250. */
  1251. public function testComplexTypesInJoinedWhereWithMatching()
  1252. {
  1253. $this->loadFixtures('Comments', 'Users', 'Articles');
  1254. $table = $this->getTableLocator()->get('Users');
  1255. $table->hasOne('Comments', [
  1256. 'foreignKey' => 'user_id',
  1257. ]);
  1258. $table->Comments->belongsTo('Articles');
  1259. $table->Comments->Articles->belongsTo('Authors', [
  1260. 'className' => 'Users',
  1261. 'foreignKey' => 'author_id',
  1262. ]);
  1263. $query = $table->find()
  1264. ->matching('Comments')
  1265. ->where([
  1266. 'Comments.updated >' => new \DateTime('2007-03-18 10:55:00'),
  1267. ]);
  1268. $result = $query->first();
  1269. $this->assertNotEmpty($result);
  1270. $this->assertInstanceOf(FrozenTime::class, $result->_matchingData['Comments']->updated);
  1271. $query = $table->find()
  1272. ->matching('Comments.Articles.Authors')
  1273. ->where([
  1274. 'Authors.created >' => new \DateTime('2007-03-17 01:16:00'),
  1275. ]);
  1276. $result = $query->first();
  1277. $this->assertNotEmpty($result);
  1278. $this->assertInstanceOf(FrozenTime::class, $result->_matchingData['Authors']->updated);
  1279. }
  1280. /**
  1281. * Test that notMatching queries map types correctly.
  1282. *
  1283. * @return void
  1284. */
  1285. public function testComplexTypesInJoinedWhereWithNotMatching()
  1286. {
  1287. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags');
  1288. $Tags = $this->getTableLocator()->get('Tags');
  1289. $Tags->belongsToMany('Articles');
  1290. $query = $Tags->find()
  1291. ->notMatching('Articles', function ($q) {
  1292. return $q ->where(['ArticlesTags.tag_id !=' => 3 ]);
  1293. })
  1294. ->where([
  1295. 'Tags.created <' => new \DateTime('2016-01-02 00:00:00'),
  1296. ]);
  1297. $result = $query->first();
  1298. $this->assertNotEmpty($result);
  1299. $this->assertEquals(3, $result->id);
  1300. $this->assertInstanceOf(FrozenTime::class, $result->created);
  1301. }
  1302. /**
  1303. * Test that innerJoinWith queries map types correctly.
  1304. *
  1305. * @return void
  1306. */
  1307. public function testComplexTypesInJoinedWhereWithInnerJoinWith()
  1308. {
  1309. $this->loadFixtures('Comments', 'Users', 'Articles');
  1310. $table = $this->getTableLocator()->get('Users');
  1311. $table->hasOne('Comments', [
  1312. 'foreignKey' => 'user_id',
  1313. ]);
  1314. $table->Comments->belongsTo('Articles');
  1315. $table->Comments->Articles->belongsTo('Authors', [
  1316. 'className' => 'Users',
  1317. 'foreignKey' => 'author_id',
  1318. ]);
  1319. $query = $table->find()
  1320. ->innerJoinWith('Comments')
  1321. ->where([
  1322. 'Comments.updated >' => new \DateTime('2007-03-18 10:55:00'),
  1323. ]);
  1324. $result = $query->first();
  1325. $this->assertNotEmpty($result);
  1326. $this->assertInstanceOf(FrozenTime::class, $result->updated);
  1327. $query = $table->find()
  1328. ->innerJoinWith('Comments.Articles.Authors')
  1329. ->where([
  1330. 'Authors.created >' => new \DateTime('2007-03-17 01:16:00'),
  1331. ]);
  1332. $result = $query->first();
  1333. $this->assertNotEmpty($result);
  1334. $this->assertInstanceOf(FrozenTime::class, $result->updated);
  1335. }
  1336. /**
  1337. * Test that leftJoinWith queries map types correctly.
  1338. *
  1339. * @return void
  1340. */
  1341. public function testComplexTypesInJoinedWhereWithLeftJoinWith()
  1342. {
  1343. $this->loadFixtures('Comments', 'Users', 'Articles');
  1344. $table = $this->getTableLocator()->get('Users');
  1345. $table->hasOne('Comments', [
  1346. 'foreignKey' => 'user_id',
  1347. ]);
  1348. $table->Comments->belongsTo('Articles');
  1349. $table->Comments->Articles->belongsTo('Authors', [
  1350. 'className' => 'Users',
  1351. 'foreignKey' => 'author_id',
  1352. ]);
  1353. $query = $table->find()
  1354. ->leftJoinWith('Comments')
  1355. ->where([
  1356. 'Comments.updated >' => new \DateTime('2007-03-18 10:55:00'),
  1357. ]);
  1358. $result = $query->first();
  1359. $this->assertNotEmpty($result);
  1360. $this->assertInstanceOf(FrozenTime::class, $result->updated);
  1361. $query = $table->find()
  1362. ->leftJoinWith('Comments.Articles.Authors')
  1363. ->where([
  1364. 'Authors.created >' => new \DateTime('2007-03-17 01:16:00'),
  1365. ]);
  1366. $result = $query->first();
  1367. $this->assertNotEmpty($result);
  1368. $this->assertInstanceOf(FrozenTime::class, $result->updated);
  1369. }
  1370. /**
  1371. * Tests that it is possible to contain to fetch
  1372. * associations off of a junction table.
  1373. *
  1374. * @return void
  1375. */
  1376. public function testBelongsToManyJoinDataAssociation()
  1377. {
  1378. $this->loadFixtures('Authors', 'Articles', 'Tags', 'SpecialTags');
  1379. $articles = $this->getTableLocator()->get('Articles');
  1380. $tags = $this->getTableLocator()->get('Tags');
  1381. $tags->hasMany('SpecialTags');
  1382. $specialTags = $this->getTableLocator()->get('SpecialTags');
  1383. $specialTags->belongsTo('Authors');
  1384. $specialTags->belongsTo('Articles');
  1385. $specialTags->belongsTo('Tags');
  1386. $articles->belongsToMany('Tags', [
  1387. 'through' => $specialTags,
  1388. ]);
  1389. $query = $articles->find()
  1390. ->contain(['Tags', 'Tags.SpecialTags.Authors'])
  1391. ->where(['Articles.id' => 1]);
  1392. $result = $query->first();
  1393. $this->assertNotEmpty($result->tags, 'Missing tags');
  1394. $this->assertNotEmpty($result->tags[0], 'Missing first tag');
  1395. $this->assertNotEmpty($result->tags[0]->_joinData, 'Missing _joinData');
  1396. $this->assertNotEmpty($result->tags[0]->special_tags[0]->author, 'Missing author on _joinData');
  1397. }
  1398. /**
  1399. * Tests that it is possible to use matching with dot notation
  1400. * even when part of the part of the path in the dot notation is
  1401. * shared for two different calls
  1402. *
  1403. * @return void
  1404. */
  1405. public function testDotNotationNotOverride()
  1406. {
  1407. $this->loadFixtures('Comments', 'Articles', 'Tags', 'Authors', 'SpecialTags');
  1408. $table = $this->getTableLocator()->get('Comments');
  1409. $articles = $table->belongsTo('Articles');
  1410. $specialTags = $articles->hasMany('SpecialTags');
  1411. $specialTags->belongsTo('Authors');
  1412. $specialTags->belongsTo('Tags');
  1413. $results = $table
  1414. ->find()
  1415. ->select(['name' => 'Authors.name', 'tag' => 'Tags.name'])
  1416. ->matching('Articles.SpecialTags.Tags')
  1417. ->matching('Articles.SpecialTags.Authors', function ($q) {
  1418. return $q->where(['Authors.id' => 2]);
  1419. })
  1420. ->distinct()
  1421. ->enableHydration(false)
  1422. ->toArray();
  1423. $this->assertEquals([['name' => 'nate', 'tag' => 'tag1']], $results);
  1424. }
  1425. /**
  1426. * Test expression based ordering with unions.
  1427. *
  1428. * @return void
  1429. */
  1430. public function testComplexOrderWithUnion()
  1431. {
  1432. $this->loadFixtures('Comments');
  1433. $table = $this->getTableLocator()->get('Comments');
  1434. $query = $table->find();
  1435. $inner = $table->find()
  1436. ->select(['content' => 'comment'])
  1437. ->where(['id >' => 3]);
  1438. $inner2 = $table->find()
  1439. ->select(['content' => 'comment'])
  1440. ->where(['id <' => 3]);
  1441. $order = $query->func()->concat(['content' => 'literal', 'test']);
  1442. $query->select(['inside.content'])
  1443. ->from(['inside' => $inner->unionAll($inner2)])
  1444. ->orderAsc($order);
  1445. $results = $query->toArray();
  1446. $this->assertCount(5, $results);
  1447. }
  1448. /**
  1449. * Test that associations that are loaded with subqueries
  1450. * do not cause errors when the subquery has a limit & order clause.
  1451. *
  1452. * @return void
  1453. */
  1454. public function testEagerLoadOrderAndSubquery()
  1455. {
  1456. $this->loadFixtures('Articles', 'Comments');
  1457. $table = $this->getTableLocator()->get('Articles');
  1458. $table->hasMany('Comments', [
  1459. 'strategy' => 'subquery',
  1460. ]);
  1461. $query = $table->find()
  1462. ->select(['score' => 100])
  1463. ->enableAutoFields()
  1464. ->contain(['Comments'])
  1465. ->limit(5)
  1466. ->order(['score' => 'desc']);
  1467. $result = $query->all();
  1468. $this->assertCount(3, $result);
  1469. }
  1470. /**
  1471. * Tests that decorating the results does not result in a memory leak
  1472. *
  1473. * @return void
  1474. */
  1475. public function testFormatResultsMemoryLeak()
  1476. {
  1477. $this->loadFixtures('Articles', 'Authors', 'Tags', 'ArticlesTags');
  1478. $this->skipIf((bool)env('CODECOVERAGE'), 'Running coverage this causes this tests to fail sometimes.');
  1479. $table = $this->getTableLocator()->get('Articles');
  1480. $table->belongsTo('Authors');
  1481. $table->belongsToMany('Tags');
  1482. gc_collect_cycles();
  1483. $memory = memory_get_usage() / 1024 / 1024;
  1484. foreach (range(1, 3) as $time) {
  1485. $table->find()
  1486. ->contain(['Authors', 'Tags'])
  1487. ->formatResults(function ($results) {
  1488. return $results;
  1489. })
  1490. ->all();
  1491. }
  1492. gc_collect_cycles();
  1493. $endMemory = memory_get_usage() / 1024 / 1024;
  1494. $this->assertWithinRange($endMemory, $memory, 1.25, 'Memory leak in ResultSet');
  1495. }
  1496. /**
  1497. * Tests that having bound placeholders in the order clause does not result
  1498. * in an error when trying to count a query.
  1499. *
  1500. * @return void
  1501. */
  1502. public function testCountWithComplexOrderBy()
  1503. {
  1504. $this->loadFixtures('Articles');
  1505. $table = $this->getTableLocator()->get('Articles');
  1506. $query = $table->find();
  1507. $query->orderDesc($query->newExpr()->addCase(
  1508. [$query->newExpr()->add(['id' => 3])],
  1509. [1, 0]
  1510. ));
  1511. $query->order(['title' => 'desc']);
  1512. // Executing the normal query before getting the count
  1513. $query->all();
  1514. $this->assertEquals(3, $query->count());
  1515. $table = $this->getTableLocator()->get('Articles');
  1516. $query = $table->find();
  1517. $query->orderDesc($query->newExpr()->addCase(
  1518. [$query->newExpr()->add(['id' => 3])],
  1519. [1, 0]
  1520. ));
  1521. $query->orderDesc($query->newExpr()->add(['id' => 3]));
  1522. // Not executing the query first, just getting the count
  1523. $this->assertEquals(3, $query->count());
  1524. }
  1525. /**
  1526. * Tests that the now() function expression can be used in the
  1527. * where clause of a query
  1528. *
  1529. * @see https://github.com/cakephp/cakephp/issues/7943
  1530. * @return void
  1531. */
  1532. public function testFunctionInWhereClause()
  1533. {
  1534. $this->loadFixtures('Comments');
  1535. $table = $this->getTableLocator()->get('Comments');
  1536. $table->updateAll(['updated' => Time::now()->addDays(2)], ['id' => 6]);
  1537. $query = $table->find();
  1538. $result = $query->where(['updated >' => $query->func()->now('datetime')])->first();
  1539. $this->assertSame(6, $result->id);
  1540. }
  1541. /**
  1542. * Tests that `notMatching()` can be used on `belongsToMany`
  1543. * associations without passing a query builder callback.
  1544. *
  1545. * @return void
  1546. */
  1547. public function testNotMatchingForBelongsToManyWithoutQueryBuilder()
  1548. {
  1549. $this->loadFixtures('Articles', 'Tags', 'ArticlesTags');
  1550. $Articles = $this->getTableLocator()->get('Articles');
  1551. $Articles->belongsToMany('Tags');
  1552. $result = $Articles->find('list')->notMatching('Tags')->toArray();
  1553. $expected = [
  1554. 3 => 'Third Article',
  1555. ];
  1556. $this->assertEquals($expected, $result);
  1557. }
  1558. /**
  1559. * Tests deep formatters get the right object type when applied in a beforeFind
  1560. *
  1561. * @see https://github.com/cakephp/cakephp/issues/9787
  1562. * @return void
  1563. */
  1564. public function testFormatDeepDistantAssociationRecords2()
  1565. {
  1566. $this->loadFixtures('Authors', 'Articles', 'Tags', 'ArticlesTags');
  1567. $table = $this->getTableLocator()->get('authors');
  1568. $table->hasMany('articles');
  1569. $articles = $table->getAssociation('articles')->getTarget();
  1570. $articles->hasMany('articlesTags');
  1571. $tags = $articles->getAssociation('articlesTags')->getTarget()->belongsTo('tags');
  1572. $tags->getTarget()->getEventManager()->on('Model.beforeFind', function ($e, $query) {
  1573. return $query->formatResults(function ($results) {
  1574. return $results->map(function (\Cake\ORM\Entity $tag) {
  1575. $tag->name .= ' - visited';
  1576. return $tag;
  1577. });
  1578. });
  1579. });
  1580. $query = $table->find()->contain(['articles.articlesTags.tags']);
  1581. $query->mapReduce(function ($row, $key, $mr) {
  1582. foreach ((array)$row->articles as $article) {
  1583. foreach ((array)$article->articles_tags as $articleTag) {
  1584. $mr->emit($articleTag->tag->name);
  1585. }
  1586. }
  1587. });
  1588. $expected = ['tag1 - visited', 'tag2 - visited', 'tag1 - visited', 'tag3 - visited'];
  1589. $this->assertEquals($expected, $query->toArray());
  1590. }
  1591. /**
  1592. * Tests that subqueries can be used with function expressions.
  1593. *
  1594. * @return void
  1595. */
  1596. public function testFunctionExpressionWithSubquery()
  1597. {
  1598. $this->loadFixtures('Articles');
  1599. $table = $this->getTableLocator()->get('Articles');
  1600. $query = $table
  1601. ->find()
  1602. ->select(function (Query $q) use ($table) {
  1603. return [
  1604. 'value' => $q
  1605. ->func()
  1606. ->ABS([
  1607. $table
  1608. ->getConnection()
  1609. ->newQuery()
  1610. ->select(-1),
  1611. ])
  1612. ->setReturnType('integer'),
  1613. ];
  1614. });
  1615. $result = $query->first()->get('value');
  1616. $this->assertEquals(1, $result);
  1617. }
  1618. /**
  1619. * Tests that correlated subqueries can be used with function expressions.
  1620. *
  1621. * @return void
  1622. */
  1623. public function testFunctionExpressionWithCorrelatedSubquery()
  1624. {
  1625. $this->loadFixtures('Articles', 'Authors');
  1626. $table = $this->getTableLocator()->get('Articles');
  1627. $table->belongsTo('Authors');
  1628. $query = $table
  1629. ->find()
  1630. ->select(function (Query $q) use ($table) {
  1631. return [
  1632. 'value' => $q->func()->UPPER([
  1633. $table
  1634. ->getAssociation('Authors')
  1635. ->find()
  1636. ->select(['Authors.name'])
  1637. ->where(function (QueryExpression $exp) {
  1638. return $exp->equalFields('Authors.id', 'Articles.author_id');
  1639. }),
  1640. ]),
  1641. ];
  1642. });
  1643. $result = $query->first()->get('value');
  1644. $this->assertEquals('MARIANO', $result);
  1645. }
  1646. /**
  1647. * Tests that subqueries can be used with multi argument function expressions.
  1648. *
  1649. * @return void
  1650. */
  1651. public function testMultiArgumentFunctionExpressionWithSubquery()
  1652. {
  1653. $this->loadFixtures('Articles', 'Authors');
  1654. $table = $this->getTableLocator()->get('Articles');
  1655. $query = $table
  1656. ->find()
  1657. ->select(function (Query $q) use ($table) {
  1658. return [
  1659. 'value' => $q
  1660. ->func()
  1661. ->ROUND(
  1662. [
  1663. $table
  1664. ->getConnection()
  1665. ->newQuery()
  1666. ->select(1.23456),
  1667. 2,
  1668. ],
  1669. [null, 'integer']
  1670. )
  1671. ->setReturnType('float'),
  1672. ];
  1673. });
  1674. $result = $query->first()->get('value');
  1675. $this->assertEquals(1.23, $result);
  1676. }
  1677. /**
  1678. * Tests that correlated subqueries can be used with multi argument function expressions.
  1679. *
  1680. * @return void
  1681. */
  1682. public function testMultiArgumentFunctionExpressionWithCorrelatedSubquery()
  1683. {
  1684. $this->loadFixtures('Articles', 'Authors');
  1685. $table = $this->getTableLocator()->get('Articles');
  1686. $table->belongsTo('Authors');
  1687. $this->assertEquals(
  1688. 1,
  1689. $table->getAssociation('Authors')->updateAll(['name' => null], ['id' => 3])
  1690. );
  1691. $query = $table
  1692. ->find()
  1693. ->select(function (Query $q) use ($table) {
  1694. return [
  1695. 'value' => $q->func()->coalesce([
  1696. $table
  1697. ->getAssociation('Authors')
  1698. ->find()
  1699. ->select(['Authors.name'])
  1700. ->where(function (QueryExpression $exp) {
  1701. return $exp->equalFields('Authors.id', 'Articles.author_id');
  1702. }),
  1703. '1',
  1704. ]),
  1705. ];
  1706. });
  1707. $results = $query->extract('value')->toArray();
  1708. $this->assertEquals(['mariano', '1', 'mariano'], $results);
  1709. }
  1710. /**
  1711. * Tests that subqueries can be used with function expressions that are being transpiled.
  1712. *
  1713. * @return void
  1714. */
  1715. public function testTranspiledFunctionExpressionWithSubquery()
  1716. {
  1717. $this->loadFixtures('Articles', 'Authors');
  1718. $table = $this->getTableLocator()->get('Articles');
  1719. $table->belongsTo('Authors');
  1720. $query = $table
  1721. ->find()
  1722. ->select(function (Query $q) use ($table) {
  1723. return [
  1724. 'value' => $q->func()->concat([
  1725. $table
  1726. ->getAssociation('Authors')
  1727. ->find()
  1728. ->select(['Authors.name'])
  1729. ->where(['Authors.id' => 1]),
  1730. ' appended',
  1731. ]),
  1732. ];
  1733. });
  1734. $result = $query->first()->get('value');
  1735. $this->assertEquals('mariano appended', $result);
  1736. }
  1737. /**
  1738. * Tests that correlated subqueries can be used with function expressions that are being transpiled.
  1739. *
  1740. * @return void
  1741. */
  1742. public function testTranspiledFunctionExpressionWithCorrelatedSubquery()
  1743. {
  1744. $this->loadFixtures('Articles', 'Authors');
  1745. $table = $this->getTableLocator()->get('Articles');
  1746. $table->belongsTo('Authors');
  1747. $query = $table
  1748. ->find()
  1749. ->select(function (Query $q) use ($table) {
  1750. return [
  1751. 'value' => $q->func()->concat([
  1752. $table
  1753. ->getAssociation('Authors')
  1754. ->find()
  1755. ->select(['Authors.name'])
  1756. ->where(function (QueryExpression $exp) {
  1757. return $exp->equalFields('Authors.id', 'Articles.author_id');
  1758. }),
  1759. ' appended',
  1760. ]),
  1761. ];
  1762. });
  1763. $result = $query->first()->get('value');
  1764. $this->assertEquals('mariano appended', $result);
  1765. }
  1766. }