Skip to content
Closed
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
38 changes: 38 additions & 0 deletions src/Illuminate/Database/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3574,6 +3574,44 @@ public function upsert(array $values, $uniqueBy, $update = null)
);
}

/**
* Insert new records or update the existing ones using a subquery.
*
* @param array $columns
* @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query
* @param array|string $uniqueBy
* @param array|null $update
* @return int
*/
public function upsertUsing(array $columns, $query, $uniqueBy, $update = null)
{
if (empty($columns)) {
return 0;
}

if ($update === []) {
return (int) $this->insertUsing($columns, $query);
}

$update ??= $columns;

$this->applyBeforeQueryCallbacks();

[$sql, $bindings] = $this->createSub($query);

$bindings = $this->cleanBindings(array_merge(
$bindings,
collect($update)->reject(function ($value, $key) {
return is_int($key);
})->all()
));

return $this->connection->affectingStatement(
$this->grammar->compileUpsertUsing($this, $columns, $sql, (array) $uniqueBy, $update),
$bindings
);
}

/**
* Increment a column's value by a given amount.
*
Expand Down
17 changes: 17 additions & 0 deletions src/Illuminate/Database/Query/Grammars/Grammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,23 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar
throw new RuntimeException('This database engine does not support upserts.');
}

/**
* Compile an "upsert" statement using a subquery into SQL.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $columns
* @param string $sql
* @param array $uniqueBy
* @param array $update
* @return string
*
* @throws \RuntimeException
*/
public function compileUpsertUsing(Builder $query, array $columns, string $sql, array $uniqueBy, array $update)
{
throw new RuntimeException('This database engine does not support upserts.');
}

/**
* Prepare the bindings for an update statement.
*
Expand Down
35 changes: 35 additions & 0 deletions src/Illuminate/Database/Query/Grammars/MySqlGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,41 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar
return $sql.$columns;
}

/**
* Compile an "upsert" statement using a subquery into SQL.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $columns
* @param string $sql
* @param array $uniqueBy
* @param array $update
* @return string
*/
public function compileUpsertUsing(Builder $query, array $columns, string $sql, array $uniqueBy, array $update)
{
$useUpsertAlias = $query->connection->getConfig('use_upsert_alias');

$sql = $this->compileInsertUsing($query, $columns, $sql);

if ($useUpsertAlias) {
$sql .= ' as laravel_upsert_alias';
}

$sql .= ' on duplicate key update ';

$columns = collect($update)->map(function ($value, $key) use ($useUpsertAlias) {
if (! is_numeric($key)) {
return $this->wrap($key).' = '.$this->parameter($value);
}

return $useUpsertAlias
? $this->wrap($value).' = '.$this->wrap('laravel_upsert_alias').'.'.$this->wrap($value)
: $this->wrap($value).' = values('.$this->wrap($value).')';
})->implode(', ');

return $sql.$columns;
}

/**
* Compile a "lateral join" clause.
*
Expand Down
25 changes: 25 additions & 0 deletions src/Illuminate/Database/Query/Grammars/PostgresGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,31 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar
return $sql.$columns;
}

/**
* Compile an "upsert" statement using a subquery into SQL.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $columns
* @param string $sql
* @param array $uniqueBy
* @param array $update
* @return string
*/
public function compileUpsertUsing(Builder $query, array $columns, string $sql, array $uniqueBy, array $update)
{
$sql = $this->compileInsertUsing($query, $columns, $sql);

$sql .= ' on conflict ('.$this->columnize($uniqueBy).') do update set ';

$columns = collect($update)->map(function ($value, $key) {
return is_numeric($key)
? $this->wrap($value).' = '.$this->wrap('excluded').'.'.$this->wrap($value)
: $this->wrap($key).' = '.$this->parameter($value);
})->implode(', ');

return $sql.$columns;
}

