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

[8.x] Support created_at timestamps on upserts #34712

Merged
merged 7 commits into from Oct 7, 2020
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
69 changes: 65 additions & 4 deletions src/Illuminate/Database/Eloquent/Builder.php
Expand Up @@ -805,13 +805,28 @@ public function update(array $values)
*
* @param array $values
* @param array|string $uniqueBy
* @param array|null $update
* @return int
*/
public function upsert(array $values, $uniqueBy)
public function upsert(array $values, $uniqueBy, $update = null)
{
return $this->toBase()->upsert(collect($values)->map(function ($value, $key) {
return $this->addUpdatedAtColumn($value);
})->toArray(), $uniqueBy);
if (empty($values)) {
return 0;
}

if (! is_array(reset($values))) {
$values = [$values];
}

if (is_null($update)) {
$update = array_keys(reset($values));
}

$values = $this->addTimestampsToValues($values);

$update = $this->addUpdatedAtToColumns($update);

return $this->toBase()->upsert($values, $uniqueBy, $update);
}

/**
Expand Down Expand Up @@ -844,6 +859,52 @@ public function decrement($column, $amount = 1, array $extra = [])
);
}

/**
* Add timestamps to the inserted values.
*
* @param array $values
* @return array
*/
protected function addTimestampsToValues(array $values)
{
if (! $this->model->usesTimestamps()) {
return $values;
}

$timestamp = $this->model->freshTimestampString();

$columns = array_filter([$this->model->getCreatedAtColumn(), $this->model->getUpdatedAtColumn()]);

foreach ($columns as $column) {
foreach ($values as &$row) {
$row = array_merge([$column => $timestamp], $row);
}
}

return $values;
}

/**
* Add the "updated at" column to the updated columns.
*
* @param array $update
* @return array
*/
protected function addUpdatedAtToColumns(array $update)
{
if (! $this->model->usesTimestamps()) {
return $update;
}

$column = $this->model->getUpdatedAtColumn();

if (! is_null($column) && ! array_key_exists($column, $update) && ! in_array($column, $update)) {
$update[] = $column;
}

return $update;
}

/**
* Add the "updated at" column to an array of values.
*
Expand Down
22 changes: 19 additions & 3 deletions src/Illuminate/Database/Query/Builder.php
Expand Up @@ -2928,14 +2928,19 @@ public function updateOrInsert(array $attributes, array $values = [])
*
* @param array $values
* @param array|string $uniqueBy
* @param array|null $update
* @return int
*/
public function upsert(array $values, $uniqueBy)
public function upsert(array $values, $uniqueBy, $update = null)
{
if (empty($values)) {
return 0;
}

if ($update === []) {
return (int) $this->insert($values);
}

if (! is_array(reset($values))) {
$values = [$values];
} else {
Expand All @@ -2946,9 +2951,20 @@ public function upsert(array $values, $uniqueBy)
}
}

if (is_null($update)) {
$update = array_keys(reset($values));
}

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

