Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 25 additions & 24 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -2975,21 +2975,17 @@ public function getDocument(string $collection, string $id, array $queries = [],
fn (Document $attribute) => $attribute->getAttribute('type') === self::VAR_RELATIONSHIP
);

// actual queries are mutated later, so we clone!
$originalQueries = array_map(fn ($q) => clone $q, $queries);

$selects = Query::groupByType($queries)['selections'];
$selections = $this->validateSelections($collection, $selects);
$nestedSelections = [];

foreach ($queries as $query) {
if ($query->getMethod() == Query::TYPE_SELECT) {
$values = $query->getValues();
foreach ($values as $valueIndex => $value) {
if (\str_contains($value, '.')) {
// Shift the top level off the dot-path to pass the selection down the chain
// 'foo.bar.baz' becomes 'bar.baz'
$nestedSelections[] = Query::select([
\implode('.', \array_slice(\explode('.', $value), 1))
]);

$key = \explode('.', $value)[0];

foreach ($relationships as $relationship) {
Expand Down Expand Up @@ -3078,8 +3074,8 @@ public function getDocument(string $collection, string $id, array $queries = [],
$document = $this->decode($collection, $document, $selections);
$this->map = [];

if ($this->resolveRelationships && (empty($selects) || !empty($nestedSelections))) {
$document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document, $nestedSelections));
if ($this->resolveRelationships && Query::hasNestedSelect($originalQueries)) {
$document = $this->silent(fn () => $this->populateDocumentRelationships($collection, $document, $originalQueries));
}

$relationships = \array_filter(
Expand Down Expand Up @@ -3133,6 +3129,16 @@ private function populateDocumentRelationships(Document $collection, Document $d

foreach ($relationships as $relationship) {
$key = $relationship['key'];

// Only continue if this relationship is requested!
$selects = Query::filterSelectsByPrefix($queries, $key);

// Skip resolving if respecting nested queries AND no fields were selected
if (empty($selects)) {
$document->removeAttribute($key);
continue;
}

$value = $document->getAttribute($key);
$relatedCollection = $this->getCollection($relationship['options']['relatedCollection']);
$relationType = $relationship['options']['relationType'];
Expand Down Expand Up @@ -3211,7 +3217,7 @@ private function populateDocumentRelationships(Document $collection, Document $d
$this->relationshipFetchDepth++;
$this->relationshipFetchStack[] = $relationship;

$related = $this->getDocument($relatedCollection->getId(), $value, $queries);
$related = $this->getDocument($relatedCollection->getId(), $value, $selects);

$this->relationshipFetchDepth--;
\array_pop($this->relationshipFetchStack);
Expand All @@ -3228,7 +3234,7 @@ private function populateDocumentRelationships(Document $collection, Document $d
$this->relationshipFetchDepth++;
$this->relationshipFetchStack[] = $relationship;

$related = $this->getDocument($relatedCollection->getId(), $value, $queries);
$related = $this->getDocument($relatedCollection->getId(), $value, $selects);

$this->relationshipFetchDepth--;
\array_pop($this->relationshipFetchStack);
Expand All @@ -3248,7 +3254,7 @@ private function populateDocumentRelationships(Document $collection, Document $d
$relatedDocuments = $this->find($relatedCollection->getId(), [
Query::equal($twoWayKey, [$document->getId()]),
Query::limit(PHP_INT_MAX),
...$queries
...$selects
]);

$this->relationshipFetchDepth--;
Expand All @@ -3273,7 +3279,7 @@ private function populateDocumentRelationships(Document $collection, Document $d
$this->relationshipFetchDepth++;
$this->relationshipFetchStack[] = $relationship;

$related = $this->getDocument($relatedCollection->getId(), $value, $queries);
$related = $this->getDocument($relatedCollection->getId(), $value, $selects);

$this->relationshipFetchDepth--;
\array_pop($this->relationshipFetchStack);
Expand All @@ -3297,7 +3303,7 @@ private function populateDocumentRelationships(Document $collection, Document $d
$relatedDocuments = $this->find($relatedCollection->getId(), [
Query::equal($twoWayKey, [$document->getId()]),
Query::limit(PHP_INT_MAX),
...$queries
...$selects
]);

$this->relationshipFetchDepth--;
Expand Down Expand Up @@ -3334,7 +3340,7 @@ private function populateDocumentRelationships(Document $collection, Document $d
$related[] = $this->getDocument(
$relatedCollection->getId(),
$junction->getAttribute($key),
$queries
$selects
);
}

Expand Down Expand Up @@ -5733,21 +5739,16 @@ public function find(string $collection, array $queries = [], string $forPermiss
self::convertQueries($collection, $filters)
);

// actual queries are mutated later, so we clone!
$originalQueries = array_map(fn ($q) => clone $q, $queries);
$selections = $this->validateSelections($collection, $selects);
$nestedSelections = [];

foreach ($queries as $index => &$query) {
switch ($query->getMethod()) {
case Query::TYPE_SELECT:
$values = $query->getValues();
foreach ($values as $valueIndex => $value) {
if (\str_contains($value, '.')) {
// Shift the top level off the dot-path to pass the selection down the chain
// 'foo.bar.baz' becomes 'bar.baz'
$nestedSelections[] = Query::select([
\implode('.', \array_slice(\explode('.', $value), 1))
]);

$key = \explode('.', $value)[0];

foreach ($relationships as $relationship) {
Expand Down Expand Up @@ -5794,8 +5795,8 @@ public function find(string $collection, array $queries = [], string $forPermiss
$results = $skipAuth ? Authorization::skip($getResults) : $getResults();

foreach ($results as &$node) {
if ($this->resolveRelationships && (empty($selects) || !empty($nestedSelections))) {
$node = $this->silent(fn () => $this->populateDocumentRelationships($collection, $node, $nestedSelections));
if ($this->resolveRelationships && Query::hasNestedSelect($originalQueries)) {
$node = $this->silent(fn () => $this->populateDocumentRelationships($collection, $node, $originalQueries));
}
$node = $this->casting($collection, $node);
$node = $this->decode($collection, $node, $selections);
Expand Down
53 changes: 53 additions & 0 deletions src/Database/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -727,4 +727,57 @@ public function setOnArray(bool $bool): void
{
$this->onArray = $bool;
}

/**
* Extracts and rewrites select fields for a specific relationship key.
*
* Example:
* `select(['actors.name', 'actors.age'])` → `select(['name', 'age'])`
*
* @param array<Query> $queries
* @return array<Query>
*/
public static function filterSelectsByPrefix(array $queries, string $prefix): array
{
$result = [];

foreach ($queries as $query) {
if ($query->getMethod() !== self::TYPE_SELECT) {
continue;
}

$filtered = array_filter($query->getValues(), fn ($v) => str_starts_with($v, "$prefix."));
$stripped = array_map(fn ($v) => substr($v, strlen($prefix) + 1), $filtered);

if (! empty($stripped)) {
$result[] = self::select($stripped);
}
}

return $result;
}

/**
* Checks if any select query contains a nested field (dot notation).
*
* This determines whether relationship resolution is required.
* For example, `select(['actors.name'])` is considered a nested select.
*
* @param array<Query> $queries
* @return bool
*/
public static function hasNestedSelect(array $queries): bool
{
foreach ($queries as $query) {
if ($query->getMethod() === self::TYPE_SELECT) {
foreach ($query->getValues() as $value) {
if (str_contains($value, '.')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A non relationship regular attribute can contain dots as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a check later down in the get related documents method that checks for key sided selects.

return true;
}
}
}
}

return false;
}
}
Loading