Skip to content

Commit a91e040

Browse files
committed
Merge branch 'feat-relationship-updates'
2 parents e318e54 + f1307e0 commit a91e040

File tree

2 files changed

+119
-8
lines changed

2 files changed

+119
-8
lines changed

src/Database/Validator/Query/Order.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ protected function isValidAttribute(string $attribute): bool
3737
// For relationships, just validate the top level.
3838
// Will validate each nested level during the recursive calls.
3939
$attribute = \explode('.', $attribute)[0];
40+
41+
if (isset($this->schema[$attribute])) {
42+
$this->message = 'Cannot order by nested attribute: ' . $attribute;
43+
return false;
44+
}
4045
}
4146

4247
// Search for attribute in schema

tests/e2e/Adapter/Scopes/RelationshipTests.php

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)