Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relations via array #375

Merged
merged 20 commits into from
Aug 17, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/ActiveRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -84,6 +85,11 @@ public function attributes(): array
return $this->getTableSchema()->getColumnNames();
}

public function columnType(string $columnName): string
{
return $this->getTableSchema()->getColumn($columnName)?->getType() ?? SchemaInterface::TYPE_STRING;
}

public function filterCondition(array $condition, array $aliases = []): array
{
$result = [];
Expand Down
7 changes: 7 additions & 0 deletions src/ActiveRecordInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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.
*/
Expand Down
69 changes: 44 additions & 25 deletions src/ActiveRelationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
use Yiisoft\Db\Exception\InvalidConfigException;
use Yiisoft\Db\Exception\NotSupportedException;

Tigrov marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down Expand Up @@ -505,11 +510,11 @@

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)) {
Expand All @@ -521,8 +526,8 @@
}
} 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];
Expand All @@ -533,31 +538,45 @@
}
}

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)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add constant TYPE_ARRAY to SchemaInterface in Yii DB?

Copy link
Member Author

@Tigrov Tigrov Jul 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, plan to move TYPE_ARRAY and TYPE_BIT from db-pgsql to db package. After it can be replaced to the constant.

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);

Check warning on line 576 in src/ActiveRelationTrait.php

View check run for this annotation

Codecov / codecov/patch

src/ActiveRelationTrait.php#L575-L576

Added lines #L575 - L576 were not covered by tests

if (!empty($value)) {
$values[] = array_combine($attributes, array_merge($nulls, $value));
}
if (!empty($value)) {
$values[] = array_combine($attributes, array_merge($nulls, $value));

Check warning on line 579 in src/ActiveRelationTrait.php

View check run for this annotation

Codecov / codecov/patch

src/ActiveRelationTrait.php#L578-L579

Added lines #L578 - L579 were not covered by tests
Tigrov marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand All @@ -568,7 +587,7 @@
return;
}

$this->andWhere(['in', $attributes, $values]);
$this->andWhere(new InCondition($attributes, 'IN', $values));
}

private function getModelKeys(ActiveRecordInterface|array $activeRecord, array $attributes): array
Expand Down
60 changes: 60 additions & 0 deletions tests/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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\Profile;
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Type;
use Yiisoft\ActiveRecord\Tests\Support\Assert;
Expand Down Expand Up @@ -1110,4 +1112,62 @@ public function testSerialization(): void
serialize($profile)
);
}

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'));
}
}
50 changes: 36 additions & 14 deletions tests/Driver/Pgsql/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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;
Expand Down Expand Up @@ -445,26 +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');

$itemQuery = new ActiveQuery(Item::class);
/** @var Item[] $items */
$items = $itemQuery->all();

$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'));
}
}
6 changes: 3 additions & 3 deletions tests/Driver/Pgsql/Stubs/Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
15 changes: 5 additions & 10 deletions tests/Driver/Pgsql/Stubs/Promotion.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}}';
Expand All @@ -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');
}
}
7 changes: 7 additions & 0 deletions tests/Stubs/ActiveRecord/Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
}
Expand Down Expand Up @@ -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');
}
}
Loading
Loading