diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 6e31dbb0d263..56e8fc2b0e0b 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -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. * diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index b8eed21e69fd..e622ba05589b 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -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. * diff --git a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php index 3d900eeb3c24..10be5379490e 100755 --- a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php @@ -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. * diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index c22720a05c7c..95dcc46721a4 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -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. * diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index e4794234686d..a8c4945cc011 100755 --- a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php @@ -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. * diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index 062041c37d30..a5906066c048 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -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. * diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index ab626dfad9c1..0c190b253848 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -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();