QueryRegressionTest.php 59 KB

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