DatabaseEloquentHasOneOfManyTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. <?php
  2. namespace Illuminate\Tests\Database;
  3. use Illuminate\Database\Capsule\Manager as DB;
  4. use Illuminate\Database\Eloquent\Model as Eloquent;
  5. use Illuminate\Database\Eloquent\SoftDeletes;
  6. use InvalidArgumentException;
  7. use PHPUnit\Framework\TestCase;
  8. class DatabaseEloquentHasOneOfManyTest extends TestCase
  9. {
  10. protected function setUp(): void
  11. {
  12. $db = new DB;
  13. $db->addConnection([
  14. 'driver' => 'sqlite',
  15. 'database' => ':memory:',
  16. ]);
  17. $db->bootEloquent();
  18. $db->setAsGlobal();
  19. $this->createSchema();
  20. }
  21. /**
  22. * Setup the database schema.
  23. *
  24. * @return void
  25. */
  26. public function createSchema()
  27. {
  28. $this->schema()->create('users', function ($table) {
  29. $table->increments('id');
  30. });
  31. $this->schema()->create('logins', function ($table) {
  32. $table->increments('id');
  33. $table->foreignId('user_id');
  34. $table->dateTime('deleted_at')->nullable();
  35. });
  36. $this->schema()->create('states', function ($table) {
  37. $table->increments('id');
  38. $table->string('state');
  39. $table->string('type');
  40. $table->foreignId('user_id');
  41. $table->timestamps();
  42. });
  43. $this->schema()->create('prices', function ($table) {
  44. $table->increments('id');
  45. $table->dateTime('published_at');
  46. $table->foreignId('user_id');
  47. });
  48. }
  49. /**
  50. * Tear down the database schema.
  51. *
  52. * @return void
  53. */
  54. protected function tearDown(): void
  55. {
  56. $this->schema()->drop('users');
  57. $this->schema()->drop('logins');
  58. $this->schema()->drop('states');
  59. $this->schema()->drop('prices');
  60. }
  61. public function testItGuessesRelationName()
  62. {
  63. $user = HasOneOfManyTestUser::make();
  64. $this->assertSame('latest_login', $user->latest_login()->getRelationName());
  65. }
  66. public function testItGuessesRelationNameAndAddsOfManyWhenTableNameIsRelationName()
  67. {
  68. $model = HasOneOfManyTestModel::make();
  69. $this->assertSame('logins_of_many', $model->logins()->getRelationName());
  70. }
  71. public function testRelationNameCanBeSet()
  72. {
  73. $user = HasOneOfManyTestUser::create();
  74. // Using "ofMany"
  75. $relation = $user->latest_login()->ofMany('id', 'max', 'foo');
  76. $this->assertSame('foo', $relation->getRelationName());
  77. // Using "latestOfMAny"
  78. $relation = $user->latest_login()->latestOfMAny('id', 'bar');
  79. $this->assertSame('bar', $relation->getRelationName());
  80. // Using "oldestOfMAny"
  81. $relation = $user->latest_login()->oldestOfMAny('id', 'baz');
  82. $this->assertSame('baz', $relation->getRelationName());
  83. }
  84. public function testEagerLoadingAppliesConstraintsToInnerJoinSubQuery()
  85. {
  86. $user = HasOneOfManyTestUser::create();
  87. $relation = $user->latest_login();
  88. $relation->addEagerConstraints([$user]);
  89. $this->assertSame('select MAX("logins"."id") as "id_aggregate", "logins"."user_id" from "logins" where "logins"."user_id" = ? and "logins"."user_id" is not null and "logins"."user_id" in (1) group by "logins"."user_id"', $relation->getOneOfManySubQuery()->toSql());
  90. }
  91. public function testGlobalScopeIsNotAppliedWhenRelationIsDefinedWithoutGlobalScope()
  92. {
  93. HasOneOfManyTestLogin::addGlobalScope('test', function ($query) {
  94. $query->orderBy('id');
  95. });
  96. $user = HasOneOfManyTestUser::create();
  97. $relation = $user->latest_login_without_global_scope();
  98. $relation->addEagerConstraints([$user]);
  99. $this->assertSame('select "logins".* from "logins" inner join (select MAX("logins"."id") as "id_aggregate", "logins"."user_id" from "logins" where "logins"."user_id" = ? and "logins"."user_id" is not null and "logins"."user_id" in (1) group by "logins"."user_id") as "latestOfMany" on "latestOfMany"."id_aggregate" = "logins"."id" and "latestOfMany"."user_id" = "logins"."user_id" where "logins"."user_id" = ? and "logins"."user_id" is not null', $relation->getQuery()->toSql());
  100. HasOneOfManyTestLogin::addGlobalScope('test', function ($query) {
  101. });
  102. }
  103. public function testGlobalScopeIsNotAppliedWhenRelationIsDefinedWithoutGlobalScopeWithComplexQuery()
  104. {
  105. HasOneOfManyTestPrice::addGlobalScope('test', function ($query) {
  106. $query->orderBy('id');
  107. });
  108. $user = HasOneOfManyTestUser::create();
  109. $relation = $user->price_without_global_scope();
  110. $this->assertSame('select "prices".* from "prices" inner join (select max("prices"."id") as "id_aggregate", "prices"."user_id" from "prices" inner join (select max("prices"."published_at") as "published_at_aggregate", "prices"."user_id" from "prices" where "published_at" < ? and "prices"."user_id" = ? and "prices"."user_id" is not null group by "prices"."user_id") as "price_without_global_scope" on "price_without_global_scope"."published_at_aggregate" = "prices"."published_at" and "price_without_global_scope"."user_id" = "prices"."user_id" where "published_at" < ? group by "prices"."user_id") as "price_without_global_scope" on "price_without_global_scope"."id_aggregate" = "prices"."id" and "price_without_global_scope"."user_id" = "prices"."user_id" where "prices"."user_id" = ? and "prices"."user_id" is not null', $relation->getQuery()->toSql());
  111. HasOneOfManyTestPrice::addGlobalScope('test', function ($query) {
  112. });
  113. }
  114. public function testQualifyingSubSelectColumn()
  115. {
  116. $user = HasOneOfManyTestUser::create();
  117. $this->assertSame('latest_login.id', $user->latest_login()->qualifySubSelectColumn('id'));
  118. }
  119. public function testItFailsWhenUsingInvalidAggregate()
  120. {
  121. $this->expectException(InvalidArgumentException::class);
  122. $this->expectExceptionMessage('Invalid aggregate [count] used within ofMany relation. Available aggregates: MIN, MAX');
  123. $user = HasOneOfManyTestUser::make();
  124. $user->latest_login_with_invalid_aggregate();
  125. }
  126. public function testItGetsCorrectResults()
  127. {
  128. $user = HasOneOfManyTestUser::create();
  129. $previousLogin = $user->logins()->create();
  130. $latestLogin = $user->logins()->create();
  131. $result = $user->latest_login()->getResults();
  132. $this->assertNotNull($result);
  133. $this->assertSame($latestLogin->id, $result->id);
  134. }
  135. public function testResultDoesNotHaveAggregateColumn()
  136. {
  137. $user = HasOneOfManyTestUser::create();
  138. $user->logins()->create();
  139. $result = $user->latest_login()->getResults();
  140. $this->assertNotNull($result);
  141. $this->assertFalse(isset($result->id_aggregate));
  142. }
  143. public function testItGetsCorrectResultsUsingShortcutMethod()
  144. {
  145. $user = HasOneOfManyTestUser::create();
  146. $previousLogin = $user->logins()->create();
  147. $latestLogin = $user->logins()->create();
  148. $result = $user->latest_login_with_shortcut()->getResults();
  149. $this->assertNotNull($result);
  150. $this->assertSame($latestLogin->id, $result->id);
  151. }
  152. public function testItGetsCorrectResultsUsingShortcutReceivingMultipleColumnsMethod()
  153. {
  154. $user = HasOneOfManyTestUser::create();
  155. $user->prices()->create([
  156. 'published_at' => '2021-05-01 00:00:00',
  157. ]);
  158. $price = $user->prices()->create([
  159. 'published_at' => '2021-05-01 00:00:00',
  160. ]);
  161. $result = $user->price_with_shortcut()->getResults();
  162. $this->assertNotNull($result);
  163. $this->assertSame($price->id, $result->id);
  164. }
  165. public function testKeyIsAddedToAggregatesWhenMissing()
  166. {
  167. $user = HasOneOfManyTestUser::create();
  168. $user->prices()->create([
  169. 'published_at' => '2021-05-01 00:00:00',
  170. ]);
  171. $price = $user->prices()->create([
  172. 'published_at' => '2021-05-01 00:00:00',
  173. ]);
  174. $result = $user->price_without_key_in_aggregates()->getResults();
  175. $this->assertNotNull($result);
  176. $this->assertSame($price->id, $result->id);
  177. }
  178. public function testItGetsWithConstraintsCorrectResults()
  179. {
  180. $user = HasOneOfManyTestUser::create();
  181. $previousLogin = $user->logins()->create();
  182. $user->logins()->create();
  183. $result = $user->latest_login()->whereKey($previousLogin->getKey())->getResults();
  184. $this->assertNull($result);
  185. }
  186. public function testItEagerLoadsCorrectModels()
  187. {
  188. $user = HasOneOfManyTestUser::create();
  189. $user->logins()->create();
  190. $latestLogin = $user->logins()->create();
  191. $user = HasOneOfManyTestUser::with('latest_login')->first();
  192. $this->assertTrue($user->relationLoaded('latest_login'));
  193. $this->assertSame($latestLogin->id, $user->latest_login->id);
  194. }
  195. public function testItJoinsOtherTableInSubQuery()
  196. {
  197. $user = HasOneOfManyTestUser::create();
  198. $user->logins()->create();
  199. $this->assertNull($user->latest_login_with_foo_state);
  200. $user->unsetRelation('latest_login_with_foo_state');
  201. $user->states()->create([
  202. 'type' => 'foo',
  203. 'state' => 'draft',
  204. ]);
  205. $this->assertNotNull($user->latest_login_with_foo_state);
  206. }
  207. public function testHasNested()
  208. {
  209. $user = HasOneOfManyTestUser::create();
  210. $previousLogin = $user->logins()->create();
  211. $latestLogin = $user->logins()->create();
  212. $found = HasOneOfManyTestUser::whereHas('latest_login', function ($query) use ($latestLogin) {
  213. $query->where('logins.id', $latestLogin->id);
  214. })->exists();
  215. $this->assertTrue($found);
  216. $found = HasOneOfManyTestUser::whereHas('latest_login', function ($query) use ($previousLogin) {
  217. $query->where('logins.id', $previousLogin->id);
  218. })->exists();
  219. $this->assertFalse($found);
  220. }
  221. public function testHasCount()
  222. {
  223. $user = HasOneOfManyTestUser::create();
  224. $user->logins()->create();
  225. $user->logins()->create();
  226. $user = HasOneOfManyTestUser::withCount('latest_login')->first();
  227. $this->assertEquals(1, $user->latest_login_count);
  228. }
  229. public function testExists()
  230. {
  231. $user = HasOneOfManyTestUser::create();
  232. $previousLogin = $user->logins()->create();
  233. $latestLogin = $user->logins()->create();
  234. $this->assertFalse($user->latest_login()->whereKey($previousLogin->getKey())->exists());
  235. $this->assertTrue($user->latest_login()->whereKey($latestLogin->getKey())->exists());
  236. }
  237. public function testIsMethod()
  238. {
  239. $user = HasOneOfManyTestUser::create();
  240. $login1 = $user->latest_login()->create();
  241. $login2 = $user->latest_login()->create();
  242. $this->assertFalse($user->latest_login()->is($login1));
  243. $this->assertTrue($user->latest_login()->is($login2));
  244. }
  245. public function testIsNotMethod()
  246. {
  247. $user = HasOneOfManyTestUser::create();
  248. $login1 = $user->latest_login()->create();
  249. $login2 = $user->latest_login()->create();
  250. $this->assertTrue($user->latest_login()->isNot($login1));
  251. $this->assertFalse($user->latest_login()->isNot($login2));
  252. }
  253. public function testGet()
  254. {
  255. $user = HasOneOfManyTestUser::create();
  256. $previousLogin = $user->logins()->create();
  257. $latestLogin = $user->logins()->create();
  258. $latestLogins = $user->latest_login()->get();
  259. $this->assertCount(1, $latestLogins);
  260. $this->assertSame($latestLogin->id, $latestLogins->first()->id);
  261. $latestLogins = $user->latest_login()->whereKey($previousLogin->getKey())->get();
  262. $this->assertCount(0, $latestLogins);
  263. }
  264. public function testCount()
  265. {
  266. $user = HasOneOfManyTestUser::create();
  267. $user->logins()->create();
  268. $user->logins()->create();
  269. $this->assertSame(1, $user->latest_login()->count());
  270. }
  271. public function testAggregate()
  272. {
  273. $user = HasOneOfManyTestUser::create();
  274. $firstLogin = $user->logins()->create();
  275. $user->logins()->create();
  276. $user = HasOneOfManyTestUser::first();
  277. $this->assertSame($firstLogin->id, $user->first_login->id);
  278. }
  279. public function testJoinConstraints()
  280. {
  281. $user = HasOneOfManyTestUser::create();
  282. $user->states()->create([
  283. 'type' => 'foo',
  284. 'state' => 'draft',
  285. ]);
  286. $currentForState = $user->states()->create([
  287. 'type' => 'foo',
  288. 'state' => 'active',
  289. ]);
  290. $user->states()->create([
  291. 'type' => 'bar',
  292. 'state' => 'baz',
  293. ]);
  294. $user = HasOneOfManyTestUser::first();
  295. $this->assertSame($currentForState->id, $user->foo_state->id);
  296. }
  297. public function testMultipleAggregates()
  298. {
  299. $user = HasOneOfManyTestUser::create();
  300. $user->prices()->create([
  301. 'published_at' => '2021-05-01 00:00:00',
  302. ]);
  303. $price = $user->prices()->create([
  304. 'published_at' => '2021-05-01 00:00:00',
  305. ]);
  306. $user = HasOneOfManyTestUser::first();
  307. $this->assertSame($price->id, $user->price->id);
  308. }
  309. public function testEagerLoadingWithMultipleAggregates()
  310. {
  311. $user1 = HasOneOfManyTestUser::create();
  312. $user2 = HasOneOfManyTestUser::create();
  313. $user1->prices()->create([
  314. 'published_at' => '2021-05-01 00:00:00',
  315. ]);
  316. $user1Price = $user1->prices()->create([
  317. 'published_at' => '2021-05-01 00:00:00',
  318. ]);
  319. $user1->prices()->create([
  320. 'published_at' => '2021-04-01 00:00:00',
  321. ]);
  322. $user2Price = $user2->prices()->create([
  323. 'published_at' => '2021-05-01 00:00:00',
  324. ]);
  325. $user2->prices()->create([
  326. 'published_at' => '2021-04-01 00:00:00',
  327. ]);
  328. $users = HasOneOfManyTestUser::with('price')->get();
  329. $this->assertNotNull($users[0]->price);
  330. $this->assertSame($user1Price->id, $users[0]->price->id);
  331. $this->assertNotNull($users[1]->price);
  332. $this->assertSame($user2Price->id, $users[1]->price->id);
  333. }
  334. public function testWithExists()
  335. {
  336. $user = HasOneOfManyTestUser::create();
  337. $user = HasOneOfManyTestUser::withExists('latest_login')->first();
  338. $this->assertFalse($user->latest_login_exists);
  339. $user->logins()->create();
  340. $user = HasOneOfManyTestUser::withExists('latest_login')->first();
  341. $this->assertTrue($user->latest_login_exists);
  342. }
  343. public function testWithExistsWithConstraintsInJoinSubSelect()
  344. {
  345. $user = HasOneOfManyTestUser::create();
  346. $user = HasOneOfManyTestUser::withExists('foo_state')->first();
  347. $this->assertFalse($user->foo_state_exists);
  348. $user->states()->create([
  349. 'type' => 'foo',
  350. 'state' => 'bar',
  351. ]);
  352. $user = HasOneOfManyTestUser::withExists('foo_state')->first();
  353. $this->assertTrue($user->foo_state_exists);
  354. }
  355. public function testWithSoftDeletes()
  356. {
  357. $user = HasOneOfManyTestUser::create();
  358. $user->logins()->create();
  359. $user->latest_login_with_soft_deletes;
  360. $this->assertNotNull($user->latest_login_with_soft_deletes);
  361. }
  362. public function testWithContraintNotInAggregate()
  363. {
  364. $user = HasOneOfManyTestUser::create();
  365. $previousFoo = $user->states()->create([
  366. 'type' => 'foo',
  367. 'state' => 'bar',
  368. 'updated_at' => '2020-01-01 00:00:00',
  369. ]);
  370. $newFoo = $user->states()->create([
  371. 'type' => 'foo',
  372. 'state' => 'active',
  373. 'updated_at' => '2021-01-01 12:00:00',
  374. ]);
  375. $newBar = $user->states()->create([
  376. 'type' => 'bar',
  377. 'state' => 'active',
  378. 'updated_at' => '2021-01-01 12:00:00',
  379. ]);
  380. $this->assertSame($newFoo->id, $user->last_updated_foo_state->id);
  381. }
  382. /**
  383. * Get a database connection instance.
  384. *
  385. * @return \Illuminate\Database\Connection
  386. */
  387. protected function connection()
  388. {
  389. return Eloquent::getConnectionResolver()->connection();
  390. }
  391. /**
  392. * Get a schema builder instance.
  393. *
  394. * @return \Illuminate\Database\Schema\Builder
  395. */
  396. protected function schema()
  397. {
  398. return $this->connection()->getSchemaBuilder();
  399. }
  400. }
  401. /**
  402. * Eloquent Models...
  403. */
  404. class HasOneOfManyTestUser extends Eloquent
  405. {
  406. protected $table = 'users';
  407. protected $guarded = [];
  408. public $timestamps = false;
  409. public function logins()
  410. {
  411. return $this->hasMany(HasOneOfManyTestLogin::class, 'user_id');
  412. }
  413. public function latest_login()
  414. {
  415. return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany();
  416. }
  417. public function latest_login_with_soft_deletes()
  418. {
  419. return $this->hasOne(HasOneOfManyTestLoginWithSoftDeletes::class, 'user_id')->ofMany();
  420. }
  421. public function latest_login_with_shortcut()
  422. {
  423. return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->latestOfMany();
  424. }
  425. public function latest_login_with_invalid_aggregate()
  426. {
  427. return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany('id', 'count');
  428. }
  429. public function latest_login_without_global_scope()
  430. {
  431. return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->withoutGlobalScopes()->latestOfMany();
  432. }
  433. public function first_login()
  434. {
  435. return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany('id', 'min');
  436. }
  437. public function latest_login_with_foo_state()
  438. {
  439. return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany(
  440. ['id' => 'max'],
  441. function ($query) {
  442. $query->join('states', 'states.user_id', 'logins.user_id')
  443. ->where('states.type', 'foo');
  444. }
  445. );
  446. }
  447. public function states()
  448. {
  449. return $this->hasMany(HasOneOfManyTestState::class, 'user_id');
  450. }
  451. public function foo_state()
  452. {
  453. return $this->hasOne(HasOneOfManyTestState::class, 'user_id')->ofMany(
  454. ['id' => 'max'],
  455. function ($q) {
  456. $q->where('type', 'foo');
  457. }
  458. );
  459. }
  460. public function last_updated_foo_state()
  461. {
  462. return $this->hasOne(HasOneOfManyTestState::class, 'user_id')->ofMany([
  463. 'updated_at' => 'max',
  464. 'id' => 'max',
  465. ], function ($q) {
  466. $q->where('type', 'foo');
  467. });
  468. }
  469. public function prices()
  470. {
  471. return $this->hasMany(HasOneOfManyTestPrice::class, 'user_id');
  472. }
  473. public function price()
  474. {
  475. return $this->hasOne(HasOneOfManyTestPrice::class, 'user_id')->ofMany([
  476. 'published_at' => 'max',
  477. 'id' => 'max',
  478. ], function ($q) {
  479. $q->where('published_at', '<', now());
  480. });
  481. }
  482. public function price_without_key_in_aggregates()
  483. {
  484. return $this->hasOne(HasOneOfManyTestPrice::class, 'user_id')->ofMany(['published_at' => 'MAX']);
  485. }
  486. public function price_with_shortcut()
  487. {
  488. return $this->hasOne(HasOneOfManyTestPrice::class, 'user_id')->latestOfMany(['published_at', 'id']);
  489. }
  490. public function price_without_global_scope()
  491. {
  492. return $this->hasOne(HasOneOfManyTestPrice::class, 'user_id')->withoutGlobalScopes()->ofMany([
  493. 'published_at' => 'max',
  494. 'id' => 'max',
  495. ], function ($q) {
  496. $q->where('published_at', '<', now());
  497. });
  498. }
  499. }
  500. class HasOneOfManyTestModel extends Eloquent
  501. {
  502. public function logins()
  503. {
  504. return $this->hasOne(HasOneOfManyTestLogin::class)->ofMany();
  505. }
  506. }
  507. class HasOneOfManyTestLogin extends Eloquent
  508. {
  509. protected $table = 'logins';
  510. protected $guarded = [];
  511. public $timestamps = false;
  512. }
  513. class HasOneOfManyTestLoginWithSoftDeletes extends Eloquent
  514. {
  515. use SoftDeletes;
  516. protected $table = 'logins';
  517. protected $guarded = [];
  518. public $timestamps = false;
  519. }
  520. class HasOneOfManyTestState extends Eloquent
  521. {
  522. protected $table = 'states';
  523. protected $guarded = [];
  524. public $timestamps = true;
  525. protected $fillable = ['type', 'state', 'updated_at'];
  526. }
  527. class HasOneOfManyTestPrice extends Eloquent
  528. {
  529. protected $table = 'prices';
  530. protected $guarded = [];
  531. public $timestamps = false;
  532. protected $fillable = ['published_at'];
  533. protected $casts = ['published_at' => 'datetime'];
  534. }