Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions src/Illuminate/Database/Query/Grammars/MySqlGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ public function prepareBindingsForUpdate(array $bindings, array $values)
}

/**
* Compile a delete query that does not use joins.
* Compile a delete statement without joins into SQL.
*
* @param \Illuminate\Database\Query\Builder $query
* @param string $table
Expand All @@ -472,9 +472,33 @@ protected function compileDeleteWithoutJoins(Builder $query, $table, $where)
{
$sql = parent::compileDeleteWithoutJoins($query, $table, $where);

// When using MySQL, delete statements may contain order by statements and limits
// so we will compile both of those here. Once we have finished compiling this
// we will return the completed SQL statement so it will be executed for us.
if (! empty($query->orders)) {
$sql .= ' '.$this->compileOrders($query, $query->orders);
}

if (isset($query->limit)) {
$sql .= ' '.$this->compileLimit($query, $query->limit);
}

return $sql;
}

/**
* Compile a delete statement with joins into SQL.
*
* Adds ORDER BY and LIMIT if present, for platforms that allow them (e.g., PlanetScale).
*
* Standard MySQL does not support ORDER BY or LIMIT with joined deletes and will throw a syntax error.
*
* @param \Illuminate\Database\Query\Builder $query
* @param string $table
* @param string $where
* @return string
*/
protected function compileDeleteWithJoins(Builder $query, $table, $where)
{
$sql = parent::compileDeleteWithJoins($query, $table, $where);

if (! empty($query->orders)) {
$sql .= ' '.$this->compileOrders($query, $query->orders);
}
Expand Down
43 changes: 35 additions & 8 deletions tests/Database/DatabaseMySqlBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@
namespace Illuminate\Tests\Database;

use Illuminate\Database\Connection;
use Illuminate\Database\Schema\Grammars\MySqlGrammar;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Grammars\MySqlGrammar;
use Illuminate\Database\Query\Processors\Processor;
use Illuminate\Database\Schema\Grammars\MySqlGrammar as MySqlGrammarSchema;
use Illuminate\Database\Schema\MySqlBuilder;
use Mockery as m;
use Mockery;
use PHPUnit\Framework\TestCase;

class DatabaseMySqlBuilderTest extends TestCase
{
protected function tearDown(): void
{
m::close();
Mockery::close();
}

public function testCreateDatabase()
public function testCreateDatabase(): void
{
$connection = m::mock(Connection::class);
$grammar = new MySqlGrammar($connection);
$connection = Mockery::mock(Connection::class);
$grammar = new MySqlGrammarSchema($connection);

$connection->shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8mb4');
$connection->shouldReceive('getConfig')->once()->with('collation')->andReturn('utf8mb4_unicode_ci');
Expand All @@ -33,8 +36,8 @@ public function testCreateDatabase()

public function testDropDatabaseIfExists()
{
$connection = m::mock(Connection::class);
$grammar = new MySqlGrammar($connection);
$connection = Mockery::mock(Connection::class);
$grammar = new MySqlGrammarSchema($connection);

$connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar);
$connection->shouldReceive('statement')->once()->with(
Expand All @@ -45,4 +48,28 @@ public function testDropDatabaseIfExists()

$builder->dropDatabaseIfExists('my_database_a');
}

public function testDeleteWithJoinCompilesOrderByAndLimit(): void
{
$connection = Mockery::mock(Connection::class);
$processor = Mockery::mock(Processor::class);
$grammar = new MySqlGrammar($connection);

$connection->shouldReceive('getDatabaseName')->andReturn('database');
$connection->shouldReceive('getTablePrefix')->andReturn('');

$builder = new Builder($connection, $grammar, $processor);

$builder
->from('users')
->join('contacts', 'users.id', '=', 'contacts.id')
->where('email', '=', 'foo')
->orderBy('users.id')
->limit(5);

$sql = $grammar->compileDelete($builder);

$this->assertStringContainsString('order by `users`.`id` asc', $sql);
$this->assertStringContainsString('limit 5', $sql);
}
}
6 changes: 3 additions & 3 deletions tests/Database/DatabaseQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4478,17 +4478,17 @@ public function testDeleteWithJoinMethod()

$builder = $this->getMySqlBuilder();
$builder->getConnection()->shouldReceive('delete')->once()->with('delete `users` from `users` inner join `contacts` on `users`.`id` = `contacts`.`id` where `email` = ?', ['foo'])->andReturn(1);
$result = $builder->from('users')->join('contacts', 'users.id', '=', 'contacts.id')->where('email', '=', 'foo')->orderBy('id')->limit(1)->delete();
$result = $builder->from('users')->join('contacts', 'users.id', '=', 'contacts.id')->where('email', '=', 'foo')->delete();
$this->assertEquals(1, $result);

$builder = $this->getMySqlBuilder();
$builder->getConnection()->shouldReceive('delete')->once()->with('delete `a` from `users` as `a` inner join `users` as `b` on `a`.`id` = `b`.`user_id` where `email` = ?', ['foo'])->andReturn(1);
$result = $builder->from('users AS a')->join('users AS b', 'a.id', '=', 'b.user_id')->where('email', '=', 'foo')->orderBy('id')->limit(1)->delete();
$result = $builder->from('users AS a')->join('users AS b', 'a.id', '=', 'b.user_id')->where('email', '=', 'foo')->delete();
$this->assertEquals(1, $result);

$builder = $this->getMySqlBuilder();
$builder->getConnection()->shouldReceive('delete')->once()->with('delete `users` from `users` inner join `contacts` on `users`.`id` = `contacts`.`id` where `users`.`id` = ?', [1])->andReturn(1);
$result = $builder->from('users')->join('contacts', 'users.id', '=', 'contacts.id')->orderBy('id')->limit(1)->delete(1);
$result = $builder->from('users')->join('contacts', 'users.id', '=', 'contacts.id')->delete(1);
$this->assertEquals(1, $result);

$builder = $this->getSqlServerBuilder();
Expand Down
66 changes: 55 additions & 11 deletions tests/Integration/Database/EloquentDeleteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Tests\Integration\Database\Fixtures\Post;
Expand Down Expand Up @@ -33,27 +34,70 @@ protected function afterRefreshingDatabase()
});
}

