Skip to content
Merged
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
149 changes: 109 additions & 40 deletions src/Database/Hook/Relationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,10 @@ private function collectCreated(PreparedCreate $prepared, Document $collection,
continue;
}

$relatedCollection = $this->collection($prepared, $relationship->relatedCollection);
$relatedCollection = $this->relatedCollection($relationship, $prepared);
if ($relatedCollection === null) {
continue;
}

if ($depth >= Database::RELATION_MAX_DEPTH - 1 && $writeStack[$depth - 1] !== $relatedCollection->getId()) {
continue;
Expand Down Expand Up @@ -701,6 +704,57 @@ private function collection(PreparedCreate $prepared, string $id): Document
return $prepared->collections[$id] ??= $this->database->getCollection($id);
}

/**
* The collection a relationship relates to, or null when it was deleted while the relationship was left behind,
* as 7.x could leave it: such a relationship is not related, populated or cascaded, and keeps its stored value.
*/
private function relatedCollection(Relationship $relationship, ?PreparedCreate $prepared = null): ?Document
{
$id = $relationship->relatedCollection;
if ($prepared !== null && isset($prepared->collections[$id])) {
return $prepared->collections[$id];
}

$collection = $this->database->findCollection($id);
if ($prepared !== null && $collection !== null) {
$prepared->collections[$id] = $collection;
}

return $collection;
}

/**
* Keep the value written to a relationship whose related collection was deleted as the side holding its key
* stores it, by ID, and drop it from a side that stores nothing.
*/
private static function detach(Attribute $attribute, Document $document): void
{
$key = $attribute->key;
$relationship = $attribute->relationship;
if ($relationship === null) {
return;
}

$parent = $attribute->side === RelationshipSide::Parent;
$storesKey = match ($relationship->type) {
RelationshipType::OneToOne => $parent || $relationship->twoWay,
RelationshipType::OneToMany => ! $parent,
RelationshipType::ManyToOne => $parent,
RelationshipType::ManyToMany => false,
};

if (! $storesKey) {
$document->removeAttribute($key);

return;
}

$value = $document->getAttribute($key);
if ($value instanceof Document) {
$document->setAttribute($key, $value->getId());
}
}

