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

Allow passing query Expression as column in Many-to-Many relationship #50849

Merged
merged 1 commit into from Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 21 additions & 17 deletions src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php
Expand Up @@ -65,7 +65,7 @@ class BelongsToMany extends Relation
/**
* The pivot table columns to retrieve.
*
* @var array
* @var array<string|\Illuminate\Contracts\Database\Query\Expression>
*/
protected $pivotColumns = [];

Expand Down Expand Up @@ -356,7 +356,7 @@ public function as($accessor)
/**
* Set a where clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
Expand All @@ -372,7 +372,7 @@ public function wherePivot($column, $operator = null, $value = null, $boolean =
/**
* Set a "where between" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param array $values
* @param string $boolean
* @param bool $not
Expand All @@ -386,7 +386,7 @@ public function wherePivotBetween($column, array $values, $boolean = 'and', $not
/**
* Set a "or where between" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param array $values
* @return $this
*/
Expand All @@ -398,7 +398,7 @@ public function orWherePivotBetween($column, array $values)
/**
* Set a "where pivot not between" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param array $values
* @param string $boolean
* @return $this
Expand All @@ -411,7 +411,7 @@ public function wherePivotNotBetween($column, array $values, $boolean = 'and')
/**
* Set a "or where not between" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param array $values
* @return $this
*/
Expand All @@ -423,7 +423,7 @@ public function orWherePivotNotBetween($column, array $values)
/**
* Set a "where in" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $values
* @param string $boolean
* @param bool $not
Expand All @@ -439,7 +439,7 @@ public function wherePivotIn($column, $values, $boolean = 'and', $not = false)
/**
* Set an "or where" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return $this
Expand All @@ -454,7 +454,7 @@ public function orWherePivot($column, $operator = null, $value = null)
*
* In addition, new pivot records will receive this value.
*
* @param string|array $column
* @param string|\Illuminate\Contracts\Database\Query\Expression|array<string, string> $column
* @param mixed $value
* @return $this
*
Expand Down Expand Up @@ -494,7 +494,7 @@ public function orWherePivotIn($column, $values)
/**
* Set a "where not in" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $values
* @param string $boolean
* @return $this
Expand All @@ -519,7 +519,7 @@ public function orWherePivotNotIn($column, $values)
/**
* Set a "where null" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param string $boolean
* @param bool $not
* @return $this
Expand All @@ -534,7 +534,7 @@ public function wherePivotNull($column, $boolean = 'and', $not = false)
/**
* Set a "where not null" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param string $boolean
* @return $this
*/
Expand All @@ -546,7 +546,7 @@ public function wherePivotNotNull($column, $boolean = 'and')
/**
* Set a "or where null" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param bool $not
* @return $this
*/
Expand All @@ -558,7 +558,7 @@ public function orWherePivotNull($column, $not = false)
/**
* Set a "or where not null" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @return $this
*/
public function orWherePivotNotNull($column)
Expand All @@ -569,7 +569,7 @@ public function orWherePivotNotNull($column)
/**
* Add an "order by" clause for a pivot table column.
*
* @param string $column
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @param string $direction
* @return $this
*/
Expand Down Expand Up @@ -1558,11 +1558,15 @@ public function getPivotColumns()
/**
* Qualify the given column name by the pivot table.
*
* @param string $column
* @return string
* @param string|\Illuminate\Contracts\Database\Query\Expression $column
* @return string|\Illuminate\Contracts\Database\Query\Expression
*/
public function qualifyPivotColumn($column)
{
if ($this->getGrammar()->isExpression($column)) {
return $column;
}

return str_contains($column, '.')
? $column
: $this->table.'.'.$column;
Expand Down
152 changes: 152 additions & 0 deletions tests/Database/DatabaseEloquentBelongsToManyExpressionTest.php
@@ -0,0 +1,152 @@
<?php

namespace Illuminate\Tests\Database;

use Illuminate\Database\Capsule\Manager as DB;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use PHPUnit\Framework\TestCase;

class DatabaseEloquentBelongsToManyExpressionTest extends TestCase
{
protected function setUp(): void
{
$db = new DB;

$db->addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
]);

$db->bootEloquent();
$db->setAsGlobal();

$this->createSchema();
}

public function testAmbiguousColumnsExpression(): void
{
$this->seedData();

$tags = DatabaseEloquentBelongsToManyExpressionTestTestPost::findOrFail(1)
->tags()
->wherePivotNotIn(new Expression("tag_id || '_' || type"), ['1_t1'])
->get();

$this->assertCount(1, $tags);
$this->assertEquals(2, $tags->first()->getKey());
}

public function testQualifiedColumnExpression(): void
{
$this->seedData();

$tags = DatabaseEloquentBelongsToManyExpressionTestTestPost::findOrFail(2)
->tags()
->wherePivotNotIn(new Expression("taggables.tag_id || '_' || taggables.type"), ['2_t2'])
->get();

$this->assertCount(1, $tags);
$this->assertEquals(3, $tags->first()->getKey());
}

/**
* Setup the database schema.
*
* @return void
*/
public function createSchema()
{
$this->schema()->create('posts', fn (Blueprint $t) => $t->id());
$this->schema()->create('tags', fn (Blueprint $t) => $t->id());
$this->schema()->create('taggables', function (Blueprint $t) {
$t->unsignedBigInteger('tag_id');
$t->unsignedBigInteger('taggable_id');
$t->string('type', 10);
$t->string('taggable_type');
}
);
}