public function testDeleteWithLimit()
public function testDeleteUseLimitWithoutJoins(): void
{
if ($this->driver === 'sqlsrv') {
$this->markTestSkipped('The limit keyword is not supported on MSSQL.');
$totalPosts = 10;
$deleteLimit = 1;

for ($i = 0; $i < $totalPosts; $i++) {
Post::query()->create();
}

// Test simple delete with limit (no join)
Post::query()->latest('id')->limit($deleteLimit)->delete();

$this->assertEquals($totalPosts - $deleteLimit, Post::query()->count());
}

public function testDeleteUseLimitWithJoins(): void
{
$ignoredDrivers = ['sqlsrv', 'mysql', 'mariadb'];

if (in_array($this->driver, $ignoredDrivers)) {
$this->markTestSkipped("{$this->driver} does not support LIMIT on DELETE statements with JOIN clauses.");
}

for ($i = 1; $i <= 10; $i++) {
Comment::create([
'post_id' => Post::create()->id,
$totalPosts = 10;
$deleteLimit = 1;
$whereThreshold = 8;

for ($i = 0; $i < $totalPosts; $i++) {
Comment::query()->create([
'post_id' => Post::query()->create()->id,
]);
}

Post::latest('id')->limit(1)->delete();
$this->assertCount(9, Post::all());
// Test delete with join and limit
Post::query()
->join('comments', 'comments.post_id', '=', 'posts.id')
->where('posts.id', '>', $whereThreshold)
->orderBy('posts.id')
->limit($deleteLimit)
->delete();

$this->assertEquals($totalPosts - $deleteLimit, Post::query()->count());
}

public function testDeleteWithLimitAndJoinThrowsExceptionOnMySql(): void
{
if (! in_array($this->driver, ['mysql', 'mariadb'])) {
$this->markTestSkipped('This test only applies to MySQL/MariaDB.');
}

$this->expectException(QueryException::class);

for ($i = 0; $i < 10; $i++) {
Comment::query()->create([
'post_id' => Post::query()->create()->id,
]);
}

Post::join('comments', 'comments.post_id', '=', 'posts.id')
->where('posts.id', '>', 8)
Post::query()
->join('comments', 'comments.post_id', '=', 'posts.id')
->where('posts.id', '>', 5)
->orderBy('posts.id')
->limit(1)
->delete();
$this->assertCount(8, Post::all());
}

public function testForceDeletedEventIsFired()
Expand Down