/**
* The attributes $collection declares, hydrated at most once per collection instance.
*
Expand Down Expand Up @@ -865,9 +919,12 @@ private function relateAttributes(Document $collection, Document $document, int
if ($copies !== null && $stackCount === 0 && (\is_array($value) || $value instanceof Document)) {
$this->copy($document, $copies);
}
$relatedCollection = $prepared === null
? $this->database->getCollection($relationship->relatedCollection)
: $this->collection($prepared, $relationship->relatedCollection);
$relatedCollection = $this->relatedCollection($relationship, $prepared);
if ($relatedCollection === null) {
self::detach($attribute, $document);

continue;
}
$relationType = $relationship->type;
$twoWay = $relationship->twoWay;
$twoWayKey = $relationship->twoWayKey ?? '';
Expand Down Expand Up @@ -1032,7 +1089,6 @@ public function afterDocumentUpdate(Document $collection, Document $old, Documen

$oldValue = $old->getAttribute($key);
$oldId = $oldValue instanceof Document ? $oldValue->getId() : (\is_string($oldValue) ? $oldValue : null);
$relatedCollection = $this->database->getCollection($relationship->relatedCollection);
$relationType = $relationship->type;
$twoWay = $relationship->twoWay;
$twoWayKey = $relationship->twoWayKey ?? '';
Expand Down Expand Up @@ -1067,6 +1123,13 @@ public function afterDocumentUpdate(Document $collection, Document $old, Documen
continue;
}

$relatedCollection = $this->relatedCollection($relationship);
if ($relatedCollection === null) {
self::detach($attribute, $document);

continue;
}

if ($stackCount >= Database::RELATION_MAX_DEPTH - 1 && $writeStack[$stackCount - 1] !== $relatedCollection->getId()) {
$document->removeAttribute($key);

Expand Down Expand Up @@ -1539,9 +1602,13 @@ public function beforeDocumentDelete(Document $collection, Document $document, b
continue;
}

$relatedCollection = $this->relatedCollection($relationship);
if ($relatedCollection === null) {
continue;
}

$key = $attribute->key;
$value = $document->getAttribute($key);
$relatedCollection = $this->database->getCollection($relationship->relatedCollection);
$relationType = $relationship->type;
$twoWay = $relationship->twoWay;
$twoWayKey = $relationship->twoWayKey ?? '';
Expand Down Expand Up @@ -1730,9 +1797,15 @@ public function populateDocuments(array $documents, Document $collection, int $f
continue;
}

$relatedCollection = $this->relatedCollection($relationship);
if ($relatedCollection === null) {
continue;
}

$relatedDocuments = $this->populateSingleRelationshipBatch(
$batchDocuments,
$batchCollection,
$relatedCollection,
$key,
$relationship,
$side,
Expand All @@ -1747,25 +1820,20 @@ public function populateDocuments(array $documents, Document $collection, int $f
($hasNestedSelects || ! $parentHasExplicitSelects);

if ($shouldQueue) {
$relatedCollectionId = $relationship->relatedCollection;
$relatedCollection = $this->database->silent(fn () => $this->database->findCollection($relatedCollectionId));

if ($relatedCollection !== null) {
$relationshipQueries = $hasNestedSelects ? $batchSelects[$key] : [];
$relationshipQueries = $hasNestedSelects ? $batchSelects[$key] : [];

$nextSelects = $this->processQueries(self::relationships($relatedCollection), $relationshipQueries);
$nextSelects = $this->processQueries(self::relationships($relatedCollection), $relationshipQueries);

$childHasExplicitSelects = $parentHasExplicitSelects || self::selects($relationshipQueries);
$childHasExplicitSelects = $parentHasExplicitSelects || self::selects($relationshipQueries);

$nextQueue[] = [
'documents' => $relatedDocuments,
'collection' => $relatedCollection,
'depth' => $currentDepth + 1,
'selects' => $nextSelects,
'skipKey' => $twoWay ? $twoWayKey : null,
'hasExplicitSelects' => $childHasExplicitSelects,
];
}
$nextQueue[] = [
'documents' => $relatedDocuments,
'collection' => $relatedCollection,
'depth' => $currentDepth + 1,
'selects' => $nextSelects,
'skipKey' => $twoWay ? $twoWayKey : null,
'hasExplicitSelects' => $childHasExplicitSelects,
];
}

if ($twoWay && ! empty($relatedDocuments)) {
Expand Down Expand Up @@ -2413,13 +2481,13 @@ private function applyRelationshipOperator(Operator $operator, array $existingId
* @param array<Query> $queries
* @return array<Document>
*/
private function populateSingleRelationshipBatch(array $documents, Document $collection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array
private function populateSingleRelationshipBatch(array $documents, Document $collection, Document $relatedCollection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array
{
return match ($relationship->type) {
RelationshipType::OneToOne => $this->populateOneToOneRelationshipsBatch($documents, $key, $relationship, $queries),
RelationshipType::OneToMany => $this->populateOneToManyRelationshipsBatch($documents, $key, $relationship, $side, $queries),
RelationshipType::ManyToOne => $this->populateManyToOneRelationshipsBatch($documents, $key, $relationship, $side, $queries),
RelationshipType::ManyToMany => $this->populateManyToManyRelationshipsBatch($documents, $collection, $key, $relationship, $side, $queries),
RelationshipType::OneToOne => $this->populateOneToOneRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $queries),
RelationshipType::OneToMany => $this->populateOneToManyRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $side, $queries),
RelationshipType::ManyToOne => $this->populateManyToOneRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $side, $queries),
RelationshipType::ManyToMany => $this->populateManyToManyRelationshipsBatch($documents, $collection, $relatedCollection, $key, $relationship, $side, $queries),
};
}

