@@ -4344,51 +4344,51 @@ public function testCountAndSumWithRelationshipQueries(): void
43444344 'author ' => 'author2 ' ,
43454345 ]));
43464346
4347- // Test 1: Count posts by author name
4347+ // Count posts by author name
43484348 $ count = $ database ->count ('postsCount ' , [
43494349 Query::equal ('author.name ' , ['Alice ' ]),
43504350 ]);
43514351 $ this ->assertEquals (3 , $ count );
43524352
4353- // Test 2: Count published posts by author age filter
4353+ // Count published posts by author age filter
43544354 $ count = $ database ->count ('postsCount ' , [
43554355 Query::lessThan ('author.age ' , 30 ),
43564356 Query::equal ('published ' , [true ]),
43574357 ]);
43584358 $ this ->assertEquals (1 , $ count ); // Only Bob's published post
43594359
4360- // Test 3: Count posts by author name (different author)
4360+ // Count posts by author name (different author)
43614361 $ count = $ database ->count ('postsCount ' , [
43624362 Query::equal ('author.name ' , ['Bob ' ]),
43634363 ]);
43644364 $ this ->assertEquals (2 , $ count );
43654365
4366- // Test 4: Count with no matches (author with no posts)
4366+ // Count with no matches (author with no posts)
43674367 $ count = $ database ->count ('postsCount ' , [
43684368 Query::equal ('author.name ' , ['Charlie ' ]),
43694369 ]);
43704370 $ this ->assertEquals (0 , $ count );
43714371
4372- // Test 5: Sum views for posts by author name
4372+ // Sum views for posts by author name
43734373 $ sum = $ database ->sum ('postsCount ' , 'views ' , [
43744374 Query::equal ('author.name ' , ['Alice ' ]),
43754375 ]);
43764376 $ this ->assertEquals (350 , $ sum ); // 100 + 200 + 50
43774377
4378- // Test 6: Sum views for published posts by author age
4378+ // Sum views for published posts by author age
43794379 $ sum = $ database ->sum ('postsCount ' , 'views ' , [
43804380 Query::lessThan ('author.age ' , 30 ),
43814381 Query::equal ('published ' , [true ]),
43824382 ]);
43834383 $ this ->assertEquals (150 , $ sum ); // Only Bob's published post
43844384
4385- // Test 7: Sum views for Bob's posts
4385+ // Sum views for Bob's posts
43864386 $ sum = $ database ->sum ('postsCount ' , 'views ' , [
43874387 Query::equal ('author.name ' , ['Bob ' ]),
43884388 ]);
43894389 $ this ->assertEquals (225 , $ sum ); // 150 + 75
43904390
4391- // Test 8: Sum with no matches
4391+ // Sum with no matches
43924392 $ sum = $ database ->sum ('postsCount ' , 'views ' , [
43934393 Query::equal ('author.name ' , ['Charlie ' ]),
43944394 ]);
@@ -4398,4 +4398,110 @@ public function testCountAndSumWithRelationshipQueries(): void
43984398 $ database ->deleteCollection ('authorsCount ' );
43994399 $ database ->deleteCollection ('postsCount ' );
44004400 }
4401+
4402+ /**
4403+ // and cursor queries properly reject relationship (dot-path) attributes.
4404+ *
4405+ * Relationship attributes like 'author.name' are NOT supported for ordering because:
4406+ * 1. Only filter queries go through convertRelationshipFiltersToSubqueries()
4407+ * 2. Order attributes are passed directly to the adapter without relationship resolution
4408+ * 3. The Order validator now catches dot-path attributes and rejects them with a clear error
4409+ * 4. Cursor validation doesn't need separate dot-path checks since order validation runs first
4410+ */
4411+ public function testOrderAndCursorWithRelationshipQueries (): void
4412+ {
4413+ /** @var Database $database */
4414+ $ database = static ::getDatabase ();
4415+
4416+ $ database ->createCollection ('authorsOrder ' );
4417+ $ database ->createCollection ('postsOrder ' );
4418+
4419+ $ database ->createAttribute ('authorsOrder ' , 'name ' , Database::VAR_STRING , 255 , true );
4420+ $ database ->createAttribute ('authorsOrder ' , 'age ' , Database::VAR_INTEGER , 0 , true );
4421+
4422+ $ database ->createAttribute ('postsOrder ' , 'title ' , Database::VAR_STRING , 255 , true );
4423+ $ database ->createAttribute ('postsOrder ' , 'views ' , Database::VAR_INTEGER , 0 , true );
4424+
4425+ $ database ->createRelationship (
4426+ collection: 'postsOrder ' ,
4427+ relatedCollection: 'authorsOrder ' ,
4428+ type: Database::RELATION_MANY_TO_ONE ,
4429+ twoWay: true ,
4430+ id: 'author ' ,
4431+ twoWayKey: 'postsOrder '
4432+ );
4433+
4434+ // Create authors
4435+ $ alice = $ database ->createDocument ('authorsOrder ' , new Document ([
4436+ '$permissions ' => [
4437+ Permission::read (Role::any ()),
4438+ ],
4439+ 'name ' => 'Alice ' ,
4440+ 'age ' => 30 ,
4441+ ]));
4442+
4443+ $ bob = $ database ->createDocument ('authorsOrder ' , new Document ([
4444+ '$permissions ' => [
4445+ Permission::read (Role::any ()),
4446+ ],
4447+ 'name ' => 'Bob ' ,
4448+ 'age ' => 25 ,
4449+ ]));
4450+
4451+ // Create posts
4452+ $ database ->createDocument ('postsOrder ' , new Document ([
4453+ '$permissions ' => [Permission::read (Role::any ())],
4454+ 'title ' => 'Post 1 ' ,
4455+ 'views ' => 100 ,
4456+ 'author ' => $ alice ->getId (),
4457+ ]));
4458+
4459+ $ database ->createDocument ('postsOrder ' , new Document ([
4460+ '$permissions ' => [Permission::read (Role::any ())],
4461+ 'title ' => 'Post 2 ' ,
4462+ 'views ' => 200 ,
4463+ 'author ' => $ bob ->getId (),
4464+ ]));
4465+
4466+ $ database ->createDocument ('postsOrder ' , new Document ([
4467+ '$permissions ' => [Permission::read (Role::any ())],
4468+ 'title ' => 'Post 3 ' ,
4469+ 'views ' => 150 ,
4470+ 'author ' => $ alice ->getId (),
4471+ ]));
4472+
4473+ // Order by relationship attribute should fail with validation error
4474+ $ caught = false ;
4475+ try {
4476+ $ database ->find ('postsOrder ' , [
4477+ Query::orderAsc ('author.name ' )
4478+ ]);
4479+ } catch (\Throwable $ e ) {
4480+ $ caught = true ;
4481+ $ this ->assertStringContainsString ('Cannot order by nested attribute ' , $ e ->getMessage ());
4482+ }
4483+ $ this ->assertTrue ($ caught , 'Should throw exception for nested order attribute ' );
4484+
4485+ // Cursor with relationship order attribute should fail with same validation error
4486+ $ caught = false ;
4487+ try {
4488+ $ firstPost = $ database ->findOne ('postsOrder ' , [
4489+ Query::orderAsc ('title ' )
4490+ ]);
4491+
4492+ $ database ->find ('postsOrder ' , [
4493+ Query::orderAsc ('author.name ' ),
4494+ Query::cursorAfter ($ firstPost )
4495+ ]);
4496+ } catch (\Throwable $ e ) {
4497+ $ caught = true ;
4498+ $ this ->assertStringContainsString ('Cannot order by nested attribute ' , $ e ->getMessage ());
4499+ }
4500+ $ this ->assertTrue ($ caught , 'Should throw exception for nested order attribute with cursor ' );
4501+
4502+
4503+ // Clean up
4504+ $ database ->deleteCollection ('authorsOrder ' );
4505+ $ database ->deleteCollection ('postsOrder ' );
4506+ }
44014507}
0 commit comments