From 8527d6717b0f08db8daf03cc3244d6409eae67d8 Mon Sep 17 00:00:00 2001 From: hiscaler Date: Sat, 17 May 2025 00:25:46 +0800 Subject: [PATCH 1/4] #523 --- .../Common/Integration/Case428/CaseTest.php | 131 ++++++++++++++ .../Integration/Case428/Entity/Order.php | 20 +++ .../Integration/Case428/Entity/OrderItem.php | 24 +++ .../Case428/Entity/PurchaseOrder.php | 20 +++ .../Case428/Entity/PurchaseOrderItem.php | 21 +++ .../Common/Integration/Case428/schema.php | 161 ++++++++++++++++++ .../MySQL/Integration/Case428/CaseTest.php | 17 ++ .../Postgres/Integration/Case428/CaseTest.php | 17 ++ .../Integration/Case428/CaseTest.php | 17 ++ .../SQLite/Integration/Case428/CaseTest.php | 17 ++ 10 files changed, 445 insertions(+) create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Order.php create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/OrderItem.php create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrder.php create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrderItem.php create mode 100644 tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php create mode 100644 tests/ORM/Functional/Driver/MySQL/Integration/Case428/CaseTest.php create mode 100644 tests/ORM/Functional/Driver/Postgres/Integration/Case428/CaseTest.php create mode 100644 tests/ORM/Functional/Driver/SQLServer/Integration/Case428/CaseTest.php create mode 100644 tests/ORM/Functional/Driver/SQLite/Integration/Case428/CaseTest.php diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php new file mode 100644 index 00000000..ec52759a --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php @@ -0,0 +1,131 @@ +orm, Entity\Order::class)) + ->load('items') + ->wherePK(1) + ->fetchOne(); + + // Check result + $this->assertInstanceOf(Entity\Order::class, $order); + $this->assertCount(2, $order->items); + + // Order 2 + $order = (new Select($this->orm, Entity\Order::class)) + ->load('items') + ->wherePK(2) + ->fetchOne(); + + // Check result + $this->assertInstanceOf(Entity\Order::class, $order); + $this->assertCount(1, $order->items); + } + + public function testSave(): void + { + /** @var Order $order */ + $order = (new Select($this->orm, Entity\Order::class)) + ->wherePK(1) + ->load('items') + ->fetchOne(); + + $purchaseOrder = new PurchaseOrder('PO001'); + $orderItems = []; + foreach ($order->items as $orderItem) { + $purchaseOrderItem = new PurchaseOrderItem($orderItem->quantity); + $purchaseOrderItem->orderItem = $orderItem; + $purchaseOrder->items[] = $purchaseOrderItem; + $orderItem->purchaseOrder = $purchaseOrder; + $orderItem->status = 1; + $orderItems[] = $orderItem; + } + + $this->save($purchaseOrder, ...$orderItems); + } + + public function setUp(): void + { + // Init DB + parent::setUp(); + $this->makeTables(); + $this->fillData(); + + $this->loadSchema(__DIR__ . '/schema.php'); + } + + private function makeTables(): void + { + // Make tables + $this->makeTable('order', [ + 'id' => 'primary', // autoincrement + 'number' => 'string', + ]); + + $this->makeTable('order_item', [ + 'id' => 'primary', + 'order_id' => 'int', + 'sku' => 'string', + 'quantity' => 'int', + 'purchase_order_id' => 'int,nullable', + 'status' => 'int', + ]); + + $this->makeTable('purchase_order', [ + 'id' => 'primary', + 'number' => 'string', + ]); + + $this->makeTable('purchase_order_item', [ + 'id' => 'primary', + 'purchase_order_id' => 'int', + 'order_item_id' => 'int,nullable', + 'quantity' => 'int', + ]); + + $this->makeFK('order_item', 'order_id', 'order', 'id', 'NO ACTION', 'CASCADE'); + + $this->makeFK('purchase_order_item', 'purchase_order_id', 'purchase_order', 'id', 'NO ACTION', 'CASCADE'); + + $this->makeFK('purchase_order_item', 'order_item_id', 'order_item', 'id', 'NO ACTION', 'CASCADE'); + } + + private function fillData(): void + { + $this->getDatabase()->table('order')->insertMultiple( + ['number'], + [ + ['O001'], + ['O002'], + ['O003'], + ], + ); + $this->getDatabase()->table('order_item')->insertMultiple( + ['order_id', 'sku', 'quantity', 'purchase_order_id', 'status'], + [ + [1, 'A', 1, null, 0], + [1, 'B', 1, null, 0], + [2, 'A', 2, null, 0], + [3, 'B', 2, null, 0], + ], + ); + } +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Order.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Order.php new file mode 100644 index 00000000..86b22f2d --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Order.php @@ -0,0 +1,20 @@ + */ + public iterable $items = []; + + public function __construct(string $number) + { + $this->number = $number; + } + +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/OrderItem.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/OrderItem.php new file mode 100644 index 00000000..0184216d --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/OrderItem.php @@ -0,0 +1,24 @@ +sku = $sku; + $this->quantity = $quantity; + } + +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrder.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrder.php new file mode 100644 index 00000000..36140903 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrder.php @@ -0,0 +1,20 @@ + */ + public iterable $items = []; + + public function __construct(string $number) + { + $this->number = $number; + } + +} diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrderItem.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrderItem.php new file mode 100644 index 00000000..f96a2e78 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrderItem.php @@ -0,0 +1,21 @@ +quantity = $quantity; + } + +} 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..905a3149 --- /dev/null +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php @@ -0,0 +1,161 @@ + [ + Schema::ENTITY => Order::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'order', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + 'number' => 'number', + ], + Schema::RELATIONS => [ + 'items' => [ + Relation::TYPE => Relation::HAS_MANY, + Relation::TARGET => 'order_item', + Relation::COLLECTION_TYPE => 'array', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => false, + Relation::WHERE => [], + Relation::ORDER_BY => [], + Relation::INNER_KEY => ['id'], + Relation::OUTER_KEY => 'order_id', + ], + ], + ], + Schema::TYPECAST => [ + 'id' => 'int', + 'number' => 'string', + ], + Schema::SCHEMA => [], + ], + 'order_item' => [ + Schema::ENTITY => OrderItem::class, + Schema::SOURCE => Source::class, + Schema::DATABASE => 'default', + Schema::MAPPER => Mapper::class, + Schema::TABLE => 'order_item', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + 'order_id' => 'order_id', + 'sku' => 'sku', + 'quantity' => 'quantity', + 'purchase_order_id' => 'purchase_order_id', + 'status' => 'status', + ], + Schema::RELATIONS => [ + 'purchaseOrder' => [ + Relation::TYPE => Relation::BELONGS_TO, + Relation::TARGET => 'purchase_order', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => true, + Relation::INNER_KEY => 'purchase_order_id', + Relation::OUTER_KEY => ['id'], + ], + ], + ], + Schema::TYPECAST => [ + 'id' => 'int', + 'order_id' => 'int', + 'sku' => 'string', + 'quantity' => 'int', + 'purchase_order_id' => 'int', + 'status' => 'int', + ], + Schema::SCHEMA => [], + ], + 'purchase_order' => [ + Schema::ENTITY => PurchaseOrder::class, + Schema::MAPPER => Mapper::class, + Schema::SOURCE => Source::class, + Schema::REPOSITORY => Repository::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'purchase_order', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + 'number' => 'number', + ], + Schema::RELATIONS => [ + 'items' => [ + Relation::TYPE => Relation::HAS_MANY, + Relation::TARGET => 'purchase_order_item', + Relation::COLLECTION_TYPE => 'array', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => false, + Relation::WHERE => [], + Relation::ORDER_BY => [], + Relation::INNER_KEY => ['id'], + Relation::OUTER_KEY => 'purchase_order_id', + ], + ], + ], + Schema::SCOPE => null, + Schema::TYPECAST => [ + 'id' => 'int', + 'number' => 'string', + ], + Schema::SCHEMA => [], + ], + 'purchase_order_item' => [ + Schema::ENTITY => PurchaseOrderItem::class, + Schema::MAPPER => Mapper::class, + Schema::SOURCE => Source::class, + Schema::REPOSITORY => Repository::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'purchase_order_item', + Schema::PRIMARY_KEY => ['id'], + Schema::FIND_BY_KEYS => ['id'], + Schema::COLUMNS => [ + 'id' => 'id', + 'purchase_order_id' => 'purchase_order_id', + 'order_item_id' => 'order_item_id', + 'quantity' => 'quantity', + ], + Schema::RELATIONS => [ + 'orderItem' => [ + Relation::TYPE => Relation::BELONGS_TO, + Relation::TARGET => 'order_item', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => true, + Relation::INNER_KEY => 'order_item_id', + Relation::OUTER_KEY => ['id'], + ], + ], + ], + Schema::SCOPE => null, + Schema::TYPECAST => [ + 'id' => 'primary', + 'purchase_order_id' => 'int', + 'order_item_id' => 'int', + 'quantity' => 'int', + ], + Schema::SCHEMA => [], + ], +]; diff --git a/tests/ORM/Functional/Driver/MySQL/Integration/Case428/CaseTest.php b/tests/ORM/Functional/Driver/MySQL/Integration/Case428/CaseTest.php new file mode 100644 index 00000000..854c70ec --- /dev/null +++ b/tests/ORM/Functional/Driver/MySQL/Integration/Case428/CaseTest.php @@ -0,0 +1,17 @@ + Date: Sat, 17 May 2025 00:52:27 +0800 Subject: [PATCH 2/4] ... --- .../Driver/Common/Integration/Case428/CaseTest.php | 2 ++ .../Driver/Common/Integration/Case428/schema.php | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php index ec52759a..ca0bf2fd 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php @@ -103,6 +103,8 @@ private function makeTables(): void $this->makeFK('order_item', 'order_id', 'order', 'id', 'NO ACTION', 'CASCADE'); + $this->makeFK('order_item', 'purchase_order_id', 'purchase_order', 'id', 'NO ACTION', 'CASCADE'); + $this->makeFK('purchase_order_item', 'purchase_order_id', 'purchase_order', 'id', 'NO ACTION', 'CASCADE'); $this->makeFK('purchase_order_item', 'order_item_id', 'order_item', 'id', 'NO ACTION', 'CASCADE'); diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php index 905a3149..910bb35c 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php @@ -15,6 +15,7 @@ return [ 'order' => [ Schema::ENTITY => Order::class, + Schema::MAPPER => Mapper::class, Schema::SOURCE => Source::class, Schema::DATABASE => 'default', Schema::TABLE => 'order', @@ -63,6 +64,17 @@ 'status' => 'status', ], Schema::RELATIONS => [ + 'order' => [ + Relation::TYPE => Relation::BELONGS_TO, + Relation::TARGET => 'order', + Relation::LOAD => Relation::LOAD_PROMISE, + Relation::SCHEMA => [ + Relation::CASCADE => true, + Relation::NULLABLE => false, + Relation::INNER_KEY => 'order_id', + Relation::OUTER_KEY => ['id'], + ], + ], 'purchaseOrder' => [ Relation::TYPE => Relation::BELONGS_TO, Relation::TARGET => 'purchase_order', From 14f8a998b06226557172d8bc62d7cde0f0899092 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 20 May 2025 18:13:22 +0400 Subject: [PATCH 3/4] Try to reproduce bug --- .../Common/Integration/Case428/CaseTest.php | 17 +++++++++++++++++ .../Common/Integration/Case428/Entity/Order.php | 1 - .../Integration/Case428/Entity/OrderItem.php | 10 ++++++---- .../Case428/Entity/PurchaseOrder.php | 3 +-- .../Case428/Entity/PurchaseOrderItem.php | 7 ++++--- .../Common/Integration/Case428/schema.php | 13 +++++++++++++ 6 files changed, 41 insertions(+), 10 deletions(-) diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php index ca0bf2fd..f5688ef1 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/CaseTest.php @@ -62,6 +62,23 @@ public function testSave(): void $this->save($purchaseOrder, ...$orderItems); } + public function testCreate(): void + { + $order = new Entity\Order('O0101'); + $purchaseOrder = new PurchaseOrder('PO101'); + + $orderItems = []; + for ($i = 0; $i < 20; $i++) { + $orderItem = new Entity\OrderItem('A' . $i, 1); + $orderItem->order = $order; + // $order->items[] = $orderItem; + $orderItem->purchaseOrder = $purchaseOrder; + $orderItems[] = $orderItem; + } + + $this->save($order, $purchaseOrder, ...$orderItems); + } + public function setUp(): void { // Init DB diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Order.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Order.php index 86b22f2d..9e841899 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Order.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/Order.php @@ -16,5 +16,4 @@ public function __construct(string $number) { $this->number = $number; } - } diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/OrderItem.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/OrderItem.php index 0184216d..47944014 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/OrderItem.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/OrderItem.php @@ -6,19 +6,21 @@ class OrderItem { - public ?int $id = null; - public int $order_id; + public string $sku; public int $quantity = 1; + public int $status = 0; + public ?int $purchase_order_id = null; public ?PurchaseOrder $purchaseOrder = null; - public int $status = 0; + + public int $order_id; + public Order $order; public function __construct(string $sku, int $quantity = 1) { $this->sku = $sku; $this->quantity = $quantity; } - } diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrder.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrder.php index 36140903..05b9e672 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrder.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrder.php @@ -6,9 +6,9 @@ class PurchaseOrder { - public ?int $id = null; public string $number; + /** @var iterable */ public iterable $items = []; @@ -16,5 +16,4 @@ public function __construct(string $number) { $this->number = $number; } - } diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrderItem.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrderItem.php index f96a2e78..ad8c2fb0 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrderItem.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/Entity/PurchaseOrderItem.php @@ -6,16 +6,17 @@ class PurchaseOrderItem { - public ?int $id = null; + public int $quantity = 1; + public int $purchase_order_id; + public PurchaseOrder $purchaseOrder; + public ?int $order_item_id = null; public ?OrderItem $orderItem = null; - public int $quantity = 1; public function __construct(int $quantity = 1) { $this->quantity = $quantity; } - } diff --git a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php index 910bb35c..11cc618b 100644 --- a/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php +++ b/tests/ORM/Functional/Driver/Common/Integration/Case428/schema.php @@ -4,6 +4,7 @@ use Cycle\ORM\Mapper\Mapper; use Cycle\ORM\Relation; +use Cycle\ORM\Schema\GeneratedField; use Cycle\ORM\SchemaInterface as Schema; use Cycle\ORM\Select\Repository; use Cycle\ORM\Select\Source; @@ -46,6 +47,9 @@ 'number' => 'string', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => GeneratedField::ON_INSERT, // autoincrement + ], ], 'order_item' => [ Schema::ENTITY => OrderItem::class, @@ -96,6 +100,9 @@ 'status' => 'int', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => GeneratedField::ON_INSERT, // autoincrement + ], ], 'purchase_order' => [ Schema::ENTITY => PurchaseOrder::class, @@ -132,6 +139,9 @@ 'number' => 'string', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => GeneratedField::ON_INSERT, // autoincrement + ], ], 'purchase_order_item' => [ Schema::ENTITY => PurchaseOrderItem::class, @@ -169,5 +179,8 @@ 'quantity' => 'int', ], Schema::SCHEMA => [], + Schema::GENERATED_FIELDS => [ + 'id' => GeneratedField::ON_INSERT, // autoincrement + ], ], ]; From d461288eb53b284eb732f559988bcc32a3915e1b Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 20 May 2025 18:29:55 +0400 Subject: [PATCH 4/4] Fix possible bug in BelongsTo --- src/Relation/BelongsTo.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Relation/BelongsTo.php b/src/Relation/BelongsTo.php index 0f3e07ad..5342c502 100644 --- a/src/Relation/BelongsTo.php +++ b/src/Relation/BelongsTo.php @@ -140,6 +140,10 @@ private function shouldPull(Tuple $tuple, Tuple $rTuple): bool $noChanges = true; $toReference = []; foreach ($this->outerKeys as $i => $outerKey) { + if (!\array_key_exists($outerKey, $newData)) { + continue; + } + $innerKey = $this->innerKeys[$i]; if (!\array_key_exists($innerKey, $oldData) || $oldData[$innerKey] !== $newData[$outerKey]) { return true;