Expand All @@ -2428,10 +2496,8 @@ private function populateSingleRelationshipBatch(array $documents, Document $col
* @param array<Query> $queries
* @return array<Document>
*/
private function populateOneToOneRelationshipsBatch(array $documents, string $key, Relationship $relationship, array $queries): array
private function populateOneToOneRelationshipsBatch(array $documents, Document $relatedCollection, string $key, Relationship $relationship, array $queries): array
{
$relatedCollection = $this->database->getCollection($relationship->relatedCollection);

$relatedIds = [];
$documentsByRelatedId = [];

Expand Down Expand Up @@ -2505,10 +2571,9 @@ private function populateOneToOneRelationshipsBatch(array $documents, string $ke
* @param array<Query> $queries
* @return array<Document>
*/
private function populateOneToManyRelationshipsBatch(array $documents, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array
private function populateOneToManyRelationshipsBatch(array $documents, Document $relatedCollection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array
{
$twoWayKey = $relationship->twoWayKey ?? '';
$relatedCollection = $this->database->getCollection($relationship->relatedCollection);

if ($side === RelationshipSide::Child) {
if (! $relationship->twoWay) {
Expand All @@ -2519,7 +2584,7 @@ private function populateOneToManyRelationshipsBatch(array $documents, string $k
return [];
}

return $this->populateOneToOneRelationshipsBatch($documents, $key, $relationship, $queries);
return $this->populateOneToOneRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $queries);
}

$parentIds = [];
Expand Down Expand Up @@ -2583,13 +2648,12 @@ private function populateOneToManyRelationshipsBatch(array $documents, string $k
* @param array<Query> $queries
* @return array<Document>
*/
private function populateManyToOneRelationshipsBatch(array $documents, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array
private function populateManyToOneRelationshipsBatch(array $documents, Document $relatedCollection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array
{
$twoWayKey = $relationship->twoWayKey ?? '';
$relatedCollection = $this->database->getCollection($relationship->relatedCollection);

if ($side === RelationshipSide::Parent) {
return $this->populateOneToOneRelationshipsBatch($documents, $key, $relationship, $queries);
return $this->populateOneToOneRelationshipsBatch($documents, $relatedCollection, $key, $relationship, $queries);
}

if (! $relationship->twoWay) {
Expand Down Expand Up @@ -2661,10 +2725,9 @@ private function populateManyToOneRelationshipsBatch(array $documents, string $k
* @param array<Query> $queries
* @return array<Document>
*/
private function populateManyToManyRelationshipsBatch(array $documents, Document $collection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array
private function populateManyToManyRelationshipsBatch(array $documents, Document $collection, Document $relatedCollection, string $key, Relationship $relationship, RelationshipSide $side, array $queries): array
{
$twoWayKey = $relationship->twoWayKey ?? '';
$relatedCollection = $this->database->getCollection($relationship->relatedCollection);

if (! $relationship->twoWay && $side === RelationshipSide::Child) {
return [];
Expand Down Expand Up @@ -3389,7 +3452,10 @@ private function prepareNestedValues(Document $collection, Document $document, a
continue;
}

$relatedCollection = $this->database->getCollection($relationship->relatedCollection);
$relatedCollection = $this->relatedCollection($relationship);
if ($relatedCollection === null) {
continue;
}
if ($depth >= Database::RELATION_MAX_DEPTH - 1 && $writeStack[$depth - 1] !== $relatedCollection->getId()) {
continue;
}
Expand Down Expand Up @@ -3537,7 +3603,10 @@ private function authorizeRelated(Document $collection, Document $document, ?Doc
continue;
}

$relatedCollection = $this->database->getCollection($relationship->relatedCollection);
$relatedCollection = $this->relatedCollection($relationship);
if ($relatedCollection === null) {
continue;
}
if ($depth >= Database::RELATION_MAX_DEPTH - 1 && $writeStack[$depth - 1] !== $relatedCollection->getId()) {
continue;
}
Expand Down
6 changes: 5 additions & 1 deletion src/Database/Trait/Documents.php
Original file line number Diff line number Diff line change
Expand Up @@ -5517,7 +5517,11 @@ private function validateRelationshipSelects(Collection $collection, array $quer
continue;
}

$related = $this->silent(fn () => $this->getCollection($relatedCollectionId));
$related = $this->silent(fn () => $this->findCollection($relatedCollectionId));
if ($related === null) {
continue;
}

$validator = new SelectValidator(
[...$related->attributes(), ...DocumentsValidator::internalAttributes()],
$this->adapter->supports(Capability::DefinedAttributes),
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/Adapter/Scopes/RelationshipTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Tests\E2E\Adapter\Scopes\Relationships\NestedPreparationTests;
use Tests\E2E\Adapter\Scopes\Relationships\OneToManyTests;
use Tests\E2E\Adapter\Scopes\Relationships\OneToOneTests;
use Tests\E2E\Adapter\Scopes\Relationships\OrphanedTests;
use Tests\E2E\Adapter\Support\EventRecorder;
use Throwable;
use Utopia\Database\Adapter\Feature;
Expand Down Expand Up @@ -46,6 +47,7 @@ trait RelationshipTests
use NestedPreparationTests;
use OneToManyTests;
use OneToOneTests;
use OrphanedTests;

/**
* @return array<string, array{RelationshipType, string, int}>
Expand Down
Loading
Loading