Skip to content

Commit

Permalink
Add "upsertUsing" method to query builder
Browse files Browse the repository at this point in the history
  • Loading branch information
nikazooz committed Feb 28, 2024
1 parent eb66928 commit d9c196c
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 0 deletions.
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
18 changes: 18 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,24 @@ 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 $values
* @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
25 changes: 25 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,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;
}

/**
* 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" 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" 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" 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

0 comments on commit d9c196c

Please sign in to comment.