@@ -2250,4 +2250,154 @@ public function testManyToManyRelationshipWithArrayOperators(): void
22502250 $ database ->deleteCollection ('library ' );
22512251 $ database ->deleteCollection ('book ' );
22522252 }
2253+
2254+ /**
2255+ * Regression: processNestedRelationshipPath used skipRelationships()
2256+ * for many-to-many reverse lookups, which prevented junction-table data
2257+ * (twoWayKey) from being populated, yielding empty matchingIds.
2258+ */
2259+ public function testNestedManyToManyRelationshipQueries (): void
2260+ {
2261+ /** @var Database $database */
2262+ $ database = $ this ->getDatabase ();
2263+
2264+ if (!$ database ->getAdapter ()->getSupportForRelationships ()) {
2265+ $ this ->expectNotToPerformAssertions ();
2266+ return ;
2267+ }
2268+
2269+ // 3-level many-to-many chain: brands <-> products <-> tags
2270+ $ database ->createCollection ('brands ' );
2271+ $ database ->createCollection ('products ' );
2272+ $ database ->createCollection ('tags ' );
2273+
2274+ $ database ->createAttribute ('brands ' , 'name ' , Database::VAR_STRING , 255 , true );
2275+ $ database ->createAttribute ('products ' , 'title ' , Database::VAR_STRING , 255 , true );
2276+ $ database ->createAttribute ('tags ' , 'label ' , Database::VAR_STRING , 255 , true );
2277+
2278+ $ database ->createRelationship (
2279+ collection: 'brands ' ,
2280+ relatedCollection: 'products ' ,
2281+ type: Database::RELATION_MANY_TO_MANY ,
2282+ twoWay: true ,
2283+ id: 'products ' ,
2284+ twoWayKey: 'brands ' ,
2285+ );
2286+
2287+ $ database ->createRelationship (
2288+ collection: 'products ' ,
2289+ relatedCollection: 'tags ' ,
2290+ type: Database::RELATION_MANY_TO_MANY ,
2291+ twoWay: true ,
2292+ id: 'tags ' ,
2293+ twoWayKey: 'products ' ,
2294+ );
2295+
2296+ // Seed data
2297+ $ database ->createDocument ('tags ' , new Document ([
2298+ '$id ' => 'tag_eco ' ,
2299+ '$permissions ' => [Permission::read (Role::any ())],
2300+ 'label ' => 'Eco-Friendly ' ,
2301+ ]));
2302+ $ database ->createDocument ('tags ' , new Document ([
2303+ '$id ' => 'tag_premium ' ,
2304+ '$permissions ' => [Permission::read (Role::any ())],
2305+ 'label ' => 'Premium ' ,
2306+ ]));
2307+ $ database ->createDocument ('tags ' , new Document ([
2308+ '$id ' => 'tag_sale ' ,
2309+ '$permissions ' => [Permission::read (Role::any ())],
2310+ 'label ' => 'Sale ' ,
2311+ ]));
2312+
2313+ $ database ->createDocument ('products ' , new Document ([
2314+ '$id ' => 'prod_a ' ,
2315+ '$permissions ' => [Permission::read (Role::any ())],
2316+ 'title ' => 'Product A ' ,
2317+ 'tags ' => ['tag_eco ' , 'tag_premium ' ],
2318+ ]));
2319+ $ database ->createDocument ('products ' , new Document ([
2320+ '$id ' => 'prod_b ' ,
2321+ '$permissions ' => [Permission::read (Role::any ())],
2322+ 'title ' => 'Product B ' ,
2323+ 'tags ' => ['tag_sale ' ],
2324+ ]));
2325+ $ database ->createDocument ('products ' , new Document ([
2326+ '$id ' => 'prod_c ' ,
2327+ '$permissions ' => [Permission::read (Role::any ())],
2328+ 'title ' => 'Product C ' ,
2329+ 'tags ' => ['tag_eco ' ],
2330+ ]));
2331+
2332+ $ database ->createDocument ('brands ' , new Document ([
2333+ '$id ' => 'brand_x ' ,
2334+ '$permissions ' => [Permission::read (Role::any ())],
2335+ 'name ' => 'BrandX ' ,
2336+ 'products ' => ['prod_a ' , 'prod_b ' ],
2337+ ]));
2338+ $ database ->createDocument ('brands ' , new Document ([
2339+ '$id ' => 'brand_y ' ,
2340+ '$permissions ' => [Permission::read (Role::any ())],
2341+ 'name ' => 'BrandY ' ,
2342+ 'products ' => ['prod_c ' ],
2343+ ]));
2344+
2345+ // --- 1-level deep: query brands by product title (many-to-many) ---
2346+ $ brands = $ database ->find ('brands ' , [
2347+ Query::equal ('products.title ' , ['Product A ' ]),
2348+ ]);
2349+ $ this ->assertCount (1 , $ brands );
2350+ $ this ->assertEquals ('brand_x ' , $ brands [0 ]->getId ());
2351+
2352+ // --- 2-level deep: query brands by product→tag label (many-to-many→many-to-many) ---
2353+ // "Eco-Friendly" tag is on prod_a (BrandX) and prod_c (BrandY)
2354+ $ brands = $ database ->find ('brands ' , [
2355+ Query::equal ('products.tags.label ' , ['Eco-Friendly ' ]),
2356+ ]);
2357+ $ this ->assertCount (2 , $ brands );
2358+ $ brandIds = \array_map (fn ($ d ) => $ d ->getId (), $ brands );
2359+ $ this ->assertContains ('brand_x ' , $ brandIds );
2360+ $ this ->assertContains ('brand_y ' , $ brandIds );
2361+
2362+ // "Sale" tag is only on prod_b (BrandX)
2363+ $ brands = $ database ->find ('brands ' , [
2364+ Query::equal ('products.tags.label ' , ['Sale ' ]),
2365+ ]);
2366+ $ this ->assertCount (1 , $ brands );
2367+ $ this ->assertEquals ('brand_x ' , $ brands [0 ]->getId ());
2368+
2369+ // "Premium" tag is only on prod_a (BrandX)
2370+ $ brands = $ database ->find ('brands ' , [
2371+ Query::equal ('products.tags.label ' , ['Premium ' ]),
2372+ ]);
2373+ $ this ->assertCount (1 , $ brands );
2374+ $ this ->assertEquals ('brand_x ' , $ brands [0 ]->getId ());
2375+
2376+ // --- 2-level deep from the child side: query tags by product→brand name ---
2377+ $ tags = $ database ->find ('tags ' , [
2378+ Query::equal ('products.brands.name ' , ['BrandY ' ]),
2379+ ]);
2380+ $ this ->assertCount (1 , $ tags );
2381+ $ this ->assertEquals ('tag_eco ' , $ tags [0 ]->getId ());
2382+
2383+ $ tags = $ database ->find ('tags ' , [
2384+ Query::equal ('products.brands.name ' , ['BrandX ' ]),
2385+ ]);
2386+ $ this ->assertCount (3 , $ tags );
2387+ $ tagIds = \array_map (fn ($ d ) => $ d ->getId (), $ tags );
2388+ $ this ->assertContains ('tag_eco ' , $ tagIds );
2389+ $ this ->assertContains ('tag_premium ' , $ tagIds );
2390+ $ this ->assertContains ('tag_sale ' , $ tagIds );
2391+
2392+ // --- No match returns empty ---
2393+ $ brands = $ database ->find ('brands ' , [
2394+ Query::equal ('products.tags.label ' , ['NonExistent ' ]),
2395+ ]);
2396+ $ this ->assertCount (0 , $ brands );
2397+
2398+ // Cleanup
2399+ $ database ->deleteCollection ('brands ' );
2400+ $ database ->deleteCollection ('products ' );
2401+ $ database ->deleteCollection ('tags ' );
2402+ }
22532403}
0 commit comments