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
4 changes: 1 addition & 3 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
convertDeprecationsToExceptions="true"
convertDeprecationsToExceptions="false"
processIsolation="false"
stopOnFailure="true"
stopOnError="true"
>
<coverage>
<include>
Expand Down
2 changes: 1 addition & 1 deletion src/Relation/BelongsTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public function queue(Pool $pool, Tuple $tuple): void

private function shouldPull(Tuple $tuple, Tuple $rTuple): bool
{
$minStatus = Tuple::STATUS_PREPROCESSED;
$minStatus = Tuple::STATUS_DEFERRED_RESOLVED;
if ($this->inversion !== null) {
$relName = $this->getTargetRelationName();
if ($rTuple->state->getRelationStatus($relName) === RelationInterface::STATUS_RESOLVED) {
Expand Down
4 changes: 3 additions & 1 deletion src/Relation/RefersTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t
return;
}
$this->registerWaitingFields($tuple->state, false);

if ($related instanceof ReferenceInterface) {
$tuple->state->setRelationStatus($relName, RelationInterface::STATUS_DEFERRED);
return;
Expand Down Expand Up @@ -98,14 +99,15 @@ public function queue(Pool $pool, Tuple $tuple): void
if ($this->checkNullValue($tuple->node, $tuple->state, $related)) {
return;
}

$rTuple = $pool->offsetGet($related);
if ($rTuple === null) {
if ($this->isCascade()) {
// todo: cascade true?
$rTuple = $pool->attachStore($related, false, null, null, false);
} elseif (
$tuple->state->getRelationStatus($relName) !== RelationInterface::STATUS_DEFERRED
|| $tuple->status !== Tuple::STATUS_PROPOSED
|| $tuple->status !== Tuple::STATUS_PROPOSED_RESOLVED
) {
$tuple->state->setRelationStatus($relName, RelationInterface::STATUS_DEFERRED);
return;
Expand Down
3 changes: 3 additions & 0 deletions src/Transaction/Pool.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ public function openIterator(): \Traversable
$tuple->status = Tuple::STATUS_WAITED;
} elseif ($tuple->status === Tuple::STATUS_DEFERRED) {
$tuple->status = Tuple::STATUS_PROPOSED;
} elseif ($tuple->status === Tuple::STATUS_DEFERRED_RESOLVED) {
$tuple->status = Tuple::STATUS_PROPOSED_RESOLVED;
}
yield $entity => $tuple;
$this->trashIt($entity, $tuple, $this->storage);
Expand All @@ -208,6 +210,7 @@ public function openIterator(): \Traversable
$this->unprocessed = [];
continue;
}

if ($this->happens === 0 && (\count($pool) > 0 || $hasUnresolved)) {
throw new PoolException('Pool has gone into an infinite loop.');
}
Expand Down
10 changes: 7 additions & 3 deletions src/Transaction/Tuple.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ final class Tuple
public const STATUS_WAITED = 2;
public const STATUS_DEFERRED = 3;
public const STATUS_PROPOSED = 4;
public const STATUS_PREPROCESSED = 5;
public const STATUS_PROCESSED = 6;
public const STATUS_UNPROCESSED = 7;
public const STATUS_DEFERRED_RESOLVED = 5;
public const STATUS_PROPOSED_RESOLVED = 6;
public const STATUS_PREPROCESSED = 7;
public const STATUS_PROCESSED = 8;
public const STATUS_UNPROCESSED = 9;

public Node $node;
public State $state;
Expand All @@ -46,6 +48,8 @@ public function __construct(
self::STATUS_WAITED,
self::STATUS_DEFERRED,
self::STATUS_PROPOSED,
self::STATUS_DEFERRED_RESOLVED,
self::STATUS_PROPOSED_RESOLVED,
self::STATUS_PREPROCESSED,
self::STATUS_PROCESSED,
self::STATUS_UNPROCESSED,
Expand Down
67 changes: 39 additions & 28 deletions src/Transaction/UnitOfWork.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ private function resolveMasterRelations(Tuple $tuple, RelationMap $map): int
$deferred = false;
$resolved = true;
foreach ($map->getMasters() as $name => $relation) {
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
if ($relationStatus === RelationInterface::STATUS_RESOLVED) {
$statusAfter = $statusBefore = $tuple->state->getRelationStatus($relation->getName());
if ($statusBefore === RelationInterface::STATUS_RESOLVED) {
continue;
}

Expand All @@ -242,10 +242,10 @@ private function resolveMasterRelations(Tuple $tuple, RelationMap $map): int
// Connected -> $parentNode->getRelationStatus()
// Disconnected -> WAIT if Tuple::STATUS_PREPARING
$relation->queue($this->pool, $tuple);
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
} else {
if ($tuple->status === Tuple::STATUS_PREPARING) {
if ($relationStatus === RelationInterface::STATUS_PREPARE) {
if ($statusAfter === RelationInterface::STATUS_PREPARE) {
$entityData ??= $tuple->mapper->fetchRelations($tuple->entity);
$relation->prepare(
$this->pool,
Expand All @@ -254,15 +254,17 @@ private function resolveMasterRelations(Tuple $tuple, RelationMap $map): int
? $entityData[$name]
: ($this->ignoreUninitializedRelations ? SpecialValue::notSet() : null),
);
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
}
} else {
$relation->queue($this->pool, $tuple);
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
}
}
$resolved = $resolved && $relationStatus >= RelationInterface::STATUS_DEFERRED;
$deferred = $deferred || $relationStatus === RelationInterface::STATUS_DEFERRED;

$statusAfter > $statusBefore and $this->pool->someHappens();
$resolved = $resolved && $statusAfter >= RelationInterface::STATUS_DEFERRED;
$deferred = $deferred || $statusAfter === RelationInterface::STATUS_DEFERRED;
}

// $tuple->waitKeys = array_unique(array_merge(...$waitKeys));
Expand All @@ -284,15 +286,15 @@ private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int
$relData = $tuple->mapper->fetchRelations($tuple->entity);
}
foreach ($map->getSlaves() as $name => $relation) {
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
if (!$relation->isCascade() || $relationStatus === RelationInterface::STATUS_RESOLVED) {
$statusBefore = $statusAfter = $tuple->state->getRelationStatus($relation->getName());
if (!$relation->isCascade() || $statusAfter === RelationInterface::STATUS_RESOLVED) {
continue;
}

$innerKeys = $relation->getInnerKeys();
$isWaitingKeys = \array_intersect($innerKeys, $tuple->state->getWaitingFields(true)) !== [];
$hasChangedKeys = \array_intersect($innerKeys, $changedFields) !== [];
if ($relationStatus === RelationInterface::STATUS_PREPARE) {
if ($statusAfter === RelationInterface::STATUS_PREPARE) {
$relData ??= $tuple->mapper->fetchRelations($tuple->entity);
$relation->prepare(
$this->pool,
Expand All @@ -302,32 +304,35 @@ private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int
: ($this->ignoreUninitializedRelations ? SpecialValue::notSet() : null),
$isWaitingKeys || $hasChangedKeys,
);
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
}

if ($relationStatus !== RelationInterface::STATUS_PREPARE
&& $relationStatus !== RelationInterface::STATUS_RESOLVED
if ($statusAfter !== RelationInterface::STATUS_PREPARE
&& $statusAfter !== RelationInterface::STATUS_RESOLVED
&& !$isWaitingKeys
&& !$hasChangedKeys
&& \count(\array_intersect($innerKeys, \array_keys($transactData))) === \count($innerKeys)
) {
// $child ??= $tuple->state->getRelation($name);
$relation->queue($this->pool, $tuple);
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
$statusAfter = $tuple->state->getRelationStatus($relation->getName());
}
$resolved = $resolved && $relationStatus === RelationInterface::STATUS_RESOLVED;
$deferred = $deferred || $relationStatus === RelationInterface::STATUS_DEFERRED;

$statusAfter > $statusBefore and $this->pool->someHappens();
$resolved = $resolved && $statusAfter === RelationInterface::STATUS_RESOLVED;
$deferred = $deferred || $statusAfter === RelationInterface::STATUS_DEFERRED;
}

return ($deferred ? self::RELATIONS_DEFERRED : 0) | ($resolved ? self::RELATIONS_RESOLVED : 0);
}

private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $hasDeferredRelations): void
{
if (!$map->hasEmbedded() && !$tuple->state->hasChanges()) {
$tuple->status = !$hasDeferredRelations
? Tuple::STATUS_PROCESSED
: \max(Tuple::STATUS_DEFERRED, $tuple->status);
$hasChanges = $tuple->state->hasChanges();
if (!$hasChanges && !$map->hasEmbedded()) {
$tuple->status = $hasDeferredRelations
? \max(Tuple::STATUS_DEFERRED_RESOLVED, $tuple->status)
: Tuple::STATUS_PROCESSED;

return;
}
Expand All @@ -337,19 +342,22 @@ private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $h
// Not embedded but has changes
$this->runCommand($command);

$tuple->status = $tuple->status <= Tuple::STATUS_PROPOSED && $hasDeferredRelations
? Tuple::STATUS_DEFERRED
$tuple->status = $tuple->status <= Tuple::STATUS_PROPOSED_RESOLVED && $hasDeferredRelations
? Tuple::STATUS_DEFERRED_RESOLVED
: Tuple::STATUS_PROCESSED;

return;
}

$entityData = $tuple->mapper->extract($tuple->entity);
$chEmb = false;
foreach ($map->getEmbedded() as $name => $relation) {
$relationStatus = $tuple->state->getRelationStatus($relation->getName());
if ($relationStatus === RelationInterface::STATUS_RESOLVED) {
continue;
}

$chEmb = true;
$tuple->state->setRelation($name, $entityData[$name] ?? null);
// We can use class MergeCommand here
$relation->queue(
Expand All @@ -358,11 +366,13 @@ private function resolveSelfWithEmbedded(Tuple $tuple, RelationMap $map, bool $h
$command instanceof Sequence ? $command->getPrimaryCommand() : $command,
);
}
$this->runCommand($command);

// Run command if there are embedded changes or other changes
$chEmb || $hasChanges and $this->runCommand($command);

$tuple->status = $tuple->status === Tuple::STATUS_PREPROCESSED || !$hasDeferredRelations
? Tuple::STATUS_PROCESSED
: \max(Tuple::STATUS_DEFERRED, $tuple->status);
: \max(Tuple::STATUS_DEFERRED_RESOLVED, $tuple->status);
}

private function resolveRelations(Tuple $tuple): void
Expand All @@ -375,9 +385,10 @@ private function resolveRelations(Tuple $tuple): void
$isDependenciesResolved = (bool) ($result & self::RELATIONS_RESOLVED);
$deferred = (bool) ($result & self::RELATIONS_DEFERRED);

// Self
if ($deferred && $tuple->status < Tuple::STATUS_PROPOSED) {
$tuple->status = Tuple::STATUS_DEFERRED;
// If deferred relations found, mark self as deferred
if ($deferred) {
$tuple->status === Tuple::STATUS_PROPOSED_RESOLVED or $this->pool->someHappens();
$tuple->status = $isDependenciesResolved ? Tuple::STATUS_DEFERRED_RESOLVED : Tuple::STATUS_DEFERRED;
}

if ($isDependenciesResolved) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ public function testCreateCyclic(): void
$this->save($c);
$this->assertNumWrites(2);

$this->captureWriteQueries();
$this->save($c);
$this->assertNumWrites(0);

$selector = new Select($this->orm->withHeap(new Heap()), 'cyclic');
$c = $selector->load('cyclic')->wherePK($c->id)->fetchOne();
$this->assertEquals('new', $c->name);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity;

class Category
{
public ?int $id = null;
public string $name;
public \DateTimeImmutable $created_at;
public \DateTimeImmutable $updated_at;

public function __construct(string $name)
{
$this->name = $name;
$this->created_at = new \DateTimeImmutable();
$this->updated_at = new \DateTimeImmutable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity;

class Comment
{
public \DateTimeImmutable $created_at;
public \DateTimeImmutable $updated_at;
public ?int $post_id = null;
public ?int $user_id = null;
public ?Comment $parent = null;
public ?int $parent_id = null;

public function __construct(
public int $id,
public string $content,
public Post $post,
public User $user,
) {
$this->created_at = new \DateTimeImmutable();
$this->updated_at = new \DateTimeImmutable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity;

class Metadata
{
public function __construct(
public string $data = '',
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity;

class Post
{
public ?int $id = null;
public string $title = '';
public string $content = '';
public \DateTimeImmutable $created_at;
public \DateTimeImmutable $updated_at;
public ?Comment $best_comment = null;
public ?int $best_comment_id = null;
public ?User $user = null;
public ?int $user_id = null;
public ?Category $category = null;
public ?int $category_id = null;
public Metadata $metadata;

public function __construct(
string $title = '',
string $content = '',
string $metadata = '',
) {
$this->title = $title;
$this->content = $content;
$this->created_at = new \DateTimeImmutable();
$this->updated_at = new \DateTimeImmutable();
$this->metadata = new Metadata($metadata);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity;

class User
{
public ?int $id = null;
public string $name;
public string $email;
public \DateTimeImmutable $created_at;
public \DateTimeImmutable $updated_at;

public function __construct(string $name, string $email)
{
$this->name = $name;
$this->email = $email;
$this->created_at = new \DateTimeImmutable();
$this->updated_at = new \DateTimeImmutable();
}
}
Loading
Loading