diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e394001..73a4faa8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,11 @@ All notable changes to this project will be documented in this file. ## [4.7.0] - coming soon +* Add `Query\Builder::upsert()` method by @GromNaN in [#3052](https://github.com/mongodb/laravel-mongodb/pull/3052) * Add `Connection::getServerVersion()` by @GromNaN in [#3043](https://github.com/mongodb/laravel-mongodb/pull/3043) * Add `Schema\Builder::getTables()` and `getTableListing()` by @GromNaN in [#3044](https://github.com/mongodb/laravel-mongodb/pull/3044) * Add `Schema\Builder::getColumns()` and `getIndexes()` by @GromNaN in [#3045](https://github.com/mongodb/laravel-mongodb/pull/3045) -* Add `Schema\Builder::hasColumn` and `hasColumns` method by @Alex-Belyi in [#3002](https://github.com/mongodb/laravel-mongodb/pull/3001) +* Add `Schema\Builder::hasColumn` and `hasColumns` method by @Alex-Belyi in [#3001](https://github.com/mongodb/laravel-mongodb/pull/3001) ## [4.6.0] - 2024-07-09 diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 89faa4b17..1d4dcf153 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -725,6 +725,47 @@ public function update(array $values, array $options = []) return $this->performUpdate($values, $options); } + /** @inheritdoc */ + public function upsert(array $values, $uniqueBy, $update = null): int + { + if ($values === []) { + return 0; + } + + $this->applyBeforeQueryCallbacks(); + + $options = $this->inheritConnectionOptions(); + $uniqueBy = array_fill_keys((array) $uniqueBy, 1); + + // If no update fields are specified, all fields are updated + if ($update !== null) { + $update = array_fill_keys((array) $update, 1); + } + + $bulk = []; + + foreach ($values as $value) { + $filter = $operation = []; + foreach ($value as $key => $val) { + if (isset($uniqueBy[$key])) { + $filter[$key] = $val; + } + + if ($update === null || array_key_exists($key, $update)) { + $operation['$set'][$key] = $val; + } else { + $operation['$setOnInsert'][$key] = $val; + } + } + + $bulk[] = ['updateOne' => [$filter, $operation, ['upsert' => true]]]; + } + + $result = $this->collection->bulkWrite($bulk, $options); + + return $result->getInsertedCount() + $result->getUpsertedCount() + $result->getModifiedCount(); + } + /** @inheritdoc */ public function increment($column, $amount = 1, array $extra = [], array $options = []) { diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 3c4cbd8df..57e49574f 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -143,6 +143,40 @@ public function testUpdate(): void $this->assertEquals('Hans Thomas', $check->fullname); } + public function testUpsert() + { + $result = User::upsert([ + ['email' => 'foo', 'name' => 'bar'], + ['name' => 'bar2', 'email' => 'foo2'], + ], ['email']); + + $this->assertSame(2, $result); + + $this->assertSame(2, $result); + $this->assertSame(2, User::count()); + $this->assertSame('bar', User::where('email', 'foo')->first()->name); + + // Update 1 document + $result = User::upsert([ + ['email' => 'foo', 'name' => 'bar2'], + ['name' => 'bar2', 'email' => 'foo2'], + ], 'email', ['name']); + + // Even if the same value is set for the 2nd document, the "updated_at" field is updated + $this->assertSame(2, $result); + $this->assertSame(2, User::count()); + $this->assertSame('bar2', User::where('email', 'foo')->first()->name); + + // If no update fields are specified, all fields are updated + $result = User::upsert([ + ['email' => 'foo', 'name' => 'bar3'], + ], 'email'); + + $this->assertSame(1, $result); + $this->assertSame(2, User::count()); + $this->assertSame('bar3', User::where('email', 'foo')->first()->name); + } + public function testManualStringId(): void { $user = new User(); diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 4320e6a54..7924e02f3 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -11,6 +11,7 @@ use Illuminate\Support\LazyCollection; use Illuminate\Support\Str; use Illuminate\Testing\Assert; +use Illuminate\Tests\Database\DatabaseQueryBuilderTest; use InvalidArgumentException; use MongoDB\BSON\ObjectId; use MongoDB\BSON\Regex; @@ -588,7 +589,7 @@ public function testSubdocumentArrayAggregate() $this->assertEquals(12, DB::collection('items')->avg('amount.*.hidden')); } - public function testUpsert() + public function testUpdateWithUpsert() { DB::collection('items')->where('name', 'knife') ->update( @@ -607,6 +608,39 @@ public function testUpsert() $this->assertEquals(2, DB::collection('items')->count()); } + public function testUpsert() + { + /** @see DatabaseQueryBuilderTest::testUpsertMethod() */ + // Insert 2 documents + $result = DB::collection('users')->upsert([ + ['email' => 'foo', 'name' => 'bar'], + ['name' => 'bar2', 'email' => 'foo2'], + ], 'email', 'name'); + + $this->assertSame(2, $result); + $this->assertSame(2, DB::collection('users')->count()); + $this->assertSame('bar', DB::collection('users')->where('email', 'foo')->first()['name']); + + // Update 1 document + $result = DB::collection('users')->upsert([ + ['email' => 'foo', 'name' => 'bar2'], + ['name' => 'bar2', 'email' => 'foo2'], + ], 'email', 'name'); + + $this->assertSame(1, $result); + $this->assertSame(2, DB::collection('users')->count()); + $this->assertSame('bar2', DB::collection('users')->where('email', 'foo')->first()['name']); + + // If no update fields are specified, all fields are updated + $result = DB::collection('users')->upsert([ + ['email' => 'foo', 'name' => 'bar3'], + ], 'email'); + + $this->assertSame(1, $result); + $this->assertSame(2, DB::collection('users')->count()); + $this->assertSame('bar3', DB::collection('users')->where('email', 'foo')->first()['name']); + } + public function testUnset() { $id1 = DB::collection('users')->insertGetId(['name' => 'John Doe', 'note1' => 'ABC', 'note2' => 'DEF']);