/**
* Tear down the database schema.
*
* @return void
*/
protected function tearDown(): void
{
$this->schema()->drop('posts');
$this->schema()->drop('tags');
$this->schema()->drop('taggables');
}

/**
* Helpers...
*/
protected function seedData(): void
{
$p1 = DatabaseEloquentBelongsToManyExpressionTestTestPost::query()->create();
$p2 = DatabaseEloquentBelongsToManyExpressionTestTestPost::query()->create();
$t1 = DatabaseEloquentBelongsToManyExpressionTestTestTag::query()->create();
$t2 = DatabaseEloquentBelongsToManyExpressionTestTestTag::query()->create();
$t3 = DatabaseEloquentBelongsToManyExpressionTestTestTag::query()->create();

$p1->tags()->sync([
$t1->getKey() => ['type' => 't1'],
$t2->getKey() => ['type' => 't2'],
]);
$p2->tags()->sync([
$t2->getKey() => ['type' => 't2'],
$t3->getKey() => ['type' => 't3'],
]);
}

/**
* Get a database connection instance.
*
* @return \Illuminate\Database\ConnectionInterface
*/
protected function connection()
{
return Eloquent::getConnectionResolver()->connection();
}

/**
* Get a schema builder instance.
*
* @return \Illuminate\Database\Schema\Builder
*/
protected function schema()
{
return $this->connection()->getSchemaBuilder();
}
}

class DatabaseEloquentBelongsToManyExpressionTestTestPost extends Eloquent
{
protected $table = 'posts';
protected $fillable = ['id'];
public $timestamps = false;

public function tags(): MorphToMany
{
return $this->morphToMany(
DatabaseEloquentBelongsToManyExpressionTestTestTag::class,
'taggable',
'taggables',
'taggable_id',
'tag_id',
'id',
'id',
);
}
}

class DatabaseEloquentBelongsToManyExpressionTestTestTag extends Eloquent
{
protected $table = 'tags';
protected $fillable = ['id'];
public $timestamps = false;
}
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Query\Grammars\Grammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;

Expand Down Expand Up @@ -61,6 +62,9 @@ protected function getRelation()
$builder->shouldReceive('getModel')->andReturn($related);
$related->shouldReceive('qualifyColumn');
$builder->shouldReceive('join', 'where');
$builder->shouldReceive('getGrammar')->andReturn(
m::mock(Grammar::class, ['isExpression' => false])
);

return new BelongsToMany(
$builder,
Expand Down
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Query\Grammars\Grammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use stdClass;
Expand Down Expand Up @@ -55,6 +56,7 @@ public function getRelationArguments()
$builder->shouldReceive('join')->once()->with('club_user', 'users.id', '=', 'club_user.user_id');
$builder->shouldReceive('where')->once()->with('club_user.club_id', '=', 1);
$builder->shouldReceive('where')->once()->with('club_user.is_admin', '=', 1, 'and');
$builder->shouldReceive('getGrammar')->andReturn(m::mock(Grammar::class, ['isExpression' => false]));

return [$builder, $parent, 'club_user', 'club_id', 'user_id', 'id', 'id', null, false];
}
Expand Down
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Query\Grammars\Grammar;
use Mockery as m;
use PHPUnit\Framework\TestCase;

Expand All @@ -31,6 +32,7 @@ public function testItWillNotTouchRelatedModelsWhenUpdatingChild(): void
$parent->shouldReceive('getAttribute')->with('id')->andReturn(1);
$builder->shouldReceive('getModel')->andReturn($related);
$builder->shouldReceive('where');
$builder->shouldReceive('getGrammar')->andReturn(m::mock(Grammar::class, ['isExpression' => false]));
$relation = new BelongsToMany($builder, $parent, 'article_users', 'user_id', 'article_id', 'id', 'id');
$builder->shouldReceive('update')->never();

Expand Down
2 changes: 2 additions & 0 deletions tests/Database/DatabaseEloquentModelTest.php
Expand Up @@ -2860,6 +2860,7 @@ protected function addMockConnection($model)
$resolver->shouldReceive('connection')->andReturn($connection = m::mock(Connection::class));
$connection->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class));
$grammar->shouldReceive('getBitwiseOperators')->andReturn([]);
$grammar->shouldReceive('isExpression')->andReturnFalse();
$connection->shouldReceive('getPostProcessor')->andReturn($processor = m::mock(Processor::class));
$connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) {
return new BaseBuilder($connection, $grammar, $processor);
Expand Down Expand Up @@ -3214,6 +3215,7 @@ public function getConnection()
$mock = m::mock(Connection::class);
$mock->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class));
$grammar->shouldReceive('getBitwiseOperators')->andReturn([]);
$grammar->shouldReceive('isExpression')->andReturnFalse();
$mock->shouldReceive('getPostProcessor')->andReturn($processor = m::mock(Processor::class));
$mock->shouldReceive('getName')->andReturn('name');
$mock->shouldReceive('query')->andReturnUsing(function () use ($mock, $grammar, $processor) {
Expand Down