Skip to content

Commit 4fc76ee

Browse files
abnegateclaude
andcommitted
Fix M2M relationship queries returning 0 results inside skipRelationships context
When Appwrite wraps find() in skipRelationships() (for requests without select queries), resolveRelationships is set to false for the entire call chain. The inner find() in resolveRelationshipGroupToIds relied on relationship population to read the twoWayKey attribute for M2M parent resolution - but with resolveRelationships=false, the twoWayKey was never populated, causing 0 results. Fix: For M2M, query the junction table directly (same pattern used by processNestedRelationshipPath) instead of relying on relationship population. This works regardless of the resolveRelationships flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e3305f6 commit 4fc76ee

File tree

2 files changed

+80
-14
lines changed

2 files changed

+80
-14
lines changed

src/Database/Database.php

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7864,7 +7864,7 @@ public function find(string $collection, array $queries = [], string $forPermiss
78647864
$nestedSelections = $this->processRelationshipQueries($relationships, $queries);
78657865

78667866
// Convert relationship filter queries to SQL-level subqueries
7867-
$queriesOrNull = $this->convertRelationshipQueries($relationships, $queries);
7867+
$queriesOrNull = $this->convertRelationshipQueries($relationships, $queries, $collection);
78687868

78697869
// If conversion returns null, it means no documents can match (relationship filter found no matches)
78707870
if ($queriesOrNull === null) {
@@ -8064,7 +8064,7 @@ public function count(string $collection, array $queries = [], ?int $max = null)
80648064
$queries = Query::groupByType($queries)['filters'];
80658065
$queries = $this->convertQueries($collection, $queries);
80668066

8067-
$queriesOrNull = $this->convertRelationshipQueries($relationships, $queries);
8067+
$queriesOrNull = $this->convertRelationshipQueries($relationships, $queries, $collection);
80688068

80698069
if ($queriesOrNull === null) {
80708070
return 0;
@@ -8130,7 +8130,7 @@ public function sum(string $collection, string $attribute, array $queries = [],
81308130
);
81318131

81328132
$queries = $this->convertQueries($collection, $queries);
8133-
$queriesOrNull = $this->convertRelationshipQueries($relationships, $queries);
8133+
$queriesOrNull = $this->convertRelationshipQueries($relationships, $queries, $collection);
81348134

81358135
// If conversion returns null, it means no documents can match (relationship filter found no matches)
81368136
if ($queriesOrNull === null) {
@@ -9084,6 +9084,7 @@ private function processNestedRelationshipPath(string $startCollection, array $q
90849084
private function convertRelationshipQueries(
90859085
array $relationships,
90869086
array $queries,
9087+
?Document $collection = null,
90879088
): ?array {
90889089
// Early return if no relationship queries exist
90899090
$hasRelationshipQuery = false;
@@ -9134,7 +9135,7 @@ private function convertRelationshipQueries(
91349135
$resolvedAttribute = '$id';
91359136
foreach ($query->getValues() as $value) {
91369137
$relatedQuery = Query::equal($nestedAttribute, [$value]);
9137-
$result = $this->resolveRelationshipGroupToIds($relationship, [$relatedQuery]);
9138+
$result = $this->resolveRelationshipGroupToIds($relationship, [$relatedQuery], $collection);
91389139

91399140
if ($result === null) {
91409141
return null;
@@ -9220,7 +9221,7 @@ private function convertRelationshipQueries(
92209221
}
92219222

92229223
try {
9223-
$result = $this->resolveRelationshipGroupToIds($relationship, $relatedQueries);
9224+
$result = $this->resolveRelationshipGroupToIds($relationship, $relatedQueries, $collection);
92249225

92259226
if ($result === null) {
92269227
return null;
@@ -9252,11 +9253,13 @@ private function convertRelationshipQueries(
92529253
*
92539254
* @param Document $relationship
92549255
* @param array<Query> $relatedQueries Queries on the related collection
9256+
* @param Document|null $collection The parent collection document (needed for junction table lookups)
92559257
* @return array{attribute: string, ids: string[]}|null
92569258
*/
92579259
private function resolveRelationshipGroupToIds(
92589260
Document $relationship,
92599261
array $relatedQueries,
9262+
?Document $collection = null,
92609263
): ?array {
92619264
$relatedCollection = $relationship->getAttribute('options')['relatedCollection'];
92629265
$relationType = $relationship->getAttribute('options')['relationType'];
@@ -9294,24 +9297,52 @@ private function resolveRelationshipGroupToIds(
92949297
($relationType === self::RELATION_MANY_TO_MANY)
92959298
);
92969299

9297-
if ($needsParentResolution) {
9298-
$matchingDocs = $this->silent(fn () => $this->find(
9300+
if ($relationType === self::RELATION_MANY_TO_MANY && $needsParentResolution && $collection !== null) {
9301+
// For many-to-many, query the junction table directly instead of relying
9302+
// on relationship population (which fails when resolveRelationships is false,
9303+
// e.g. when the outer find() is wrapped in skipRelationships()).
9304+
$matchingDocs = $this->silent(fn () => $this->skipRelationships(fn () => $this->find(
92999305
$relatedCollection,
93009306
\array_merge($relatedQueries, [
9307+
Query::select(['$id']),
93019308
Query::limit(PHP_INT_MAX),
93029309
])
9303-
));
9304-
} else {
9305-
$matchingDocs = $this->silent(fn () => $this->skipRelationships(fn () => $this->find(
9310+
)));
9311+
9312+
$matchingIds = \array_map(fn ($doc) => $doc->getId(), $matchingDocs);
9313+
9314+
if (empty($matchingIds)) {
9315+
return null;
9316+
}
9317+
9318+
$twoWayKey = $relationship->getAttribute('options')['twoWayKey'];
9319+
$relatedCollectionDoc = $this->silent(fn () => $this->getCollection($relatedCollection));
9320+
$junction = $this->getJunctionCollection($collection, $relatedCollectionDoc, $side);
9321+
9322+
$junctionDocs = $this->silent(fn () => $this->skipRelationships(fn () => $this->find($junction, [
9323+
Query::equal($relationshipKey, $matchingIds),
9324+
Query::limit(PHP_INT_MAX),
9325+
])));
9326+
9327+
$parentIds = [];
9328+
foreach ($junctionDocs as $jDoc) {
9329+
$pId = $jDoc->getAttribute($twoWayKey);
9330+
if ($pId && !\in_array($pId, $parentIds)) {
9331+
$parentIds[] = $pId;
9332+
}
9333+
}
9334+
9335+
return empty($parentIds) ? null : ['attribute' => '$id', 'ids' => $parentIds];
9336+
} elseif ($needsParentResolution) {
9337+
// For one-to-many/many-to-one parent resolution, we need relationship
9338+
// population to read the twoWayKey attribute from the related documents.
9339+
$matchingDocs = $this->silent(fn () => $this->find(
93069340
$relatedCollection,
93079341
\array_merge($relatedQueries, [
9308-
Query::select(['$id']),
93099342
Query::limit(PHP_INT_MAX),
93109343
])
9311-
)));
9312-
}
9344+
));
93139345

9314-
if ($needsParentResolution) {
93159346
$twoWayKey = $relationship->getAttribute('options')['twoWayKey'];
93169347
$parentIds = [];
93179348

@@ -9339,6 +9370,14 @@ private function resolveRelationshipGroupToIds(
93399370

93409371
return empty($parentIds) ? null : ['attribute' => '$id', 'ids' => $parentIds];
93419372
} else {
9373+
$matchingDocs = $this->silent(fn () => $this->skipRelationships(fn () => $this->find(
9374+
$relatedCollection,
9375+
\array_merge($relatedQueries, [
9376+
Query::select(['$id']),
9377+
Query::limit(PHP_INT_MAX),
9378+
])
9379+
)));
9380+
93429381
$matchingIds = \array_map(fn ($doc) => $doc->getId(), $matchingDocs);
93439382
return empty($matchingIds) ? null : ['attribute' => $relationshipKey, 'ids' => $matchingIds];
93449383
}

tests/e2e/Adapter/Scopes/RelationshipTests.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3533,6 +3533,33 @@ public function testQueryByRelationshipId(): void
35333533
$this->assertStringContainsString('Query::containsAll()', $e->getMessage());
35343534
}
35353535

3536+
// Test M2M relationship query inside skipRelationships context
3537+
// This simulates Appwrite's XList.php which wraps find() in skipRelationships()
3538+
// when no select queries are provided
3539+
$projects = $database->skipRelationships(fn () => $database->find('projectsMtmId', [
3540+
Query::equal('developers.$id', ['dev1']),
3541+
]));
3542+
$this->assertCount(2, $projects);
3543+
3544+
$projects = $database->skipRelationships(fn () => $database->find('projectsMtmId', [
3545+
Query::equal('developers.$id', ['dev2']),
3546+
]));
3547+
$this->assertCount(1, $projects);
3548+
$this->assertEquals('project1', $projects[0]->getId());
3549+
3550+
// Also test inverse direction inside skipRelationships
3551+
$developers = $database->skipRelationships(fn () => $database->find('developersMtmId', [
3552+
Query::equal('projects.$id', ['project1']),
3553+
]));
3554+
$this->assertCount(2, $developers);
3555+
3556+
// Test containsAll inside skipRelationships
3557+
$projects = $database->skipRelationships(fn () => $database->find('projectsMtmId', [
3558+
Query::containsAll('developers.$id', ['dev1', 'dev2']),
3559+
]));
3560+
$this->assertCount(1, $projects);
3561+
$this->assertEquals('project1', $projects[0]->getId());
3562+
35363563
// Clean up MANY_TO_MANY test
35373564
$database->deleteCollection('developersMtmId');
35383565
$database->deleteCollection('projectsMtmId');

0 commit comments

Comments
 (0)