/**
* Compile a "lateral join" clause.
*
Expand Down
42 changes: 42 additions & 0 deletions src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,48 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar
return $sql.$columns;
}

/**
* Compile an "upsert" statement using a subquery into SQL.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $columns
* @param string $sql
* @param array $uniqueBy
* @param array $update
* @return string
*/
public function compileUpsertUsing(Builder $query, array $columns, string $sql, array $uniqueBy, array $update)
{
// To avoid potential parser ambiguity, when an INSERT statement to which UPSERT
// is attached takes its values from a SELECT statement, the SELECT statement
// should always include a WHERE clause, even if that's just "WHERE true".
if (! Str::contains($sql, ' where ', true)) {
$fromClausePosition = strpos(mb_strtolower($sql), ' from ');
$afterFromTablePosition = strpos($sql, ' ', $fromClausePosition + strlen(' from '));

if ($afterFromTablePosition === false) {
$afterFromTablePosition = strlen($sql);
}

$before = substr($sql, 0, $afterFromTablePosition);
$after = substr($sql, $afterFromTablePosition);

$sql = $before.' where true'.$after;
}

$sql = $this->compileInsertUsing($query, $columns, $sql);

$sql .= ' on conflict ('.$this->columnize($uniqueBy).') do update set ';

$columns = collect($update)->map(function ($value, $key) {
return is_numeric($key)
? $this->wrap($value).' = '.$this->wrap('excluded').'.'.$this->wrap($value)
: $this->wrap($key).' = '.$this->parameter($value);
})->implode(', ');

return $sql.$columns;
}

/**
* Group the nested JSON columns.
*
Expand Down
39 changes: 39 additions & 0 deletions src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,45 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar
return $sql;
}

/**
* Compile an "upsert" statement using a subquery into SQL.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $columns
* @param string $sql
* @param array $uniqueBy
* @param array $update
* @return string
*/
public function compileUpsertUsing(Builder $query, array $columns, string $sql, array $uniqueBy, array $update)
{
$compiledSql = 'merge '.$this->wrapTable($query->from).' ';

$columns = $this->columnize($columns);

$compiledSql .= 'using ('.$sql.') '.$this->wrapTable('laravel_source').' ('.$columns.') ';

$on = collect($uniqueBy)->map(function ($column) use ($query) {
return $this->wrap('laravel_source.'.$column).' = '.$this->wrap($query->from.'.'.$column);
})->implode(' and ');

$compiledSql .= 'on '.$on.' ';

if ($update) {
$update = collect($update)->map(function ($value, $key) {
return is_numeric($key)
? $this->wrap($value).' = '.$this->wrap('laravel_source.'.$value)
: $this->wrap($key).' = '.$this->parameter($value);
})->implode(', ');

$compiledSql .= 'when matched then update set '.$update.' ';
}

$compiledSql .= 'when not matched then insert ('.$columns.') values ('.$columns.');';

return $compiledSql;
}

