From 22fafd03692646cc7f3a0f02e16eb30be3a2dda3 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 21 Nov 2025 16:51:29 +0400 Subject: [PATCH 1/6] test(Case428): Add base test case --- .../Integration/Case428/Entity/Comment.php | 23 +++++ .../Integration/Case428/Entity/Post.php | 24 +++++ .../Common/Integration/Case428/TestCase.php | 93 +++++++++++++++++++ .../Common/Integration/Case428/schema.php | 86 +++++++++++++++++ .../MySQL/Integration/Case428/TestCase.php | 17 ++++ .../Postgres/Integration/Case428/TestCase.php | 17 ++++ .../Integration/Case428/TestCase.php | 17 ++++ .../SQLite/Integration/Case428/TestCase.php | 17 ++++ 8 files changed, 294 insertions(+) create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Post.php create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php create mode 100644 tests/ORM/Functional/Driver/MySQL/Integration/Case428/TestCase.php create mode 100644 tests/ORM/Functional/Driver/Postgres/Integration/Case428/TestCase.php create mode 100644 tests/ORM/Functional/Driver/SQLServer/Integration/Case428/TestCase.php create mode 100644 tests/ORM/Functional/Driver/SQLite/Integration/Case428/TestCase.php diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php new file mode 100644 index 00000000..3c572358 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php @@ -0,0 +1,23 @@ +post = $post; + $this->content = $content; + $this->created_at = new \DateTimeImmutable(); + $this->updated_at = new \DateTimeImmutable(); + } +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Post.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Post.php new file mode 100644 index 00000000..47827d28 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Post.php @@ -0,0 +1,24 @@ +title = $title; + $this->content = $content; + $this->created_at = new \DateTimeImmutable(); + $this->updated_at = new \DateTimeImmutable(); + } +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php new file mode 100644 index 00000000..546055b2 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php @@ -0,0 +1,93 @@ +orm, Entity\Post::class))->fetchOne(); + + // Check result + $this->assertInstanceOf(Entity\Post::class, $post); + $this->assertInstanceOf(Entity\Comment::class, $post->best_comment); + $this->assertSame(2, $post->best_comment->id); + } + + public function testCreate(): void + { + // Get entity + $post = new Entity\Post('New title', 'New content'); + $post->best_comment = new Entity\Comment('New comment content', $post); + + $this->enableProfiling(); + + // Store changes and calc write queries + $this->captureWriteQueries(); + $this->save($post); + + // Check write queries count + $this->assertNumWrites(3); + } + + public function setUp(): void + { + // Init DB + parent::setUp(); + $this->makeTables(); + $this->fillData(); + + $this->loadSchema(__DIR__ . '/schema.php'); + } + + private function makeTables(): void + { + $this->makeTable('post', [ + 'id' => 'primary', + 'title' => 'string', + 'content' => 'string', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'best_comment_id' => 'int,nullable', + ]); + + $this->makeTable('comment', [ + 'id' => 'primary', + 'content' => 'string', + 'post_id' => 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]); + // $this->makeFK('comment', 'post_id', 'post', 'id', 'NO ACTION', 'NO ACTION'); + // $this->makeFK('post', 'best_comment_id', 'comment', 'id', 'SET NULL', 'SET NULL'); + } + + private function fillData(): void + { + $this->getDatabase()->table('post')->insertMultiple( + ['id', 'title', 'content', 'best_comment_id'], + [ + [1, 'Title 1', 'Foo-bar-baz content 1', 2], + ], + ); + $this->getDatabase()->table('comment')->insertMultiple( + ['post_id', 'content'], + [ + [1, 'Foo-bar-baz comment 1'], + [1, 'Foo-bar-baz comment 2'], + [1, 'Foo-bar-baz comment 3'], + ], + ); + } +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php new file mode 100644 index 00000000..8901d785 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php @@ -0,0 +1,86 @@ + [ + Schema::ENTITY => Comment::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'comment', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + 'content' => 'content', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at', + 'post_id' => 'post_id', + ], + Schema::RELATIONS => [ + 'post' => [ + Relation::TYPE => Relation::BELONGS_TO, + Relation::TARGET => 'post', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => false, + Relation::INNER_KEY => ['post_id'], + Relation::OUTER_KEY => ['id'], + ], + ], + ], + Schema::TYPECAST => [ + 'id' => 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'post_id' => 'int', + ], + Schema::SCHEMA => [], + ], + 'post' => [ + Schema::ENTITY => Post::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::MAPPER => Mapper::class, + Schema::TABLE => 'post', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + 'title' => 'title', + 'content' => 'content', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at', + 'best_comment_id' => 'best_comment_id', + ], + Schema::RELATIONS => [ + 'best_comment' => [ + Relation::TYPE => Relation::REFERS_TO, + Relation::TARGET => 'comment', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => true, + Relation::INNER_KEY => ['best_comment_id'], + Relation::OUTER_KEY => ['id'], + ], + ], + ], + Schema::TYPECAST => [ + 'id' => 'int', + 'public' => 'bool', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'best_comment_id' => 'int', + ], + Schema::SCHEMA => [], + ], +]; diff --git a/tests/ORM/Functional/Driver/MySQL/Integration/Case428/TestCase.php b/tests/ORM/Functional/Driver/MySQL/Integration/Case428/TestCase.php new file mode 100644 index 00000000..c9deb7db --- /dev/null +++ b/tests/ORM/Functional/Driver/MySQL/Integration/Case428/TestCase.php @@ -0,0 +1,17 @@ + Date: Fri, 21 Nov 2025 16:52:02 +0400 Subject: [PATCH 2/6] test(Case428): Add more relations --- .../Integration/Case428/Entity/Category.php | 20 ++++++ .../Integration/Case428/Entity/Post.php | 4 ++ .../Integration/Case428/Entity/User.php | 22 ++++++ .../Common/Integration/Case428/TestCase.php | 45 ++++++++++-- .../Common/Integration/Case428/schema.php | 71 +++++++++++++++++++ 5 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Category.php create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/User.php diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Category.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Category.php new file mode 100644 index 00000000..c4529891 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Category.php @@ -0,0 +1,20 @@ +name = $name; + $this->created_at = new \DateTimeImmutable(); + $this->updated_at = new \DateTimeImmutable(); + } +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Post.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Post.php index 47827d28..f27d0351 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Post.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Post.php @@ -13,6 +13,10 @@ class Post 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 function __construct(string $title = '', string $content = '') { diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/User.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/User.php new file mode 100644 index 00000000..f8ceb6d3 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/User.php @@ -0,0 +1,22 @@ +name = $name; + $this->email = $email; + $this->created_at = new \DateTimeImmutable(); + $this->updated_at = new \DateTimeImmutable(); + } +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php index 546055b2..e89b6b00 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php @@ -53,6 +53,21 @@ public function setUp(): void private function makeTables(): void { + $this->makeTable('user', [ + 'id' => 'primary', + 'name' => 'string', + 'email' => 'string', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]); + + $this->makeTable('category', [ + 'id' => 'primary', + 'name' => 'string', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]); + $this->makeTable('post', [ 'id' => 'primary', 'title' => 'string', @@ -60,6 +75,8 @@ private function makeTables(): void 'created_at' => 'datetime', 'updated_at' => 'datetime', 'best_comment_id' => 'int,nullable', + 'user_id' => 'int,nullable', + 'category_id' => 'int,nullable', ]); $this->makeTable('comment', [ @@ -69,18 +86,38 @@ private function makeTables(): void 'created_at' => 'datetime', 'updated_at' => 'datetime', ]); - // $this->makeFK('comment', 'post_id', 'post', 'id', 'NO ACTION', 'NO ACTION'); - // $this->makeFK('post', 'best_comment_id', 'comment', 'id', 'SET NULL', 'SET NULL'); + + $this->makeFK('post', 'user_id', 'user', 'id', 'SET NULL', 'SET NULL'); + $this->makeFK('post', 'category_id', 'category', 'id', 'SET NULL', 'SET NULL'); + $this->makeFK('comment', 'post_id', 'post', 'id', 'NO ACTION', 'NO ACTION'); + $this->makeFK('post', 'best_comment_id', 'comment', 'id', 'SET NULL', 'SET NULL'); } private function fillData(): void { + $this->getDatabase()->table('user')->insertMultiple( + ['id', 'name', 'email'], + [ + [1, 'John Doe', 'john@example.com'], + [2, 'Jane Smith', 'jane@example.com'], + ], + ); + + $this->getDatabase()->table('category')->insertMultiple( + ['id', 'name'], + [ + [1, 'Technology'], + [2, 'Science'], + ], + ); + $this->getDatabase()->table('post')->insertMultiple( - ['id', 'title', 'content', 'best_comment_id'], + ['id', 'title', 'content', 'best_comment_id', 'user_id', 'category_id'], [ - [1, 'Title 1', 'Foo-bar-baz content 1', 2], + [1, 'Title 1', 'Foo-bar-baz content 1', 2, 1, 1], ], ); + $this->getDatabase()->table('comment')->insertMultiple( ['post_id', 'content'], [ diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php index 8901d785..8b18f1c5 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php @@ -6,10 +6,55 @@ use Cycle\ORM\Relation; use Cycle\ORM\SchemaInterface as Schema; use Cycle\ORM\Select\Source; +use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity\Category; use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity\Comment; use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity\Post; +use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity\User; return [ + 'user' => [ + Schema::ENTITY => User::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'user', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + 'name' => 'name', + 'email' => 'email', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at', + ], + Schema::RELATIONS => [], + Schema::TYPECAST => [ + 'id' => 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ], + Schema::SCHEMA => [], + ], + 'category' => [ + Schema::ENTITY => Category::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'category', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + 'name' => 'name', + 'created_at' => 'created_at', + 'updated_at' => 'updated_at', + ], + Schema::RELATIONS => [], + Schema::TYPECAST => [ + 'id' => 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ], + Schema::SCHEMA => [], + ], 'comment' => [ Schema::ENTITY => Comment::class, Schema::SOURCE => Source::class, @@ -60,6 +105,8 @@ 'created_at' => 'created_at', 'updated_at' => 'updated_at', 'best_comment_id' => 'best_comment_id', + 'user_id' => 'user_id', + 'category_id' => 'category_id', ], Schema::RELATIONS => [ 'best_comment' => [ @@ -73,6 +120,28 @@ Relation::OUTER_KEY => ['id'], ], ], + 'user' => [ + Relation::TYPE => Relation::BELONGS_TO, + Relation::TARGET => 'user', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => true, + Relation::INNER_KEY => ['user_id'], + Relation::OUTER_KEY => ['id'], + ], + ], + 'category' => [ + Relation::TYPE => Relation::BELONGS_TO, + Relation::TARGET => 'category', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => true, + Relation::INNER_KEY => ['category_id'], + Relation::OUTER_KEY => ['id'], + ], + ], ], Schema::TYPECAST => [ 'id' => 'int', @@ -80,6 +149,8 @@ 'created_at' => 'datetime', 'updated_at' => 'datetime', 'best_comment_id' => 'int', + 'user_id' => 'int', + 'category_id' => 'int', ], Schema::SCHEMA => [], ], From 152c85beb258dc189d623d3bf7d31bab0f7a400a Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 21 Nov 2025 21:25:10 +0400 Subject: [PATCH 3/6] test(Case428): Add more relations --- src/Relation/RefersTo.php | 4 ++ .../Integration/Case428/Entity/Comment.php | 18 ++--- .../Integration/Case428/Entity/Metadata.php | 12 ++++ .../Integration/Case428/Entity/Post.php | 9 ++- .../Common/Integration/Case428/TestCase.php | 43 ++++++++---- .../Common/Integration/Case428/schema.php | 68 +++++++++++++++++++ 6 files changed, 130 insertions(+), 24 deletions(-) create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Metadata.php diff --git a/src/Relation/RefersTo.php b/src/Relation/RefersTo.php index f575deb8..7d983dc6 100644 --- a/src/Relation/RefersTo.php +++ b/src/Relation/RefersTo.php @@ -34,6 +34,7 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t { $state = $tuple->state; $relName = $this->getName(); + trap($this)->if($this->name === 'ofd_isna_action'); if (SpecialValue::isNotSet($related)) { if (!$state->hasRelation($relName)) { @@ -41,10 +42,13 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t return; } + trap($related)->if($this->name === 'ofd_isna_action'); $related = $state->getRelation($relName); } $node = $tuple->node; + trap($related)->if($this->name === 'ofd_isna_action'); + trap()->stackTrace()->if($this->name === 'ofd_isna_action'); $tuple->state->setRelation($relName, $related); if ($related instanceof ReferenceInterface && $this->resolve($related, false) !== null) { diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php index 3c572358..5b29e884 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php @@ -6,17 +6,19 @@ class Comment { - public ?int $id = null; - public string $content; public \DateTimeImmutable $created_at; public \DateTimeImmutable $updated_at; - public Post $post; - public int $post_id; + public ?int $post_id = null; + public ?int $user_id = null; + public ?Comment $parent = null; + public ?int $parent_id = null; - public function __construct(string $content, Post $post) - { - $this->post = $post; - $this->content = $content; + 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(); } diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Metadata.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Metadata.php new file mode 100644 index 00000000..c52d4bb6 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Metadata.php @@ -0,0 +1,12 @@ +title = $title; $this->content = $content; $this->created_at = new \DateTimeImmutable(); $this->updated_at = new \DateTimeImmutable(); + $this->metadata = new Metadata($metadata); } } diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php index e89b6b00..b21ec027 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php @@ -4,6 +4,7 @@ namespace Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428; +use Cycle\ORM\EntityManager; use Cycle\ORM\Select; use Cycle\ORM\Tests\Functional\Driver\Common\BaseTest; use Cycle\ORM\Tests\Functional\Driver\Common\Integration\IntegrationTestTrait; @@ -27,18 +28,28 @@ public function testSelect(): void public function testCreate(): void { + $this->enableProfiling(); // Get entity + $user = new Entity\User('Test User', 'test@example.com'); $post = new Entity\Post('New title', 'New content'); - $post->best_comment = new Entity\Comment('New comment content', $post); + $post->user = $user; - $this->enableProfiling(); + $em = new EntityManager($this->orm); + $em->persist($post); + $em->run(); + + $post->best_comment = new Entity\Comment(42, 'New comment content', $post, $user); // Store changes and calc write queries - $this->captureWriteQueries(); - $this->save($post); + $em = new EntityManager($this->orm); + $em->persist($post); + $em->run(); // Check write queries count - $this->assertNumWrites(3); + $this->orm->getHeap()->clean(); + + $post = (new Select($this->orm, Entity\Post::class))->where(['id' => $post->id])->fetchOne(); + self::assertSame(42, $post->best_comment_id); } public function setUp(): void @@ -54,7 +65,7 @@ public function setUp(): void private function makeTables(): void { $this->makeTable('user', [ - 'id' => 'primary', + 'id' => 'int', 'name' => 'string', 'email' => 'string', 'created_at' => 'datetime', @@ -62,7 +73,7 @@ private function makeTables(): void ]); $this->makeTable('category', [ - 'id' => 'primary', + 'id' => 'int', 'name' => 'string', 'created_at' => 'datetime', 'updated_at' => 'datetime', @@ -77,19 +88,23 @@ private function makeTables(): void 'best_comment_id' => 'int,nullable', 'user_id' => 'int,nullable', 'category_id' => 'int,nullable', + 'data' => 'string', ]); $this->makeTable('comment', [ - 'id' => 'primary', + 'id' => 'int', 'content' => 'string', 'post_id' => 'int', + 'user_id' => 'int', 'created_at' => 'datetime', 'updated_at' => 'datetime', + 'parent_id' => 'int,nullable', ]); $this->makeFK('post', 'user_id', 'user', 'id', 'SET NULL', 'SET NULL'); $this->makeFK('post', 'category_id', 'category', 'id', 'SET NULL', 'SET NULL'); $this->makeFK('comment', 'post_id', 'post', 'id', 'NO ACTION', 'NO ACTION'); + $this->makeFK('comment', 'user_id', 'user', 'id', 'NO ACTION', 'NO ACTION'); $this->makeFK('post', 'best_comment_id', 'comment', 'id', 'SET NULL', 'SET NULL'); } @@ -112,18 +127,18 @@ private function fillData(): void ); $this->getDatabase()->table('post')->insertMultiple( - ['id', 'title', 'content', 'best_comment_id', 'user_id', 'category_id'], + ['title', 'content', 'best_comment_id', 'user_id', 'category_id', 'data'], [ - [1, 'Title 1', 'Foo-bar-baz content 1', 2, 1, 1], + ['Title 1', 'Foo-bar-baz content 1', 2, 1, 1, 'metadata'], ], ); $this->getDatabase()->table('comment')->insertMultiple( - ['post_id', 'content'], + ['id', 'post_id', 'user_id', 'content'], [ - [1, 'Foo-bar-baz comment 1'], - [1, 'Foo-bar-baz comment 2'], - [1, 'Foo-bar-baz comment 3'], + [1, 1, 1, 'Foo-bar-baz comment 1'], + [2, 1, 2, 'Foo-bar-baz comment 2'], + [3, 1, 1, 'Foo-bar-baz comment 3'], ], ); } diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php index 8b18f1c5..9224e73a 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php @@ -8,6 +8,7 @@ use Cycle\ORM\Select\Source; use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity\Category; use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity\Comment; +use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity\Metadata; use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity\Post; use Cycle\ORM\Tests\Functional\Driver\Common\Integration\Case428\Entity\User; @@ -33,6 +34,9 @@ 'updated_at' => 'datetime', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => 2, + ], ], 'category' => [ Schema::ENTITY => Category::class, @@ -54,6 +58,9 @@ 'updated_at' => 'datetime', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => 2, + ], ], 'comment' => [ Schema::ENTITY => Comment::class, @@ -68,8 +75,20 @@ 'created_at' => 'created_at', 'updated_at' => 'updated_at', 'post_id' => 'post_id', + 'user_id' => 'user_id', ], Schema::RELATIONS => [ + 'parent' => [ + Relation::TYPE => Relation::REFERS_TO, + Relation::TARGET => 'comment', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => true, + Relation::INNER_KEY => ['parent_id'], + Relation::OUTER_KEY => ['id'], + ], + ], 'post' => [ Relation::TYPE => Relation::BELONGS_TO, Relation::TARGET => 'post', @@ -81,14 +100,29 @@ Relation::OUTER_KEY => ['id'], ], ], + 'user' => [ + Relation::TYPE => Relation::BELONGS_TO, + Relation::TARGET => 'user', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => false, + Relation::INNER_KEY => ['user_id'], + Relation::OUTER_KEY => ['id'], + ], + ], ], Schema::TYPECAST => [ 'id' => 'int', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'post_id' => 'int', + 'user_id' => 'int', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => 2, + ], ], 'post' => [ Schema::ENTITY => Post::class, @@ -142,6 +176,13 @@ Relation::OUTER_KEY => ['id'], ], ], + 'metadata' => [ + Relation::TYPE => 1, + Relation::TARGET => 'post:metadata:metadata', + Relation::LOAD => Relation::LOAD_EAGER, + Relation::SCHEMA => [], + ], + ], Schema::TYPECAST => [ 'id' => 'int', @@ -153,5 +194,32 @@ 'category_id' => 'int', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => 2, + ], + ], + 'post:metadata:metadata' => [ + Schema::ENTITY => Metadata::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::MAPPER => Mapper::class, + Schema::TABLE => 'post', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'data' => 'data', + 'id' => 'id', + ], + Schema::RELATIONS => [], + Schema::SCOPE => null, + Schema::TYPECAST => [ + 'data' => 'string', + 'id' => 'int', + ], + Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => 2, + ], ], + ]; From 92469e1949e48ce0c35a1d4f296fc1d14e0725c4 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 22 Nov 2025 01:01:48 +0400 Subject: [PATCH 4/6] refactor: Update tuple status constants and logic for deferred relations --- src/Relation/BelongsTo.php | 2 +- src/Relation/RefersTo.php | 8 +-- src/Transaction/Pool.php | 3 + src/Transaction/Tuple.php | 10 ++- src/Transaction/UnitOfWork.php | 67 +++++++++++-------- .../Classless/ClasslessHasOneCyclicTest.php | 4 ++ .../Common/Integration/Case428/schema.php | 1 - 7 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/Relation/BelongsTo.php b/src/Relation/BelongsTo.php index 0f3e07ad..6be61cc4 100644 --- a/src/Relation/BelongsTo.php +++ b/src/Relation/BelongsTo.php @@ -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) { diff --git a/src/Relation/RefersTo.php b/src/Relation/RefersTo.php index 7d983dc6..a5fe43c2 100644 --- a/src/Relation/RefersTo.php +++ b/src/Relation/RefersTo.php @@ -34,7 +34,6 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t { $state = $tuple->state; $relName = $this->getName(); - trap($this)->if($this->name === 'ofd_isna_action'); if (SpecialValue::isNotSet($related)) { if (!$state->hasRelation($relName)) { @@ -42,13 +41,10 @@ public function prepare(Pool $pool, Tuple $tuple, mixed $related, bool $load = t return; } - trap($related)->if($this->name === 'ofd_isna_action'); $related = $state->getRelation($relName); } $node = $tuple->node; - trap($related)->if($this->name === 'ofd_isna_action'); - trap()->stackTrace()->if($this->name === 'ofd_isna_action'); $tuple->state->setRelation($relName, $related); if ($related instanceof ReferenceInterface && $this->resolve($related, false) !== null) { @@ -59,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; @@ -102,6 +99,7 @@ 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()) { @@ -109,7 +107,7 @@ public function queue(Pool $pool, Tuple $tuple): void $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; diff --git a/src/Transaction/Pool.php b/src/Transaction/Pool.php index 3f93d07f..597e071b 100644 --- a/src/Transaction/Pool.php +++ b/src/Transaction/Pool.php @@ -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); @@ -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.'); } diff --git a/src/Transaction/Tuple.php b/src/Transaction/Tuple.php index a60bad87..3170e643 100644 --- a/src/Transaction/Tuple.php +++ b/src/Transaction/Tuple.php @@ -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; @@ -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, diff --git a/src/Transaction/UnitOfWork.php b/src/Transaction/UnitOfWork.php index 90e3808a..d5aff672 100644 --- a/src/Transaction/UnitOfWork.php +++ b/src/Transaction/UnitOfWork.php @@ -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; } @@ -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, @@ -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)); @@ -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, @@ -302,21 +304,23 @@ 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); @@ -324,10 +328,11 @@ private function resolveSlaveRelations(Tuple $tuple, RelationMap $map): int 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; } @@ -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( @@ -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 @@ -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) { diff --git a/tests/ORM/Functional/Driver/Common/Classless/ClasslessHasOneCyclicTest.php b/tests/ORM/Functional/Driver/Common/Classless/ClasslessHasOneCyclicTest.php index 75c4a922..0a856561 100644 --- a/tests/ORM/Functional/Driver/Common/Classless/ClasslessHasOneCyclicTest.php +++ b/tests/ORM/Functional/Driver/Common/Classless/ClasslessHasOneCyclicTest.php @@ -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); diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php index 9224e73a..df37cf3a 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php @@ -221,5 +221,4 @@ 'id' => 2, ], ], - ]; From 2e354ce7b727a81de1710beefd71141da1e13af4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 21 Nov 2025 21:02:30 +0000 Subject: [PATCH 5/6] style(php-cs-fixer): fix coding standards --- .../Driver/Common/Integration/Case428/Entity/Comment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php index 5b29e884..568acc20 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Comment.php @@ -17,7 +17,7 @@ public function __construct( public int $id, public string $content, public Post $post, - public User $user + public User $user, ) { $this->created_at = new \DateTimeImmutable(); $this->updated_at = new \DateTimeImmutable(); From c95a47e7b347e8dbcf3f587dd8c32cb96918b091 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 22 Nov 2025 01:10:59 +0400 Subject: [PATCH 6/6] chore: cleanup --- phpunit.xml | 4 +--- .../Functional/Driver/Common/Integration/Case428/TestCase.php | 1 - .../Common/Relation/BelongsTo/BelongsToRelationTest.php | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index a98f05fb..1347a4d0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,10 +10,8 @@ convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" - convertDeprecationsToExceptions="true" + convertDeprecationsToExceptions="false" processIsolation="false" - stopOnFailure="true" - stopOnError="true" > diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php index b21ec027..597e7e4e 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/TestCase.php @@ -28,7 +28,6 @@ public function testSelect(): void public function testCreate(): void { - $this->enableProfiling(); // Get entity $user = new Entity\User('Test User', 'test@example.com'); $post = new Entity\Post('New title', 'New content'); diff --git a/tests/ORM/Functional/Driver/Common/Relation/BelongsTo/BelongsToRelationTest.php b/tests/ORM/Functional/Driver/Common/Relation/BelongsTo/BelongsToRelationTest.php index 33b54a93..1b5e7355 100644 --- a/tests/ORM/Functional/Driver/Common/Relation/BelongsTo/BelongsToRelationTest.php +++ b/tests/ORM/Functional/Driver/Common/Relation/BelongsTo/BelongsToRelationTest.php @@ -400,7 +400,6 @@ public function testUnsetProperty(): void */ public function testUnsetPropertyWithoutIgnoreUninitializedRelations(): void { - echo static::class; $this->orm = $this->orm->with(options: (new Options())->withIgnoreUninitializedRelations(false)); /** @var Profile $profile */ $profile = (new Select($this->orm, Profile::class))