@@ -1577,6 +1577,74 @@ public function it_propagates_entry_if_configured()
15771577 });
15781578 }
15791579
1580+ #[Test]
1581+ public function it_resolves_descendants_with_one_query_per_level_rather_than_one_per_localization ()
1582+ {
1583+ $ collection = (new Collection )->handle ('pages ' )->save ();
1584+
1585+ $ entry = (new Entry )->id ('a ' )->locale ('en ' )->collection ($ collection );
1586+
1587+ // Three direct localizations, all leaves (no further descendants).
1588+ $ fr = (new Entry )->id ('b ' )->locale ('fr ' )->origin ('a ' )->collection ($ collection );
1589+ $ de = (new Entry )->id ('c ' )->locale ('de ' )->origin ('a ' )->collection ($ collection );
1590+ $ es = (new Entry )->id ('d ' )->locale ('es ' )->origin ('a ' )->collection ($ collection );
1591+
1592+ // A flat tree is one level deep, so it should take exactly two queries
1593+ // (the direct descendants, then a single batched lookup for the next
1594+ // level which comes back empty) regardless of how many localizations
1595+ // exist. The previous recursive implementation issued one query per
1596+ // node, i.e. O(number of localizations).
1597+ Facades \Entry::shouldReceive ('query ' )->times (2 )->andReturn (
1598+ $ this ->fakeDescendantsQuery (collect ([$ fr , $ de , $ es ])), // direct descendants of 'a'
1599+ $ this ->fakeDescendantsQuery (collect (), ['b ' , 'c ' , 'd ' ]), // batched lookup for the next level
1600+ );
1601+
1602+ $ descendants = $ entry ->descendants ();
1603+
1604+ $ this ->assertEquals (['de ' , 'es ' , 'fr ' ], $ descendants ->keys ()->sort ()->values ()->all ());
1605+ }
1606+
1607+ #[Test]
1608+ public function it_includes_descendants_nested_via_an_origin_chain ()
1609+ {
1610+ $ collection = (new Collection )->handle ('pages ' )->save ();
1611+
1612+ $ entry = (new Entry )->id ('a ' )->locale ('en ' )->collection ($ collection );
1613+
1614+ // de's origin is fr, whose origin is en: a grandchild only reachable by
1615+ // walking past the first level. The flattened result must include it.
1616+ $ fr = (new Entry )->id ('b ' )->locale ('fr ' )->origin ('a ' )->collection ($ collection );
1617+ $ de = (new Entry )->id ('c ' )->locale ('de ' )->origin ('b ' )->collection ($ collection );
1618+
1619+ Facades \Entry::shouldReceive ('query ' )->times (3 )->andReturn (
1620+ $ this ->fakeDescendantsQuery (collect ([$ fr ])), // direct descendants of en
1621+ $ this ->fakeDescendantsQuery (collect ([$ de ]), ['b ' ]), // batched descendants of fr
1622+ $ this ->fakeDescendantsQuery (collect (), ['c ' ]), // batched descendants of de
1623+ );
1624+
1625+ $ descendants = $ entry ->descendants ();
1626+
1627+ $ this ->assertEquals (['de ' , 'fr ' ], $ descendants ->keys ()->sort ()->values ()->all ());
1628+ $ this ->assertSame ($ fr , $ descendants ->get ('fr ' ));
1629+ $ this ->assertSame ($ de , $ descendants ->get ('de ' ));
1630+ }
1631+
1632+ private function fakeDescendantsQuery ($ results , ?array $ whereInOrigins = null ): QueryBuilder
1633+ {
1634+ $ query = Mockery::mock (QueryBuilder::class);
1635+ $ query ->shouldReceive ('where ' )->with ('collection ' , 'pages ' )->andReturnSelf ();
1636+
1637+ if ($ whereInOrigins === null ) {
1638+ // directDescendants() uses where('origin', string); batched levels use whereIn.
1639+ $ query ->shouldReceive ('where ' )->with ('origin ' , Mockery::type ('string ' ))->andReturnSelf ();
1640+ }
1641+
1642+ $ query ->shouldReceive ('whereIn ' )->with ('origin ' , $ whereInOrigins ?? Mockery::type ('array ' ))->andReturnSelf ();
1643+ $ query ->shouldReceive ('get ' )->andReturn ($ results );
1644+
1645+ return $ query ;
1646+ }
1647+
15801648 #[Test]
15811649 public function it_doesnt_fire_events_when_propagating_entry_and_saved_quietly ()
15821650 {
0 commit comments