/**
* Prepare the bindings for an update statement.
*
Expand Down
148 changes: 148 additions & 0 deletions tests/Database/DatabaseQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3263,6 +3263,154 @@ public function testUpsertMethodWithUpdateColumns()
$this->assertEquals(2, $result);
}

public function testUpsertUsingMethod()
{
$builder = $this->getMySqlBuilder();
$builder->getConnection()
->shouldReceive('getDatabaseName')->andReturn('test')
->shouldReceive('getConfig')->with('use_upsert_alias')->andReturn(false)
->shouldReceive('affectingStatement')->once()->with('insert into `users` (`email`, `name`) select `column1`, `column2` from `table2` where `foreign_id` = ? on duplicate key update `email` = values(`email`), `name` = values(`name`)', [5])->andReturn(2);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2')->where('foreign_id', '=', 5);
}, 'email');
$this->assertEquals(2, $result);

$builder = $this->getMySqlBuilder();
$builder->getConnection()
->shouldReceive('getDatabaseName')->andReturn('test')
->shouldReceive('getConfig')->with('use_upsert_alias')->andReturn(true)
->shouldReceive('affectingStatement')->once()->with('insert into `users` (`email`, `name`) select `column1`, `column2` from `table2` where `foreign_id` = ? as laravel_upsert_alias on duplicate key update `email` = `laravel_upsert_alias`.`email`, `name` = `laravel_upsert_alias`.`name`', [5])->andReturn(2);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2')->where('foreign_id', '=', 5);
}, 'email');
$this->assertEquals(2, $result);

$builder = $this->getPostgresBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") select "column1", "column2" from "table2" where "foreign_id" = ? on conflict ("email") do update set "email" = "excluded"."email", "name" = "excluded"."name"', [5])->andReturn(2);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2')->where('foreign_id', '=', 5);
}, 'email');
$this->assertEquals(2, $result);

$builder = $this->getSQLiteBuilder();
$builder->getConnection()
->shouldReceive('getDatabaseName')->andReturn('test')
->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") select "column1", "column2" from "table2" where "foreign_id" = ? on conflict ("email") do update set "email" = "excluded"."email", "name" = "excluded"."name"', [5])->andReturn(2);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2')->where('foreign_id', '=', 5);
}, 'email');
$this->assertEquals(2, $result);

$builder = $this->getSqlServerBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('merge [users] using (select [column1], [column2] from [table2] where [foreign_id] = ?) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [email] = [laravel_source].[email], [name] = [laravel_source].[name] when not matched then insert ([email], [name]) values ([email], [name]);', [5])->andReturn(2);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2')->where('foreign_id', '=', 5);
}, 'email');
$this->assertEquals(2, $result);
}

public function testUpsertUsingMethodWithUpdateColumns()
{
$builder = $this->getMySqlBuilder();
$builder->getConnection()
->shouldReceive('getDatabaseName')->andReturn('test')
->shouldReceive('getConfig')->with('use_upsert_alias')->andReturn(false)
->shouldReceive('affectingStatement')->once()->with('insert into `users` (`email`, `name`) select `column1`, `column2` from `table2` on duplicate key update `name` = values(`name`)', [])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name']);
$this->assertEquals(1, $result);

$builder = $this->getMySqlBuilder();
$builder->getConnection()
->shouldReceive('getDatabaseName')->andReturn('test')
->shouldReceive('getConfig')->with('use_upsert_alias')->andReturn(false)
->shouldReceive('affectingStatement')->once()->with('insert into `users` (`email`, `name`) select `column1`, `column2` from `table2` on duplicate key update `name` = ?', ['New name'])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name' => 'New name']);
$this->assertEquals(1, $result);

$builder = $this->getMySqlBuilder();
$builder->getConnection()
->shouldReceive('getDatabaseName')->andReturn('test')
->shouldReceive('getConfig')->with('use_upsert_alias')->andReturn(false)
->shouldReceive('affectingStatement')->once()->with('insert into `users` (`email`, `name`) select `column1`, `column2` from `table2` on duplicate key update `name` = concat(`name`, " 2")', [])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name' => new Raw('concat(`name`, " 2")')]);
$this->assertEquals(1, $result);

$builder = $this->getPostgresBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") select "column1", "column2" from "table2" on conflict ("email") do update set "name" = "excluded"."name"', [])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name']);
$this->assertEquals(1, $result);

$builder = $this->getPostgresBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") select "column1", "column2" from "table2" on conflict ("email") do update set "name" = ?', ['New name'])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name' => 'New name']);
$this->assertEquals(1, $result);

$builder = $this->getPostgresBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") select "column1", "column2" from "table2" on conflict ("email") do update set "name" = concat("name", \' 2\')', [])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name' => new Raw('concat("name", \' 2\')')]);
$this->assertEquals(1, $result);

$builder = $this->getSQLiteBuilder();
$builder->getConnection()
->shouldReceive('getDatabaseName')->andReturn('test')
->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") select "column1", "column2" from "table2" where true on conflict ("email") do update set "name" = "excluded"."name"', [])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name']);
$this->assertEquals(1, $result);

$builder = $this->getSQLiteBuilder();
$builder->getConnection()
->shouldReceive('getDatabaseName')->andReturn('test')
->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") select "column1", "column2" from "table2" where true on conflict ("email") do update set "name" = ?', ['New name'])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name' => 'New name']);
$this->assertEquals(1, $result);

$builder = $this->getSQLiteBuilder();
$builder->getConnection()
->shouldReceive('getDatabaseName')->andReturn('test')
->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") select "column1", "column2" from "table2" where true on conflict ("email") do update set "name" = concat("name", \' 2\')', [])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name' => new Raw('concat("name", \' 2\')')]);
$this->assertEquals(1, $result);

$builder = $this->getSqlServerBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('merge [users] using (select [column1], [column2] from [table2]) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [name] = [laravel_source].[name] when not matched then insert ([email], [name]) values ([email], [name]);', [])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name']);
$this->assertEquals(1, $result);

$builder = $this->getSqlServerBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('merge [users] using (select [column1], [column2] from [table2]) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [name] = ? when not matched then insert ([email], [name]) values ([email], [name]);', ['New name'])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name' => 'New name']);
$this->assertEquals(1, $result);

$builder = $this->getSqlServerBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('merge [users] using (select [column1], [column2] from [table2]) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [name] = concat([name], " 2") when not matched then insert ([email], [name]) values ([email], [name]);', [])->andReturn(1);
$result = $builder->from('users')->upsertUsing(['email', 'name'], function (Builder $query) {
$query->select(['column1', 'column2'])->from('table2');
}, 'email', ['name' => new Raw('concat([name], " 2")')]);
$this->assertEquals(1, $result);
}

public function testUpdateMethodWithJoins()
{
$builder = $this->getBuilder();
Expand Down