return $this->connection->affectingStatement(
$this->grammar->compileUpsert($this, $values, (array) $uniqueBy),
$this->cleanBindings(Arr::flatten($values, 1))
$this->grammar->compileUpsert($this, $values, (array) $uniqueBy, $update),
$bindings
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/Illuminate/Database/Query/Grammars/Grammar.php
Expand Up @@ -1001,11 +1001,12 @@ protected function compileUpdateWithJoins(Builder $query, $table, $columns, $whe
* @param \Illuminate\Database\Query\Builder $query
* @param array $values
* @param array $uniqueBy
* @param array $update
* @return string
*
* @throws \RuntimeException
*/
public function compileUpsert(Builder $query, array $values, array $uniqueBy)
public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update)
{
throw new RuntimeException('This database engine does not support upserts.');
}
Expand Down
9 changes: 6 additions & 3 deletions src/Illuminate/Database/Query/Grammars/MySqlGrammar.php
Expand Up @@ -159,14 +159,17 @@ protected function compileUpdateColumns(Builder $query, array $values)
* @param \Illuminate\Database\Query\Builder $query
* @param array $values
* @param array $uniqueBy
* @param array $update
* @return string
*/
public function compileUpsert(Builder $query, array $values, array $uniqueBy)
public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update)
{
$sql = $this->compileInsert($query, $values).' on duplicate key update ';

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

return $sql.$columns;
Expand Down
9 changes: 6 additions & 3 deletions src/Illuminate/Database/Query/Grammars/PostgresGrammar.php
Expand Up @@ -224,16 +224,19 @@ protected function compileUpdateColumns(Builder $query, array $values)
* @param \Illuminate\Database\Query\Builder $query
* @param array $values
* @param array $uniqueBy
* @param array $update
* @return string
*/
public function compileUpsert(Builder $query, array $values, array $uniqueBy)
public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update)
{
$sql = $this->compileInsert($query, $values);

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

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

return $sql.$columns;
Expand Down
9 changes: 6 additions & 3 deletions src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php
Expand Up @@ -188,16 +188,19 @@ protected function compileUpdateColumns(Builder $query, array $values)
* @param \Illuminate\Database\Query\Builder $query
* @param array $values
* @param array $uniqueBy
* @param array $update
* @return string
*/
public function compileUpsert(Builder $query, array $values, array $uniqueBy)
public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update)
{
$sql = $this->compileInsert($query, $values);

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

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

return $sql.$columns;
Expand Down
13 changes: 12 additions & 1 deletion src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php
Expand Up @@ -347,9 +347,10 @@ protected function compileUpdateWithJoins(Builder $query, $table, $columns, $whe
* @param \Illuminate\Database\Query\Builder $query
* @param array $values
* @param array $uniqueBy
* @param array $update
* @return string
*/
public function compileUpsert(Builder $query, array $values, array $uniqueBy)
public function compileUpsert(Builder $query, array $values, array $uniqueBy, array $update)
{
$columns = $this->columnize(array_keys(reset($values)));

Expand All @@ -367,6 +368,16 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy)

$sql .= '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(', ');

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

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

return $sql;
Expand Down
8 changes: 5 additions & 3 deletions tests/Database/DatabaseEloquentBuilderTest.php
Expand Up @@ -1329,10 +1329,12 @@ public function testUpsert()

$query->shouldReceive('upsert')->once()
->with([
['email' => 'foo', 'name' => 'bar', 'foo_table.updated_at' => $now],
['name' => 'bar2', 'email' => 'foo2', 'foo_table.updated_at' => $now], ], 'email')->andReturn(2);
['email' => 'foo', 'name' => 'bar', 'updated_at' => $now, 'created_at' => $now],
['name' => 'bar2', 'email' => 'foo2', 'updated_at' => $now, 'created_at' => $now],
], ['email'], ['email', 'name', 'updated_at'])->andReturn(2);

$result = $builder->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], ['email']);

$result = $builder->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email');
$this->assertEquals(2, $result);

Carbon::setTestNow(null);
Expand Down
25 changes: 24 additions & 1 deletion tests/Database/DatabaseQueryBuilderTest.php
Expand Up @@ -2206,11 +2206,34 @@ public function testUpsertMethod()
$this->assertEquals(2, $result);

$builder = $this->getSqlServerBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('merge [users] using (values (?, ?), (?, ?)) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when not matched then insert ([email], [name]) values ([email], [name])', ['foo', 'bar', 'foo2', 'bar2'])->andReturn(2);
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('merge [users] using (values (?, ?), (?, ?)) [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])', ['foo', 'bar', 'foo2', 'bar2'])->andReturn(2);
$result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email');
$this->assertEquals(2, $result);
}

public function testUpsertMethodWithUpdateColumns()
{
$builder = $this->getMySqlBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert into `users` (`email`, `name`) values (?, ?), (?, ?) on duplicate key update `name` = values(`name`)', ['foo', 'bar', 'foo2', 'bar2'])->andReturn(2);
$result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email', ['name']);
$this->assertEquals(2, $result);

$builder = $this->getPostgresBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") values (?, ?), (?, ?) on conflict ("email") do update set "name" = "excluded"."name"', ['foo', 'bar', 'foo2', 'bar2'])->andReturn(2);
$result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email', ['name']);
$this->assertEquals(2, $result);

$builder = $this->getSQLiteBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('insert into "users" ("email", "name") values (?, ?), (?, ?) on conflict ("email") do update set "name" = "excluded"."name"', ['foo', 'bar', 'foo2', 'bar2'])->andReturn(2);
$result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email', ['name']);
$this->assertEquals(2, $result);

$builder = $this->getSqlServerBuilder();
$builder->getConnection()->shouldReceive('affectingStatement')->once()->with('merge [users] using (values (?, ?), (?, ?)) [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])', ['foo', 'bar', 'foo2', 'bar2'])->andReturn(2);
$result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email', ['name']);
$this->assertEquals(2, $result);
}

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