From 5f01c9a6261a259b2fb9658337a46395bf940f06 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 5 Jun 2024 17:20:04 +0700 Subject: [PATCH 01/13] Refactor `populateRelation()` --- src/ActiveRelationTrait.php | 212 ++++++++++++++---------------------- 1 file changed, 79 insertions(+), 133 deletions(-) diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index e15d8ddec..fd12a6db9 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -17,6 +17,7 @@ use function array_diff_key; use function array_fill_keys; use function array_filter; +use function array_flip; use function array_intersect_key; use function array_keys; use function array_merge; @@ -248,20 +249,18 @@ public function populateRelation(string $name, array &$primaryModels): array } if (!$this->multiple && count($primaryModels) === 1) { - $model = $this->onePopulate(); + $models = [$this->onePopulate()]; + $this->populateInverseRelation($models, $primaryModels); + $primaryModel = reset($primaryModels); if ($primaryModel instanceof ActiveRecordInterface) { - $primaryModel->populateRelation($name, $model); + $primaryModel->populateRelation($name, $models[0]); } else { - $primaryModels[key($primaryModels)][$name] = $model; - } - - if ($this->inverseOf !== null) { - $this->populateInverseRelation($primaryModels, [$model], $name, $this->inverseOf); + $primaryModels[key($primaryModels)][$name] = $models[0]; } - return [$model]; + return $models; } /** @@ -273,6 +272,8 @@ public function populateRelation(string $name, array &$primaryModels): array $this->indexBy(null); $models = $this->all(); + $this->populateInverseRelation($models, $primaryModels); + if (isset($viaModels, $viaQuery)) { $buckets = $this->buildBuckets($models, $viaModels, $viaQuery); } else { @@ -297,51 +298,7 @@ public function populateRelation(string $name, array &$primaryModels): array $link = $this->link; } - foreach ($primaryModels as $i => $primaryModel) { - $keys = null; - - if ($this->multiple && count($link) === 1) { - $primaryModelKey = reset($link); - - if ($primaryModel instanceof ActiveRecordInterface) { - $keys = $primaryModel->getAttribute($primaryModelKey); - } else { - $keys = $primaryModel[$primaryModelKey] ?? null; - } - } - - if (is_array($keys)) { - $value = []; - - foreach ($keys as $key) { - $key = (string) $key; - - if (isset($buckets[$key])) { - $value[] = $buckets[$key]; - } - } - - if ($indexBy !== null) { - /** if indexBy is set, array_merge will cause renumbering of numeric array */ - $value = array_replace(...$value); - } else { - $value = array_merge(...$value); - } - } else { - $key = $this->getModelKey($primaryModel, $link); - $value = $buckets[$key] ?? ($this->multiple ? [] : null); - } - - if ($primaryModel instanceof ActiveRecordInterface) { - $primaryModel->populateRelation($name, $value); - } else { - $primaryModels[$i][$name] = $value; - } - } - - if ($this->inverseOf !== null) { - $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf); - } + $this->populateRelationFromBuckets($primaryModels, $buckets, $name, $link); return $models; } @@ -350,84 +307,57 @@ public function populateRelation(string $name, array &$primaryModels): array * @throws \Yiisoft\Definitions\Exception\InvalidConfigException */ private function populateInverseRelation( - array &$primaryModels, - array $models, - string $primaryName, - string $name + array &$models, + array $primaryModels, ): void { - if (empty($models) || empty($primaryModels)) { + if ($this->inverseOf === null || empty($models) || empty($primaryModels)) { return; } + $name = $this->inverseOf; $model = reset($models); - if ($model instanceof ActiveRecordInterface) { - $this->populateInverseRelationToModels($models, $primaryModels, $name); - return; - } + /** @var ActiveQuery $relation */ + $relation = is_array($model) + ? $this->getARInstance()->relationQuery($name) + : $model->relationQuery($name); - $primaryModel = reset($primaryModels); + $link = $relation->getLink(); + $buckets = $relation->buildBuckets($primaryModels); - if ($primaryModel instanceof ActiveRecordInterface) { - if ($this->multiple) { - foreach ($primaryModels as $primaryModel) { - $models = $primaryModel->relation($primaryName); - if (!empty($models)) { - $this->populateInverseRelationToModels($models, $primaryModels, $name); - $primaryModel->populateRelation($primaryName, $models); - } - } - } else { - foreach ($primaryModels as $primaryModel) { - $model = $primaryModel->relation($primaryName); - if (!empty($model)) { - $models = [$model]; - $this->populateInverseRelationToModels($models, $primaryModels, $name); - $primaryModel->populateRelation($primaryName, $models[0]); - } - } - } - } else { - if ($this->multiple) { - foreach ($primaryModels as &$primaryModel) { - if (!empty($primaryModel[$primaryName])) { - $this->populateInverseRelationToModels($primaryModel[$primaryName], $primaryModels, $name); - } - } - } else { - foreach ($primaryModels as &$primaryModel) { - if (!empty($primaryModel[$primaryName])) { - $models = [$primaryModel[$primaryName]]; - $this->populateInverseRelationToModels($models, $primaryModels, $name); - $primaryModel[$primaryName] = $models[0]; - } - } - } + if ($relation->getMultiple() && $relation->getIndexBy() !== null) { + $buckets = $this->indexBuckets($buckets, $relation->getIndexBy()); } + + $relation->populateRelationFromBuckets($models, $buckets, $name, $link); } - private function populateInverseRelationToModels(array &$models, array $primaryModels, string $name): void - { - $model = reset($models); - $isArray = is_array($model); + private function populateRelationFromBuckets( + array &$models, + array $buckets, + string $name, + array $link + ): void { + $indexBy = $this->getIndexBy(); + $default = $this->multiple ? [] : null; - /** @var ActiveQuery $relation */ - $relation = $isArray ? $this->getARInstance()->relationQuery($name) : $model->relationQuery($name); - $buckets = $relation->buildBuckets($primaryModels); - $link = $relation->getLink(); - $default = $relation->getMultiple() ? [] : null; + foreach ($models as &$model) { + $keys = $this->getModelKeys($model, $link); - if ($isArray) { - /** @var array $model */ - foreach ($models as &$model) { - $key = $this->getModelKey($model, $link); - $model[$name] = $buckets[$key] ?? $default; - } - } else { - /** @var ActiveRecordInterface $model */ - foreach ($models as $model) { - $key = $this->getModelKey($model, $link); - $model->populateRelation($name, $buckets[$key] ?? $default); + $value = match (count($keys)) { + 0 => $default, + 1 => $buckets[$keys[0]] ?? $default, + default => !$this->multiple + ? $default + : ($indexBy !== null + ? array_replace(...array_intersect_key($buckets, array_flip($keys))) + : array_merge(...array_intersect_key($buckets, array_flip($keys)))), + }; + + if ($model instanceof ActiveRecordInterface) { + $model->populateRelation($name, $value); + } else { + $model[$name] = $value; } } } @@ -445,9 +375,17 @@ private function buildBuckets( $viaVia = null; foreach ($viaModels as $viaModel) { - $key1 = $this->getModelKey($viaModel, $viaLinkKeys); - $key2 = $this->getModelKey($viaModel, $linkValues); - $map[$key2][$key1] = true; + $key1 = $this->getModelKeys($viaModel, $viaLinkKeys); + $key2 = $this->getModelKeys($viaModel, $linkValues); + $flags = array_fill_keys($key1, true); + + foreach ($key2 as $key) { + if (isset($map[$key])) { + $map[$key] += $flags; + } else { + $map[$key] = $flags; + } + } } if ($viaQuery !== null) { @@ -473,18 +411,22 @@ private function buildBuckets( if (isset($map)) { foreach ($models as $model) { - $key = $this->getModelKey($model, $linkKeys); - if (isset($map[$key])) { - foreach (array_keys($map[$key]) as $key2) { - /** @psalm-suppress InvalidArrayOffset */ + $keys = $this->getModelKeys($model, $linkKeys); + $filtered = array_intersect_key($map, array_flip($keys)); + + foreach ($filtered as $keys2) { + foreach (array_keys($keys2) as $key2) { $buckets[$key2][] = $model; } } } } else { foreach ($models as $model) { - $key = $this->getModelKey($model, $linkKeys); - $buckets[$key][] = $model; + $keys = $this->getModelKeys($model, $linkKeys); + + foreach ($keys as $key) { + $buckets[$key][] = $model; + } } } @@ -636,14 +578,16 @@ protected function filterByModels(array $models): void $this->andWhere(['in', $attributes, $values]); } - private function getModelKey(ActiveRecordInterface|array $activeRecord, array $attributes): string + private function getModelKeys(ActiveRecordInterface|array $activeRecord, array $attributes): array { $key = []; if (is_array($activeRecord)) { foreach ($attributes as $attribute) { if (isset($activeRecord[$attribute])) { - $key[] = (string) $activeRecord[$attribute]; + $key[] = is_array($activeRecord[$attribute]) + ? $activeRecord[$attribute] + : (string) $activeRecord[$attribute]; } } } else { @@ -651,15 +595,17 @@ private function getModelKey(ActiveRecordInterface|array $activeRecord, array $a $value = $activeRecord->getAttribute($attribute); if ($value !== null) { - $key[] = (string) $value; + $key[] = is_array($value) + ? $value + : (string) $value; } } } return match (count($key)) { - 0 => '', - 1 => $key[0], - default => serialize($key), + 0 => [], + 1 => is_array($key[0]) ? $key[0] : [$key[0]], + default => [serialize($key)], }; } From 48a060d3d20d4ea4aae1e0cf3144bf57de202355 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Thu, 6 Jun 2024 09:21:11 +0700 Subject: [PATCH 02/13] Fix --- docs/create-model.md | 2 +- src/ActiveRelationTrait.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/create-model.md b/docs/create-model.md index 791bea951..8ddc71df4 100644 --- a/docs/create-model.md +++ b/docs/create-model.md @@ -277,7 +277,7 @@ final class User extends ActiveRecord } ``` -Now you can use `$user->getProfile()` and `$user->getOrders()` to access the relation. +Now you can use `$user->getProfile()` and `$user->getOrders()` to access the relations. ```php use Yiisoft\ActiveRecord\ActiveQuery; diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index fd12a6db9..be8598303 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -323,10 +323,11 @@ private function populateInverseRelation( : $model->relationQuery($name); $link = $relation->getLink(); + $indexBy = $relation->getIndexBy(); $buckets = $relation->buildBuckets($primaryModels); - if ($relation->getMultiple() && $relation->getIndexBy() !== null) { - $buckets = $this->indexBuckets($buckets, $relation->getIndexBy()); + if ($indexBy !== null && $relation->getMultiple()) { + $buckets = $this->indexBuckets($buckets, $indexBy); } $relation->populateRelationFromBuckets($models, $buckets, $name, $link); @@ -344,6 +345,7 @@ private function populateRelationFromBuckets( foreach ($models as &$model) { $keys = $this->getModelKeys($model, $link); + /** @psalm-suppress NamedArgumentNotAllowed */ $value = match (count($keys)) { 0 => $default, 1 => $buckets[$keys[0]] ?? $default, @@ -412,6 +414,7 @@ private function buildBuckets( if (isset($map)) { foreach ($models as $model) { $keys = $this->getModelKeys($model, $linkKeys); + /** @var bool[][] $filtered */ $filtered = array_intersect_key($map, array_flip($keys)); foreach ($filtered as $keys2) { From 48510178e1691cddcf6e1f575425020616f532a5 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 9 Jun 2024 08:47:10 +0700 Subject: [PATCH 03/13] Add tests --- tests/Driver/Pgsql/ActiveRecordTest.php | 32 ++++++++++++++++++++++ tests/Driver/Pgsql/Stubs/Item.php | 27 +++++++++++++++++++ tests/Driver/Pgsql/Stubs/Promotion.php | 35 +++++++++++++++++++++++++ tests/Stubs/ActiveRecord/Item.php | 2 +- tests/data/pgsql.sql | 12 +++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/Driver/Pgsql/Stubs/Item.php create mode 100644 tests/Driver/Pgsql/Stubs/Promotion.php diff --git a/tests/Driver/Pgsql/ActiveRecordTest.php b/tests/Driver/Pgsql/ActiveRecordTest.php index 74707f18a..82540eb4a 100644 --- a/tests/Driver/Pgsql/ActiveRecordTest.php +++ b/tests/Driver/Pgsql/ActiveRecordTest.php @@ -7,6 +7,9 @@ use ArrayAccess; use Traversable; use Yiisoft\ActiveRecord\ActiveQuery; +use Yiisoft\ActiveRecord\ArArrayHelper; +use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Item; +use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Promotion; use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Type; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\ArrayAndJsonTypes; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Beta; @@ -441,4 +444,33 @@ public function testToArrayWithClosure(): void $customer->toArray(), ); } + + public function testRelationViaArray() + { + $this->checkFixture($this->db, 'promotion'); + + $promotionQuery = new ActiveQuery(Promotion::class, $this->db); + /** @var Promotion[] $promotions */ + $promotions = $promotionQuery->with('items')->all(); + + $this->assertSame([1, 2], ArArrayHelper::getColumn($promotions[0]->getItems(), 'id')); + $this->assertSame([3, 4, 5], ArArrayHelper::getColumn($promotions[1]->getItems(), 'id')); + $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItems(), 'id')); + $this->assertCount(0, $promotions[3]->getItems()); + + /** Test inverse relation */ + foreach ($promotions as $promotion) { + foreach ($promotion->getItems() as $item) { + $this->assertTrue($item->isRelationPopulated('promotions')); + } + } + + $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[0]->getItems()[0]->getPromotions(), 'id')); + $this->assertSame([1], ArArrayHelper::getColumn($promotions[0]->getItems()[1]->getPromotions(), 'id')); + $this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[1]->getItems()[0]->getPromotions(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItems()[1]->getPromotions(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItems()[2]->getPromotions(), 'id')); + $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItems()[0]->getPromotions(), 'id')); + $this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[2]->getItems()[1]->getPromotions(), 'id')); + } } diff --git a/tests/Driver/Pgsql/Stubs/Item.php b/tests/Driver/Pgsql/Stubs/Item.php new file mode 100644 index 000000000..fcb19dc49 --- /dev/null +++ b/tests/Driver/Pgsql/Stubs/Item.php @@ -0,0 +1,27 @@ + $this->hasMany(Promotion::class, ['item_ids' => 'id']), + default => parent::relationQuery($name), + }; + } + + /** @return Promotion[] */ + public function getPromotions(): array + { + return $this->relation('promotions'); + } +} diff --git a/tests/Driver/Pgsql/Stubs/Promotion.php b/tests/Driver/Pgsql/Stubs/Promotion.php new file mode 100644 index 000000000..730728bd5 --- /dev/null +++ b/tests/Driver/Pgsql/Stubs/Promotion.php @@ -0,0 +1,35 @@ + $this->hasMany(Item::class, ['id' => 'item_ids'])->inverseOf('promotions'), + default => parent::relationQuery($name), + }; + } + + /** @return Item[] */ + public function getItems(): array + { + return $this->relation('items'); + } +} diff --git a/tests/Stubs/ActiveRecord/Item.php b/tests/Stubs/ActiveRecord/Item.php index 27c4635fb..40d149586 100644 --- a/tests/Stubs/ActiveRecord/Item.php +++ b/tests/Stubs/ActiveRecord/Item.php @@ -11,7 +11,7 @@ /** * Class Item. */ -final class Item extends ActiveRecord +class Item extends ActiveRecord { protected int $id; protected string $name; diff --git a/tests/data/pgsql.sql b/tests/data/pgsql.sql index 2ce4fe6b0..8083b5b16 100644 --- a/tests/data/pgsql.sql +++ b/tests/data/pgsql.sql @@ -28,6 +28,7 @@ DROP TABLE IF EXISTS "employee" CASCADE; DROP TABLE IF EXISTS "department" CASCADE; DROP TABLE IF EXISTS "alpha" CASCADE; DROP TABLE IF EXISTS "beta" CASCADE; +DROP TABLE IF EXISTS "promotion" CASCADE; DROP VIEW IF EXISTS "animal_view" CASCADE; DROP TABLE IF EXISTS "T_constraints_4" CASCADE; DROP TABLE IF EXISTS "T_constraints_3" CASCADE; @@ -224,6 +225,12 @@ CREATE TABLE "beta" ( PRIMARY KEY (id) ); +CREATE TABLE "promotion" ( + id serial primary key, + item_ids integer[] not null, + title varchar(126) not null +); + CREATE VIEW "animal_view" AS SELECT * FROM "animal"; INSERT INTO "animal" (type) VALUES ('Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Cat'); @@ -302,6 +309,11 @@ INSERT INTO "beta" (id, alpha_string_identifier) VALUES (6, '2b'); INSERT INTO "beta" (id, alpha_string_identifier) VALUES (7, '2b'); INSERT INTO "beta" (id, alpha_string_identifier) VALUES (8, '02'); +INSERT INTO "promotion" (item_ids, title) VALUES ('{1,2}', 'Discounted items'); +INSERT INTO "promotion" (item_ids, title) VALUES ('{3,4,5}', 'New arrivals'); +INSERT INTO "promotion" (item_ids, title) VALUES ('{1,3}', 'Free shipping'); +INSERT INTO "promotion" (item_ids, title) VALUES ('{}', 'Free!'); + /** * (Postgres-)Database Schema for validator tests */ From 93405ec043af610f2c6cad14a078ca5cb2bbf81e Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 9 Jun 2024 01:47:32 +0000 Subject: [PATCH 04/13] Apply fixes from StyleCI --- tests/Driver/Pgsql/ActiveRecordTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Driver/Pgsql/ActiveRecordTest.php b/tests/Driver/Pgsql/ActiveRecordTest.php index 82540eb4a..5a30cfb8b 100644 --- a/tests/Driver/Pgsql/ActiveRecordTest.php +++ b/tests/Driver/Pgsql/ActiveRecordTest.php @@ -8,7 +8,6 @@ use Traversable; use Yiisoft\ActiveRecord\ActiveQuery; use Yiisoft\ActiveRecord\ArArrayHelper; -use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Item; use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Promotion; use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Type; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\ArrayAndJsonTypes; From 7bd8f3830d4d3bdc99b4e176107239320e731f45 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 9 Jun 2024 11:16:53 +0700 Subject: [PATCH 05/13] Improve --- src/ActiveRelationTrait.php | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index be8598303..5fa26c548 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -379,15 +379,11 @@ private function buildBuckets( foreach ($viaModels as $viaModel) { $key1 = $this->getModelKeys($viaModel, $viaLinkKeys); $key2 = $this->getModelKeys($viaModel, $linkValues); - $flags = array_fill_keys($key1, true); + $map[] = array_fill_keys($key2, array_fill_keys($key1, true)); + } - foreach ($key2 as $key) { - if (isset($map[$key])) { - $map[$key] += $flags; - } else { - $map[$key] = $flags; - } - } + if (!empty($map)) { + $map = array_replace_recursive(...$map); } if ($viaQuery !== null) { @@ -415,12 +411,10 @@ private function buildBuckets( foreach ($models as $model) { $keys = $this->getModelKeys($model, $linkKeys); /** @var bool[][] $filtered */ - $filtered = array_intersect_key($map, array_flip($keys)); + $filtered = array_intersect_key($map, array_fill_keys($keys, null)); - foreach ($filtered as $keys2) { - foreach (array_keys($keys2) as $key2) { - $buckets[$key2][] = $model; - } + foreach (array_keys(array_replace(...$filtered)) as $key2) { + $buckets[$key2][] = $model; } } } else { @@ -448,11 +442,7 @@ private function mapVia(array $map, array $viaMap): array $resultMap = []; foreach ($map as $key => $linkKeys) { - $resultMap[$key] = []; - foreach (array_keys($linkKeys) as $linkKey) { - /** @psalm-suppress InvalidArrayOffset */ - $resultMap[$key] += $viaMap[$linkKey]; - } + $resultMap[$key] = array_replace(...array_intersect_key($viaMap, $linkKeys)); } return $resultMap; From 28f13f639aebae30e6bf034bb184dd42d2f40bf1 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 16 Jun 2024 09:45:55 +0700 Subject: [PATCH 06/13] Add test --- tests/Driver/Pgsql/ActiveRecordTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Driver/Pgsql/ActiveRecordTest.php b/tests/Driver/Pgsql/ActiveRecordTest.php index 5a30cfb8b..eb4944c3e 100644 --- a/tests/Driver/Pgsql/ActiveRecordTest.php +++ b/tests/Driver/Pgsql/ActiveRecordTest.php @@ -472,4 +472,25 @@ public function testRelationViaArray() $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItems()[0]->getPromotions(), 'id')); $this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[2]->getItems()[1]->getPromotions(), 'id')); } + + public function testLazzyRelationViaArray() + { + $this->checkFixture($this->db, 'item'); + + $itemQuery = new ActiveQuery(Item::class, $this->db); + /** @var Item[] $items */ + $items = $itemQuery->all(); + + $this->assertFalse($items[0]->isRelationPopulated('promotions')); + $this->assertFalse($items[1]->isRelationPopulated('promotions')); + $this->assertFalse($items[2]->isRelationPopulated('promotions')); + $this->assertFalse($items[3]->isRelationPopulated('promotions')); + $this->assertFalse($items[4]->isRelationPopulated('promotions')); + + $this->assertSame([1, 3], ArArrayHelper::getColumn($items[0]->getPromotions(), 'id')); + $this->assertSame([1], ArArrayHelper::getColumn($items[1]->getPromotions(), 'id')); + $this->assertSame([2, 3], ArArrayHelper::getColumn($items[2]->getPromotions(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($items[3]->getPromotions(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($items[4]->getPromotions(), 'id')); + } } From 5db311bf55d8481633a12429d3ca0258f1cd9e59 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 7 Jul 2024 17:36:46 +0700 Subject: [PATCH 07/13] Relations via array --- src/ActiveRecord.php | 9 ++++ src/ActiveRelationTrait.php | 68 ++++++++++++++++--------- tests/ActiveRecordTest.php | 60 ++++++++++++++++++++++ tests/Driver/Pgsql/ActiveRecordTest.php | 52 +++++++++---------- tests/Driver/Pgsql/Stubs/Item.php | 6 +-- tests/Driver/Pgsql/Stubs/Promotion.php | 15 ++---- tests/Stubs/ActiveRecord/Item.php | 7 +++ tests/Stubs/ActiveRecord/Promotion.php | 38 ++++++++++++++ tests/data/mysql.sql | 12 +++++ tests/data/pgsql.sql | 25 ++++----- tests/data/sqlite.sql | 13 +++++ 11 files changed, 229 insertions(+), 76 deletions(-) create mode 100644 tests/Stubs/ActiveRecord/Promotion.php diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 8d3aea4f8..5f74dec13 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -8,6 +8,7 @@ use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Exception\InvalidConfigException; +use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Schema\TableSchemaInterface; use function array_diff; @@ -84,6 +85,14 @@ public function attributes(): array return $this->getTableSchema()->getColumnNames(); } + /** + * Returns the abstract type of the column. + */ + public function columnType(string $columnName): string + { + return $this->getTableSchema()->getColumn($columnName)?->getType() ?? SchemaInterface::TYPE_STRING; + } + public function filterCondition(array $condition, array $aliases = []): array { $result = []; diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index 5fa26c548..34bcb8fe1 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -12,6 +12,10 @@ use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\QueryBuilder\Condition\ArrayOverlapsCondition; +use Yiisoft\Db\QueryBuilder\Condition\InCondition; +use Yiisoft\Db\QueryBuilder\Condition\JsonOverlapsCondition; +use Yiisoft\Db\Schema\SchemaInterface; use function array_column; use function array_combine; use function array_diff_key; @@ -505,11 +509,11 @@ protected function filterByModels(array $models): void if (count($attributes) === 1) { /** single key */ - $attribute = reset($this->link); + $linkedAttribute = reset($this->link); if ($model instanceof ActiveRecordInterface) { foreach ($models as $model) { - $value = $model->getAttribute($attribute); + $value = $model->getAttribute($linkedAttribute); if ($value !== null) { if (is_array($value)) { @@ -521,8 +525,8 @@ protected function filterByModels(array $models): void } } else { foreach ($models as $model) { - if (isset($model[$attribute])) { - $value = $model[$attribute]; + if (isset($model[$linkedAttribute])) { + $value = $model[$linkedAttribute]; if (is_array($value)) { $values = [...$values, ...$value]; @@ -533,31 +537,45 @@ protected function filterByModels(array $models): void } } - if (!empty($values)) { - $scalarValues = array_filter($values, 'is_scalar'); - $nonScalarValues = array_diff_key($values, $scalarValues); - - $scalarValues = array_unique($scalarValues); - $values = [...$scalarValues, ...$nonScalarValues]; + if (empty($values)) { + $this->emulateExecution(); + $this->andWhere('1=0'); + return; } - } else { - $nulls = array_fill_keys($this->link, null); - if ($model instanceof ActiveRecordInterface) { - foreach ($models as $model) { - $value = $model->getAttributes($this->link); + $scalarValues = array_filter($values, 'is_scalar'); + $nonScalarValues = array_diff_key($values, $scalarValues); - if (!empty($value)) { - $values[] = array_combine($attributes, array_merge($nulls, $value)); - } + $scalarValues = array_unique($scalarValues); + $values = [...$scalarValues, ...$nonScalarValues]; + + $attribute = reset($attributes); + + match ($this->getARInstance()->columnType($attribute)) { + 'array' => $this->andWhere(new ArrayOverlapsCondition($attribute, $values)), + SchemaInterface::TYPE_JSON => $this->andWhere(new JsonOverlapsCondition($attribute, $values)), + default => $this->andWhere(new InCondition($attribute, 'IN', $values)), + }; + + return; + } + + $nulls = array_fill_keys($this->link, null); + + if ($model instanceof ActiveRecordInterface) { + foreach ($models as $model) { + $value = $model->getAttributes($this->link); + + if (!empty($value)) { + $values[] = array_combine($attributes, array_merge($nulls, $value)); } - } else { - foreach ($models as $model) { - $value = array_intersect_key($model, $nulls); + } + } else { + foreach ($models as $model) { + $value = array_intersect_key($model, $nulls); - if (!empty($value)) { - $values[] = array_combine($attributes, array_merge($nulls, $value)); - } + if (!empty($value)) { + $values[] = array_combine($attributes, array_merge($nulls, $value)); } } } @@ -568,7 +586,7 @@ protected function filterByModels(array $models): void return; } - $this->andWhere(['in', $attributes, $values]); + $this->andWhere(new InCondition($attributes, 'IN', $values)); } private function getModelKeys(ActiveRecordInterface|array $activeRecord, array $attributes): array diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index ca9e246df..940422801 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -8,6 +8,7 @@ use DivisionByZeroError; use ReflectionException; use Yiisoft\ActiveRecord\ActiveQuery; +use Yiisoft\ActiveRecord\ArArrayHelper; use Yiisoft\ActiveRecord\ConnectionProvider; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Animal; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Cat; @@ -25,6 +26,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderItem; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderItemWithNullFK; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderWithFactory; +use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Promotion; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Type; use Yiisoft\ActiveRecord\Tests\Support\Assert; use Yiisoft\Db\Exception\Exception; @@ -1089,4 +1091,62 @@ public function testWithFactoryNonInitiated(): void $customer = $order->getCustomerWithFactory(); } + + public function testRelationViaJson() + { + if (in_array($this->db()->getDriverName(), ['oci', 'sqlsrv'], true)) { + $this->markTestSkipped('Oracle and MSSQL drivers do not support JSON columns.'); + } + + $this->checkFixture($this->db(), 'promotion'); + + $promotionQuery = new ActiveQuery(Promotion::class); + /** @var Promotion[] $promotions */ + $promotions = $promotionQuery->with('itemsViaJson')->all(); + + $this->assertSame([1, 2], ArArrayHelper::getColumn($promotions[0]->getItemsViaJson(), 'id')); + $this->assertSame([3, 4, 5], ArArrayHelper::getColumn($promotions[1]->getItemsViaJson(), 'id')); + $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItemsViaJson(), 'id')); + $this->assertCount(0, $promotions[3]->getItemsViaJson()); + + /** Test inverse relation */ + foreach ($promotions as $promotion) { + foreach ($promotion->getItemsViaJson() as $item) { + $this->assertTrue($item->isRelationPopulated('promotionsViaJson')); + } + } + + $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[0]->getItemsViaJson()[0]->getPromotionsViaJson(), 'id')); + $this->assertSame([1], ArArrayHelper::getColumn($promotions[0]->getItemsViaJson()[1]->getPromotionsViaJson(), 'id')); + $this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[1]->getItemsViaJson()[0]->getPromotionsViaJson(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItemsViaJson()[1]->getPromotionsViaJson(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItemsViaJson()[2]->getPromotionsViaJson(), 'id')); + $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItemsViaJson()[0]->getPromotionsViaJson(), 'id')); + $this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[2]->getItemsViaJson()[1]->getPromotionsViaJson(), 'id')); + } + + public function testLazzyRelationViaJson() + { + if (in_array($this->db()->getDriverName(), ['oci', 'sqlsrv'], true)) { + $this->markTestSkipped('Oracle and MSSQL drivers do not support JSON columns.'); + } + + $this->checkFixture($this->db(), 'item'); + + $itemQuery = new ActiveQuery(Item::class); + /** @var Item[] $items */ + $items = $itemQuery->all(); + + $this->assertFalse($items[0]->isRelationPopulated('promotionsViaJson')); + $this->assertFalse($items[1]->isRelationPopulated('promotionsViaJson')); + $this->assertFalse($items[2]->isRelationPopulated('promotionsViaJson')); + $this->assertFalse($items[3]->isRelationPopulated('promotionsViaJson')); + $this->assertFalse($items[4]->isRelationPopulated('promotionsViaJson')); + + $this->assertSame([1, 3], ArArrayHelper::getColumn($items[0]->getPromotionsViaJson(), 'id')); + $this->assertSame([1], ArArrayHelper::getColumn($items[1]->getPromotionsViaJson(), 'id')); + $this->assertSame([2, 3], ArArrayHelper::getColumn($items[2]->getPromotionsViaJson(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($items[3]->getPromotionsViaJson(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($items[4]->getPromotionsViaJson(), 'id')); + } } diff --git a/tests/Driver/Pgsql/ActiveRecordTest.php b/tests/Driver/Pgsql/ActiveRecordTest.php index de1a393ba..177e4fcd9 100644 --- a/tests/Driver/Pgsql/ActiveRecordTest.php +++ b/tests/Driver/Pgsql/ActiveRecordTest.php @@ -446,47 +446,47 @@ public function testRelationViaArray() $promotionQuery = new ActiveQuery(Promotion::class); /** @var Promotion[] $promotions */ - $promotions = $promotionQuery->with('items')->all(); + $promotions = $promotionQuery->with('itemsViaArray')->all(); - $this->assertSame([1, 2], ArArrayHelper::getColumn($promotions[0]->getItems(), 'id')); - $this->assertSame([3, 4, 5], ArArrayHelper::getColumn($promotions[1]->getItems(), 'id')); - $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItems(), 'id')); - $this->assertCount(0, $promotions[3]->getItems()); + $this->assertSame([1, 2], ArArrayHelper::getColumn($promotions[0]->getItemsViaArray(), 'id')); + $this->assertSame([3, 4, 5], ArArrayHelper::getColumn($promotions[1]->getItemsViaArray(), 'id')); + $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItemsViaArray(), 'id')); + $this->assertCount(0, $promotions[3]->getItemsViaArray()); /** Test inverse relation */ foreach ($promotions as $promotion) { - foreach ($promotion->getItems() as $item) { - $this->assertTrue($item->isRelationPopulated('promotions')); + foreach ($promotion->getItemsViaArray() as $item) { + $this->assertTrue($item->isRelationPopulated('promotionsViaArray')); } } - $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[0]->getItems()[0]->getPromotions(), 'id')); - $this->assertSame([1], ArArrayHelper::getColumn($promotions[0]->getItems()[1]->getPromotions(), 'id')); - $this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[1]->getItems()[0]->getPromotions(), 'id')); - $this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItems()[1]->getPromotions(), 'id')); - $this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItems()[2]->getPromotions(), 'id')); - $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItems()[0]->getPromotions(), 'id')); - $this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[2]->getItems()[1]->getPromotions(), 'id')); + $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[0]->getItemsViaArray()[0]->getPromotionsViaArray(), 'id')); + $this->assertSame([1], ArArrayHelper::getColumn($promotions[0]->getItemsViaArray()[1]->getPromotionsViaArray(), 'id')); + $this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[1]->getItemsViaArray()[0]->getPromotionsViaArray(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItemsViaArray()[1]->getPromotionsViaArray(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($promotions[1]->getItemsViaArray()[2]->getPromotionsViaArray(), 'id')); + $this->assertSame([1, 3], ArArrayHelper::getColumn($promotions[2]->getItemsViaArray()[0]->getPromotionsViaArray(), 'id')); + $this->assertSame([2, 3], ArArrayHelper::getColumn($promotions[2]->getItemsViaArray()[1]->getPromotionsViaArray(), 'id')); } public function testLazzyRelationViaArray() { - $this->checkFixture($this->db, 'item'); + $this->checkFixture($this->db(), 'item'); $itemQuery = new ActiveQuery(Item::class); /** @var Item[] $items */ $items = $itemQuery->all(); - $this->assertFalse($items[0]->isRelationPopulated('promotions')); - $this->assertFalse($items[1]->isRelationPopulated('promotions')); - $this->assertFalse($items[2]->isRelationPopulated('promotions')); - $this->assertFalse($items[3]->isRelationPopulated('promotions')); - $this->assertFalse($items[4]->isRelationPopulated('promotions')); - - $this->assertSame([1, 3], ArArrayHelper::getColumn($items[0]->getPromotions(), 'id')); - $this->assertSame([1], ArArrayHelper::getColumn($items[1]->getPromotions(), 'id')); - $this->assertSame([2, 3], ArArrayHelper::getColumn($items[2]->getPromotions(), 'id')); - $this->assertSame([2], ArArrayHelper::getColumn($items[3]->getPromotions(), 'id')); - $this->assertSame([2], ArArrayHelper::getColumn($items[4]->getPromotions(), 'id')); + $this->assertFalse($items[0]->isRelationPopulated('promotionsViaArray')); + $this->assertFalse($items[1]->isRelationPopulated('promotionsViaArray')); + $this->assertFalse($items[2]->isRelationPopulated('promotionsViaArray')); + $this->assertFalse($items[3]->isRelationPopulated('promotionsViaArray')); + $this->assertFalse($items[4]->isRelationPopulated('promotionsViaArray')); + + $this->assertSame([1, 3], ArArrayHelper::getColumn($items[0]->getPromotionsViaArray(), 'id')); + $this->assertSame([1], ArArrayHelper::getColumn($items[1]->getPromotionsViaArray(), 'id')); + $this->assertSame([2, 3], ArArrayHelper::getColumn($items[2]->getPromotionsViaArray(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($items[3]->getPromotionsViaArray(), 'id')); + $this->assertSame([2], ArArrayHelper::getColumn($items[4]->getPromotionsViaArray(), 'id')); } } diff --git a/tests/Driver/Pgsql/Stubs/Item.php b/tests/Driver/Pgsql/Stubs/Item.php index fcb19dc49..07be93d07 100644 --- a/tests/Driver/Pgsql/Stubs/Item.php +++ b/tests/Driver/Pgsql/Stubs/Item.php @@ -14,14 +14,14 @@ final class Item extends \Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Item public function relationQuery(string $name): ActiveQueryInterface { return match ($name) { - 'promotions' => $this->hasMany(Promotion::class, ['item_ids' => 'id']), + 'promotionsViaArray' => $this->hasMany(Promotion::class, ['array_item_ids' => 'id']), default => parent::relationQuery($name), }; } /** @return Promotion[] */ - public function getPromotions(): array + public function getPromotionsViaArray(): array { - return $this->relation('promotions'); + return $this->relation('promotionsViaArray'); } } diff --git a/tests/Driver/Pgsql/Stubs/Promotion.php b/tests/Driver/Pgsql/Stubs/Promotion.php index 730728bd5..2b447babb 100644 --- a/tests/Driver/Pgsql/Stubs/Promotion.php +++ b/tests/Driver/Pgsql/Stubs/Promotion.php @@ -5,15 +5,9 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs; use Yiisoft\ActiveRecord\ActiveQueryInterface; -use Yiisoft\ActiveRecord\ActiveRecord; -final class Promotion extends ActiveRecord +final class Promotion extends \Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Promotion { - public int $id; - /** @var int[] $item_ids */ - public array $item_ids; - public string $title; - public function getTableName(): string { return '{{%promotion}}'; @@ -22,14 +16,15 @@ public function getTableName(): string public function relationQuery(string $name): ActiveQueryInterface { return match ($name) { - 'items' => $this->hasMany(Item::class, ['id' => 'item_ids'])->inverseOf('promotions'), + 'itemsViaArray' => $this->hasMany(Item::class, ['id' => 'array_item_ids']) + ->inverseOf('promotionsViaArray'), default => parent::relationQuery($name), }; } /** @return Item[] */ - public function getItems(): array + public function getItemsViaArray(): array { - return $this->relation('items'); + return $this->relation('itemsViaArray'); } } diff --git a/tests/Stubs/ActiveRecord/Item.php b/tests/Stubs/ActiveRecord/Item.php index 40d149586..ee4ed4658 100644 --- a/tests/Stubs/ActiveRecord/Item.php +++ b/tests/Stubs/ActiveRecord/Item.php @@ -26,6 +26,7 @@ public function relationQuery(string $name): ActiveQueryInterface { return match ($name) { 'category' => $this->getCategoryQuery(), + 'promotionsViaJson' => $this->hasMany(Promotion::class, ['json_item_ids' => 'id']), default => parent::relationQuery($name), }; } @@ -54,4 +55,10 @@ public function getCategoryQuery(): ActiveQuery { return $this->hasOne(Category::class, ['id' => 'category_id']); } + + /** @return Promotion[] */ + public function getPromotionsViaJson(): array + { + return $this->relation('promotionsViaJson'); + } } diff --git a/tests/Stubs/ActiveRecord/Promotion.php b/tests/Stubs/ActiveRecord/Promotion.php new file mode 100644 index 000000000..86a5654e5 --- /dev/null +++ b/tests/Stubs/ActiveRecord/Promotion.php @@ -0,0 +1,38 @@ + $this->hasMany(Item::class, ['id' => 'json_item_ids']) + ->inverseOf('promotionsViaJson'), + default => parent::relationQuery($name), + }; + } + + /** @return Item[] */ + public function getItemsViaJson(): array + { + return $this->relation('itemsViaJson'); + } +} diff --git a/tests/data/mysql.sql b/tests/data/mysql.sql index f4a4b3848..308c18199 100644 --- a/tests/data/mysql.sql +++ b/tests/data/mysql.sql @@ -7,6 +7,7 @@ DROP TABLE IF EXISTS `composite_fk` CASCADE; DROP TABLE IF EXISTS `order_item` CASCADE; DROP TABLE IF EXISTS `order_item_with_null_fk` CASCADE; DROP TABLE IF EXISTS `item` CASCADE; +DROP TABLE IF EXISTS `promotion` CASCADE; DROP TABLE IF EXISTS `order` CASCADE; DROP TABLE IF EXISTS `order_with_null_fk` CASCADE; DROP TABLE IF EXISTS `category` CASCADE; @@ -74,6 +75,12 @@ CREATE TABLE `item` ( CONSTRAINT `FK_item_category_id` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `promotion` ( + `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `json_item_ids` json NOT NULL, + `title` varchar(126) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + CREATE TABLE `order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `customer_id` int(11) NOT NULL, @@ -246,6 +253,11 @@ INSERT INTO `item` (name, category_id) VALUES ('Ice Age', 2); INSERT INTO `item` (name, category_id) VALUES ('Toy Story', 2); INSERT INTO `item` (name, category_id) VALUES ('Cars', 2); +INSERT INTO `promotion` (json_item_ids, title) VALUES ('[1,2]', 'Discounted items'); +INSERT INTO `promotion` (json_item_ids, title) VALUES ('[3,4,5]', 'New arrivals'); +INSERT INTO `promotion` (json_item_ids, title) VALUES ('[1,3]', 'Free shipping'); +INSERT INTO `promotion` (json_item_ids, title) VALUES ('[]', 'Free!'); + INSERT INTO `order` (customer_id, created_at, total) VALUES (1, 1325282384, 110.0); INSERT INTO `order` (customer_id, created_at, total) VALUES (2, 1325334482, 33.0); INSERT INTO `order` (customer_id, created_at, total) VALUES (2, 1325502201, 40.0); diff --git a/tests/data/pgsql.sql b/tests/data/pgsql.sql index 8083b5b16..d53a408f3 100644 --- a/tests/data/pgsql.sql +++ b/tests/data/pgsql.sql @@ -8,6 +8,7 @@ DROP TABLE IF EXISTS "composite_fk" CASCADE; DROP TABLE IF EXISTS "order_item" CASCADE; DROP TABLE IF EXISTS "item" CASCADE; DROP SEQUENCE IF EXISTS "item_id_seq_2" CASCADE; +DROP TABLE IF EXISTS "promotion" CASCADE; DROP TABLE IF EXISTS "order_item_with_null_fk" CASCADE; DROP TABLE IF EXISTS "order" CASCADE; DROP TABLE IF EXISTS "order_with_null_fk" CASCADE; @@ -28,7 +29,6 @@ DROP TABLE IF EXISTS "employee" CASCADE; DROP TABLE IF EXISTS "department" CASCADE; DROP TABLE IF EXISTS "alpha" CASCADE; DROP TABLE IF EXISTS "beta" CASCADE; -DROP TABLE IF EXISTS "promotion" CASCADE; DROP VIEW IF EXISTS "animal_view" CASCADE; DROP TABLE IF EXISTS "T_constraints_4" CASCADE; DROP TABLE IF EXISTS "T_constraints_3" CASCADE; @@ -83,6 +83,13 @@ CREATE TABLE "item" ( ); CREATE SEQUENCE "item_id_seq_2"; +CREATE TABLE "promotion" ( + id serial primary key, + array_item_ids integer[] NOT NULL, + json_item_ids jsonb NOT NULL, + title varchar(126) NOT NULL +); + CREATE TABLE "order" ( id serial not null primary key, customer_id integer NOT NULL references "customer"(id) on UPDATE CASCADE on DELETE CASCADE, @@ -225,12 +232,6 @@ CREATE TABLE "beta" ( PRIMARY KEY (id) ); -CREATE TABLE "promotion" ( - id serial primary key, - item_ids integer[] not null, - title varchar(126) not null -); - CREATE VIEW "animal_view" AS SELECT * FROM "animal"; INSERT INTO "animal" (type) VALUES ('Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Cat'); @@ -256,6 +257,11 @@ INSERT INTO "item" (name, category_id) VALUES ('Ice Age', 2); INSERT INTO "item" (name, category_id) VALUES ('Toy Story', 2); INSERT INTO "item" (name, category_id) VALUES ('Cars', 2); +INSERT INTO "promotion" (array_item_ids, json_item_ids, title) VALUES ('{1,2}', '[1,2]', 'Discounted items'); +INSERT INTO "promotion" (array_item_ids, json_item_ids, title) VALUES ('{3,4,5}', '[3,4,5]', 'New arrivals'); +INSERT INTO "promotion" (array_item_ids, json_item_ids, title) VALUES ('{1,3}', '[1,3]', 'Free shipping'); +INSERT INTO "promotion" (array_item_ids, json_item_ids, title) VALUES ('{}', '[]', 'Free!'); + INSERT INTO "order" (customer_id, created_at, total) VALUES (1, 1325282384, 110.0); INSERT INTO "order" (customer_id, created_at, total) VALUES (2, 1325334482, 33.0); INSERT INTO "order" (customer_id, created_at, total) VALUES (2, 1325502201, 40.0); @@ -309,11 +315,6 @@ INSERT INTO "beta" (id, alpha_string_identifier) VALUES (6, '2b'); INSERT INTO "beta" (id, alpha_string_identifier) VALUES (7, '2b'); INSERT INTO "beta" (id, alpha_string_identifier) VALUES (8, '02'); -INSERT INTO "promotion" (item_ids, title) VALUES ('{1,2}', 'Discounted items'); -INSERT INTO "promotion" (item_ids, title) VALUES ('{3,4,5}', 'New arrivals'); -INSERT INTO "promotion" (item_ids, title) VALUES ('{1,3}', 'Free shipping'); -INSERT INTO "promotion" (item_ids, title) VALUES ('{}', 'Free!'); - /** * (Postgres-)Database Schema for validator tests */ diff --git a/tests/data/sqlite.sql b/tests/data/sqlite.sql index e10b97fa9..8678bd494 100644 --- a/tests/data/sqlite.sql +++ b/tests/data/sqlite.sql @@ -7,6 +7,7 @@ DROP TABLE IF EXISTS "composite_fk"; DROP TABLE IF EXISTS "order_item"; DROP TABLE IF EXISTS "order_item_with_null_fk"; DROP TABLE IF EXISTS "item"; +DROP TABLE IF EXISTS "promotion"; DROP TABLE IF EXISTS "order"; DROP TABLE IF EXISTS "order_with_null_fk"; DROP TABLE IF EXISTS "category"; @@ -61,6 +62,13 @@ CREATE TABLE "item" ( PRIMARY KEY (id) ); +CREATE TABLE "promotion" ( + id INTEGER NOT NULL, + json_item_ids JSON NOT NULL, + title varchar(126) NOT NULL, + PRIMARY KEY (id) +); + CREATE TABLE "order" ( id INTEGER NOT NULL, customer_id INTEGER NOT NULL, @@ -210,6 +218,11 @@ INSERT INTO "item" (name, category_id) VALUES ('Ice Age', 2); INSERT INTO "item" (name, category_id) VALUES ('Toy Story', 2); INSERT INTO "item" (name, category_id) VALUES ('Cars', 2); +INSERT INTO "promotion" (json_item_ids, title) VALUES ('[1,2]', 'Discounted items'); +INSERT INTO "promotion" (json_item_ids, title) VALUES ('[3,4,5]', 'New arrivals'); +INSERT INTO "promotion" (json_item_ids, title) VALUES ('[1,3]', 'Free shipping'); +INSERT INTO "promotion" (json_item_ids, title) VALUES ('[]', 'Free!'); + INSERT INTO "order" (customer_id, created_at, total) VALUES (1, 1325282384, 110.0); INSERT INTO "order" (customer_id, created_at, total) VALUES (2, 1325334482, 33.0); INSERT INTO "order" (customer_id, created_at, total) VALUES (2, 1325502201, 40.0); From e9021c7bc65717f29417d6f46de9cae9e1005c94 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 7 Jul 2024 10:37:09 +0000 Subject: [PATCH 08/13] Apply fixes from StyleCI --- src/ActiveRelationTrait.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index 34bcb8fe1..5febf3392 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -16,6 +16,7 @@ use Yiisoft\Db\QueryBuilder\Condition\InCondition; use Yiisoft\Db\QueryBuilder\Condition\JsonOverlapsCondition; use Yiisoft\Db\Schema\SchemaInterface; + use function array_column; use function array_combine; use function array_diff_key; From 0bccd2c27d8d6f6ce2e54e2e507bfda475beeae6 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 7 Jul 2024 17:45:25 +0700 Subject: [PATCH 09/13] Fix psalm issue --- src/ActiveRecordInterface.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ActiveRecordInterface.php b/src/ActiveRecordInterface.php index a54f65ef8..6ae6d0bfc 100644 --- a/src/ActiveRecordInterface.php +++ b/src/ActiveRecordInterface.php @@ -11,6 +11,7 @@ use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Exception\StaleObjectException; +use Yiisoft\Db\Schema\SchemaInterface; interface ActiveRecordInterface { @@ -25,6 +26,12 @@ interface ActiveRecordInterface */ public function attributes(): array; + /** + * Returns the abstract type of the column. See {@see SchemaInterface} constants started with prefix `TYPE_` for + * possible abstract types. + */ + public function columnType(string $columnName): string; + /** * Returns the database connection used by the Active Record instance. */ From 025674ffbac59d14d74a8bc982c0e33c8d7cdfea Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 7 Jul 2024 17:47:10 +0700 Subject: [PATCH 10/13] Remove comment [skip ci] --- src/ActiveRecord.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 5f74dec13..10e7b20fa 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -85,9 +85,6 @@ public function attributes(): array return $this->getTableSchema()->getColumnNames(); } - /** - * Returns the abstract type of the column. - */ public function columnType(string $columnName): string { return $this->getTableSchema()->getColumn($columnName)?->getType() ?? SchemaInterface::TYPE_STRING; From b6eed375e898217c0e0ee01a996507e2bcc39b91 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 12 Jul 2024 19:23:22 +0700 Subject: [PATCH 11/13] Fix possible bag when using join another table --- src/ActiveRelationTrait.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index 5febf3392..275d9d6cd 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -24,6 +24,7 @@ use function array_filter; use function array_flip; use function array_intersect_key; +use function array_key_first; use function array_keys; use function array_merge; use function array_unique; @@ -551,8 +552,9 @@ protected function filterByModels(array $models): void $values = [...$scalarValues, ...$nonScalarValues]; $attribute = reset($attributes); + $columnName = array_key_first($this->link); - match ($this->getARInstance()->columnType($attribute)) { + match ($this->getARInstance()->columnType($columnName)) { 'array' => $this->andWhere(new ArrayOverlapsCondition($attribute, $values)), SchemaInterface::TYPE_JSON => $this->andWhere(new JsonOverlapsCondition($attribute, $values)), default => $this->andWhere(new InCondition($attribute, 'IN', $values)), From 94302e0cc59a2976b48493dfabd6f71ddc99bd42 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Fri, 12 Jul 2024 19:53:41 +0700 Subject: [PATCH 12/13] Fix psalm issue --- src/ActiveRelationTrait.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index 275d9d6cd..0afb755fc 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -552,6 +552,7 @@ protected function filterByModels(array $models): void $values = [...$scalarValues, ...$nonScalarValues]; $attribute = reset($attributes); + /** @var string $columnName */ $columnName = array_key_first($this->link); match ($this->getARInstance()->columnType($columnName)) { From fc72f1c62860bad4311b0237ddffcf1f7913391d Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Fri, 16 Aug 2024 15:55:59 +0700 Subject: [PATCH 13/13] Apply suggestions from code review Co-authored-by: Sergei Predvoditelev --- src/ActiveRelationTrait.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index 0afb755fc..270a5e0ff 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -11,7 +11,6 @@ use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; - use Yiisoft\Db\QueryBuilder\Condition\ArrayOverlapsCondition; use Yiisoft\Db\QueryBuilder\Condition\InCondition; use Yiisoft\Db\QueryBuilder\Condition\JsonOverlapsCondition;