From 7d793e496c7a83eb67f2848fe996ad2711a4b1ab Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 5 Oct 2023 16:56:21 +0900 Subject: [PATCH 01/23] Revert "[10.x] Revert from using `createOrFirst` in other `*OrCreate` methods (#48531)" This reverts commit 408a3e3e3605de03392467bc4722e00de3db79f9. --- src/Illuminate/Database/Eloquent/Builder.php | 8 +++++--- .../Eloquent/Relations/BelongsToMany.php | 20 +++++++------------ .../Eloquent/Relations/HasManyThrough.php | 10 +++++----- .../Eloquent/Relations/HasOneOrMany.php | 10 +++++----- .../Database/DatabaseEloquentHasManyTest.php | 14 ++++++++++--- tests/Database/DatabaseEloquentMorphTest.php | 14 ++++++++++--- 6 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index a925336f6bb9..6d502d6cd118 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -567,7 +567,7 @@ public function firstOrCreate(array $attributes = [], array $values = []) return $instance; } - return $this->create(array_merge($attributes, $values)); + return $this->createOrFirst($attributes, $values); } /** @@ -595,8 +595,10 @@ public function createOrFirst(array $attributes = [], array $values = []) */ public function updateOrCreate(array $attributes, array $values = []) { - return tap($this->firstOrNew($attributes), function ($instance) use ($values) { - $instance->fill($values)->save(); + return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { + if (! $instance->wasRecentlyCreated) { + $instance->fill($values)->save(); + } }); } diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index dbf71768ccc3..37c698f3d80f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -622,7 +622,7 @@ public function firstOrCreate(array $attributes = [], array $values = [], array { if (is_null($instance = (clone $this)->where($attributes)->first())) { if (is_null($instance = $this->related->where($attributes)->first())) { - $instance = $this->create(array_merge($attributes, $values), $joining, $touch); + $instance = $this->createOrFirst($attributes, $values, $joining, $touch); } else { try { $this->getQuery()->withSavepointIfNeeded(fn () => $this->attach($instance, $joining, $touch)); @@ -672,19 +672,13 @@ public function createOrFirst(array $attributes = [], array $values = [], array */ public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) { - if (is_null($instance = (clone $this)->where($attributes)->first())) { - if (is_null($instance = $this->related->where($attributes)->first())) { - return $this->create(array_merge($attributes, $values), $joining, $touch); - } else { - $this->attach($instance, $joining, $touch); - } - } - - $instance->fill($values); + return tap($this->firstOrCreate($attributes, $values, $joining, $touch), function ($instance) use ($values) { + if (! $instance->wasRecentlyCreated) { + $instance->fill($values); - $instance->save(['touch' => false]); - - return $instance; + $instance->save(['touch' => false]); + } + }); } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php index d8aa1809a9d8..dcbe20ea198a 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php @@ -305,11 +305,11 @@ public function createOrFirst(array $attributes = [], array $values = []) */ public function updateOrCreate(array $attributes, array $values = []) { - $instance = $this->firstOrNew($attributes); - - $instance->fill($values)->save(); - - return $instance; + return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { + if (! $instance->wasRecentlyCreated) { + $instance->fill($values)->save(); + } + }); } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index f107ec310e41..154dc0e4516f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -236,7 +236,7 @@ public function firstOrNew(array $attributes = [], array $values = []) public function firstOrCreate(array $attributes = [], array $values = []) { if (is_null($instance = $this->where($attributes)->first())) { - $instance = $this->create(array_merge($attributes, $values)); + $instance = $this->createOrFirst($attributes, $values); } return $instance; @@ -267,10 +267,10 @@ public function createOrFirst(array $attributes = [], array $values = []) */ public function updateOrCreate(array $attributes, array $values = []) { - return tap($this->firstOrNew($attributes), function ($instance) use ($values) { - $instance->fill($values); - - $instance->save(); + return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { + if (! $instance->wasRecentlyCreated) { + $instance->fill($values)->save(); + } }); } diff --git a/tests/Database/DatabaseEloquentHasManyTest.php b/tests/Database/DatabaseEloquentHasManyTest.php index 0bec03bc97fa..caa8b1d8196a 100755 --- a/tests/Database/DatabaseEloquentHasManyTest.php +++ b/tests/Database/DatabaseEloquentHasManyTest.php @@ -155,6 +155,7 @@ public function testFirstOrCreateMethodCreatesNewModelWithForeignKeySet() $relation = $this->getRelation(); $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope()); $model = $this->expectCreatedModel($relation, ['foo']); $this->assertEquals($model, $relation->firstOrCreate(['foo'])); @@ -165,6 +166,7 @@ public function testFirstOrCreateMethodWithValuesCreatesNewModelWithForeignKeySe $relation = $this->getRelation(); $relation->getQuery()->shouldReceive('where')->once()->with(['foo' => 'bar'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope()); $model = $this->expectCreatedModel($relation, ['foo' => 'bar', 'baz' => 'qux']); $this->assertEquals($model, $relation->firstOrCreate(['foo' => 'bar'], ['baz' => 'qux'])); @@ -225,7 +227,9 @@ public function testUpdateOrCreateMethodFindsFirstModelAndUpdates() $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn($model = m::mock(stdClass::class)); $relation->getRelated()->shouldReceive('newInstance')->never(); - $model->shouldReceive('fill')->once()->with(['bar']); + + $model->wasRecentlyCreated = false; + $model->shouldReceive('fill')->once()->with(['bar'])->andReturn($model); $model->shouldReceive('save')->once(); $this->assertInstanceOf(stdClass::class, $relation->updateOrCreate(['foo'], ['bar'])); @@ -234,11 +238,15 @@ public function testUpdateOrCreateMethodFindsFirstModelAndUpdates() public function testUpdateOrCreateMethodCreatesNewModelWithForeignKeySet() { $relation = $this->getRelation(); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(function ($scope) { + return $scope(); + }); $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); - $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo'])->andReturn($model = m::mock(Model::class)); + $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo', 'bar'])->andReturn($model = m::mock(Model::class)); + + $model->wasRecentlyCreated = true; $model->shouldReceive('save')->once()->andReturn(true); - $model->shouldReceive('fill')->once()->with(['bar']); $model->shouldReceive('setAttribute')->once()->with('foreign_key', 1); $this->assertInstanceOf(Model::class, $relation->updateOrCreate(['foo'], ['bar'])); diff --git a/tests/Database/DatabaseEloquentMorphTest.php b/tests/Database/DatabaseEloquentMorphTest.php index 924cd17fc97b..bb9da590e7e0 100755 --- a/tests/Database/DatabaseEloquentMorphTest.php +++ b/tests/Database/DatabaseEloquentMorphTest.php @@ -195,6 +195,7 @@ public function testFirstOrCreateMethodCreatesNewMorphModel() $relation = $this->getOneRelation(); $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope()); $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo'])->andReturn($model = m::mock(Model::class)); $model->shouldReceive('setAttribute')->once()->with('morph_id', 1); $model->shouldReceive('setAttribute')->once()->with('morph_type', get_class($relation->getParent())); @@ -208,6 +209,7 @@ public function testFirstOrCreateMethodWithValuesCreatesNewMorphModel() $relation = $this->getOneRelation(); $relation->getQuery()->shouldReceive('where')->once()->with(['foo' => 'bar'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope()); $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo' => 'bar', 'baz' => 'qux'])->andReturn($model = m::mock(Model::class)); $model->shouldReceive('setAttribute')->once()->with('morph_id', 1); $model->shouldReceive('setAttribute')->once()->with('morph_type', get_class($relation->getParent())); @@ -300,8 +302,10 @@ public function testUpdateOrCreateMethodFindsFirstModelAndUpdates() $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn($model = m::mock(Model::class)); $relation->getRelated()->shouldReceive('newInstance')->never(); + + $model->wasRecentlyCreated = false; $model->shouldReceive('setAttribute')->never(); - $model->shouldReceive('fill')->once()->with(['bar']); + $model->shouldReceive('fill')->once()->with(['bar'])->andReturn($model); $model->shouldReceive('save')->once(); $this->assertInstanceOf(Model::class, $relation->updateOrCreate(['foo'], ['bar'])); @@ -310,13 +314,17 @@ public function testUpdateOrCreateMethodFindsFirstModelAndUpdates() public function testUpdateOrCreateMethodCreatesNewMorphModel() { $relation = $this->getOneRelation(); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(function ($scope) { + return $scope(); + }); $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); - $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo'])->andReturn($model = m::mock(Model::class)); + $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo', 'bar'])->andReturn($model = m::mock(Model::class)); + + $model->wasRecentlyCreated = true; $model->shouldReceive('setAttribute')->once()->with('morph_id', 1); $model->shouldReceive('setAttribute')->once()->with('morph_type', get_class($relation->getParent())); $model->shouldReceive('save')->once()->andReturn(true); - $model->shouldReceive('fill')->once()->with(['bar']); $this->assertInstanceOf(Model::class, $relation->updateOrCreate(['foo'], ['bar'])); } From 495db28e81a06abc7fda31e6a622abb114cf4fb3 Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 5 Oct 2023 16:57:19 +0900 Subject: [PATCH 02/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20`Builder::cr?= =?UTF-8?q?eateOrFirst()`=20snapshot=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/DatabaseEloquentBuilderTest.php | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 470214f20a5c..24a8600d45ef 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -4,6 +4,7 @@ use BadMethodCallException; use Closure; +use Exception; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Eloquent\Builder; @@ -16,6 +17,7 @@ use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Database\Query\Grammars\Grammar; use Illuminate\Database\Query\Processors\Processor; +use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Carbon; use Illuminate\Support\Collection as BaseCollection; use Mockery as m; @@ -2135,6 +2137,69 @@ public function testUpdateWithAliasWithQualifiedTimestampValue() Carbon::setTestNow(null); } + public function testCreateOrFirstMethodCreatesNewRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentBuilderTestStubStringPrimaryKey(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection()->expects('insert')->with( + 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testCreateOrFirstMethodRetrievesExistingRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentBuilderTestStubStringPrimaryKey(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], false) + ->andReturn([[ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->newQuery()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + public function testUpsert() { Carbon::setTestNow($now = '2017-10-10 10:10:10'); From ebc9ba1c164e5688dd420fad0dc9eaea79d0afe9 Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 5 Oct 2023 17:30:42 +0900 Subject: [PATCH 03/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20`Builder::fi?= =?UTF-8?q?rstOrCreate()`=20snapshot=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/DatabaseEloquentBuilderTest.php | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 24a8600d45ef..8a710517bb59 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -2200,6 +2200,110 @@ public function testCreateOrFirstMethodRetrievesExistingRecord() }); } + public function testFirstOrCreateMethodRetrievesExistingRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentBuilderTestStubStringPrimaryKey(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testFirstOrCreateMethodCreatesNewRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentBuilderTestStubStringPrimaryKey(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentBuilderTestStubStringPrimaryKey(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $sql = 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with('select * from "foo_table" where ("attr" = ?) and ("attr" = ?) limit 1', ['foo', 'foo'], false) + ->andReturn([[ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + public function testUpsert() { Carbon::setTestNow($now = '2017-10-10 10:10:10'); From 1eb9b1a3a9b334638e54c22ee78e8d9767ae604f Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 5 Oct 2023 18:46:41 +0900 Subject: [PATCH 04/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20`Builder::up?= =?UTF-8?q?dateOrCreate()`=20snapshot=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/DatabaseEloquentBuilderTest.php | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 8a710517bb59..d9426adf67bc 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -2304,6 +2304,130 @@ public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() }); } + public function testUpdateOrCreateMethodUpdatesExistingRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentBuilderTestStubStringPrimaryKey(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection() + ->expects('update') + ->with( + 'update "foo_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', '123'], + ) + ->andReturn(1); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertEquals([ + 'id' => '123', + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testUpdateOrCreateMethodCreatesNewRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentBuilderTestStubStringPrimaryKey(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentBuilderTestStubStringPrimaryKey(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $sql = 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'baz', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with('select * from "foo_table" where ("attr" = ?) and ("attr" = ?) limit 1', ['foo', 'foo'], false) + ->andReturn([[ + 'id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection() + ->expects('update') + ->with( + 'update "foo_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', '123'], + ) + ->andReturn(1); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertEquals([ + 'id' => '123', + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + public function testUpsert() { Carbon::setTestNow($now = '2017-10-10 10:10:10'); From ff458774389498d70f15cd985317ecc06a702b2a Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 5 Oct 2023 19:24:42 +0900 Subject: [PATCH 05/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20test=20stubs?= =?UTF-8?q?=20for=20`DatabaseEloquentHasManyTest`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/DatabaseEloquentHasManyTest.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/Database/DatabaseEloquentHasManyTest.php b/tests/Database/DatabaseEloquentHasManyTest.php index caa8b1d8196a..739fdaaca03e 100755 --- a/tests/Database/DatabaseEloquentHasManyTest.php +++ b/tests/Database/DatabaseEloquentHasManyTest.php @@ -3,10 +3,13 @@ namespace Illuminate\Tests\Database; use Exception; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Database\UniqueConstraintViolationException; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -356,6 +359,22 @@ protected function getRelation() return new HasMany($builder, $parent, 'table.foreign_key', 'id'); } + protected function mockConnectionForModel($model, $database) + { + $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; + $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; + $grammar = new $grammarClass; + $processor = new $processorClass; + $connection = m::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new BaseBuilder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = m::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + $class = get_class($model); + $class::setConnectionResolver($resolver); + } + protected function expectNewModel($relation, $attributes = null) { $model = $this->getMockBuilder(Model::class)->onlyMethods(['setAttribute', 'save'])->getMock(); @@ -390,3 +409,26 @@ class EloquentHasManyModelStub extends Model { public $foreign_key = 'foreign.value'; } + +class EloquentHasManyModelStubWithRelation extends Model +{ + public $incrementing = false; + + protected $table = 'parent_table'; + + protected $keyType = 'string'; + + public function child() + { + return $this->hasMany(EloquentHasManyChildModelStub::class, 'parent_id'); + } +} + +class EloquentHasManyChildModelStub extends Model +{ + public $incrementing = false; + + protected $table = 'child_table'; + + protected $keyType = 'string'; +} From 9dad5175e50ec2e459179496b4b5f848956f862a Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 5 Oct 2023 19:26:15 +0900 Subject: [PATCH 06/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20`HasMany::cr?= =?UTF-8?q?eateOrFirst()`=20snapshot=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/DatabaseEloquentHasManyTest.php | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/Database/DatabaseEloquentHasManyTest.php b/tests/Database/DatabaseEloquentHasManyTest.php index 739fdaaca03e..dd2da9febeb2 100755 --- a/tests/Database/DatabaseEloquentHasManyTest.php +++ b/tests/Database/DatabaseEloquentHasManyTest.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Database\UniqueConstraintViolationException; +use Illuminate\Support\Carbon; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass; @@ -344,6 +345,74 @@ public function testCreateManyCreatesARelatedModelForEachRecord() $this->assertEquals($colin, $instances[1]); } + public function testCreateOrFirstMethodCreatesNewRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentHasManyModelStubWithRelation(); + $model->id = '123'; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->child()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testCreateOrFirstMethodRetrievesExistingRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentHasManyModelStubWithRelation(); + $model->id = '123'; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], false) + ->andReturn([[ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->child()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + protected function getRelation() { $builder = m::mock(Builder::class); From de4c48cdac8087e353fd10e67a0dc5a796de0d9b Mon Sep 17 00:00:00 2001 From: mpyw Date: Thu, 5 Oct 2023 20:16:20 +0900 Subject: [PATCH 07/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20`HasMany::fi?= =?UTF-8?q?rstOrCreate()`=20snapshot=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/DatabaseEloquentHasManyTest.php | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/Database/DatabaseEloquentHasManyTest.php b/tests/Database/DatabaseEloquentHasManyTest.php index dd2da9febeb2..adc9d912cdd5 100755 --- a/tests/Database/DatabaseEloquentHasManyTest.php +++ b/tests/Database/DatabaseEloquentHasManyTest.php @@ -413,6 +413,118 @@ public function testCreateOrFirstMethodRetrievesExistingRecord() }); } + public function testFirstOrCreateMethodCreatesNewRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentHasManyModelStubWithRelation(); + $model->id = '123'; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->child()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testFirstOrCreateMethodRetrievesExistingRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentHasManyModelStubWithRelation(); + $model->id = '123'; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) + ->andReturn([[ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $result = $model->child()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentHasManyModelStubWithRelation(); + $model->id = '123'; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) + ->andReturn([]); + + $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) and ("attr" = ?) limit 1', ['123', 'foo', 'foo'], false) + ->andReturn([[ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->child()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + protected function getRelation() { $builder = m::mock(Builder::class); From 1b2620c5bada6f2bb020f63f3d1546eb23e03bba Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 00:23:14 +0900 Subject: [PATCH 08/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20`HasMany::up?= =?UTF-8?q?dateOrCreate()`=20snapshot=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/DatabaseEloquentHasManyTest.php | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/tests/Database/DatabaseEloquentHasManyTest.php b/tests/Database/DatabaseEloquentHasManyTest.php index adc9d912cdd5..d87479dea16f 100755 --- a/tests/Database/DatabaseEloquentHasManyTest.php +++ b/tests/Database/DatabaseEloquentHasManyTest.php @@ -525,6 +525,132 @@ public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() }); } + public function testUpdateOrCreateMethodCreatesNewRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentHasManyModelStubWithRelation(); + $model->id = '123'; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->child()->updateOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testUpdateOrCreateMethodUpdatesExistingRecord() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentHasManyModelStubWithRelation(); + $model->id = '123'; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) + ->andReturn([[ + 'id' => '123', + 'parent_id' => '456', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $model->getConnection()->expects('update')->with( + 'update "child_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', '123'], + )->andReturn(1); + + $result = $model->child()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertEquals([ + 'id' => '123', + 'parent_id' => '456', + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow() + { + Carbon::setTestNow('2023-01-01 00:00:00'); + + Model::unguarded(function () { + $model = new EloquentHasManyModelStubWithRelation(); + $model->id = '123'; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) + ->andReturn([]); + + $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; + $bindings = ['foo', 'baz', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) and ("attr" = ?) limit 1', ['123', 'foo', 'foo'], false) + ->andReturn([[ + 'id' => '456', + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection()->expects('update')->with( + 'update "child_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', '456'], + )->andReturn(1); + + $result = $model->child()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertEquals([ + 'id' => '456', + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + protected function getRelation() { $builder = m::mock(Builder::class); From 02b11804c4fcef01011ab53410acaec6f67cae7d Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 6 Oct 2023 09:08:35 +0900 Subject: [PATCH 09/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20prepare=20HasManyT?= =?UTF-8?q?hrough=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatabaseEloquentHasManyThroughTest.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 tests/Database/DatabaseEloquentHasManyThroughTest.php diff --git a/tests/Database/DatabaseEloquentHasManyThroughTest.php b/tests/Database/DatabaseEloquentHasManyThroughTest.php new file mode 100644 index 000000000000..660a41aae8c0 --- /dev/null +++ b/tests/Database/DatabaseEloquentHasManyThroughTest.php @@ -0,0 +1,91 @@ + $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new Builder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + $class = get_class($model); + $class::setConnectionResolver($resolver); + } +} + +/** + * @property string $id + */ +class HasManyThroughChild extends Model +{ + public $incrementing = false; + + protected $table = 'child'; + + protected $keyType = 'string'; +} + +/** + * @property string $id + * @property string $parent_id + * @property string $child_id + */ +class HasManyThroughPivot extends Model +{ + public $incrementing = false; + + protected $table = 'pivot'; + + protected $keyType = 'string'; +} + +/** + * @property string $id + */ +class HasManyThroughParent extends Model +{ + public $incrementing = false; + + protected $table = ''; + + protected $keyType = 'string'; + + public function children(): HasManyThrough + { + return $this->hasManyThrough( + HasManyThroughChild::class, + HasManyThroughPivot::class, + 'parent_id', + 'id', + 'id', + 'child_id', + ); + } +} + From 08beb9849a967e1768ca51ff77633030bd40c333 Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 6 Oct 2023 09:10:37 +0900 Subject: [PATCH 10/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20HasManyThrou?= =?UTF-8?q?gh::CreateOrFirst()=20snapshot=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatabaseEloquentHasManyThroughTest.php | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/Database/DatabaseEloquentHasManyThroughTest.php b/tests/Database/DatabaseEloquentHasManyThroughTest.php index 660a41aae8c0..c18b98b488d1 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughTest.php @@ -22,6 +22,72 @@ public function setUp(): void parent::setUp(); Carbon::setTestNow('2023-01-01 00:00:00'); } + + public function testCreateOrFirstMethodCreatesNewRecord(): void + { + Model::unguarded(function () { + $parent = new HasManyThroughParent(); + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + $parent->getConnection()->expects('insert')->with( + 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $parent->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testCreateOrFirstMethodRetrievesExistingRecord(): void + { + Model::unguarded(function () { + $parent = new HasManyThroughParent(); + $parent->id = '123'; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $parent->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + ['123', 'foo'], + true, + ) + ->andReturn([[ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $parent->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + protected function mockConnectionForModel(Model $model, string $database): void { $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; From b91d6e864a638a9c69e723ae19811e358a84d61a Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 6 Oct 2023 09:35:34 +0900 Subject: [PATCH 11/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20HasManyThrou?= =?UTF-8?q?gh::firstOrCreate()=20snapshot=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatabaseEloquentHasManyThroughTest.php | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/tests/Database/DatabaseEloquentHasManyThroughTest.php b/tests/Database/DatabaseEloquentHasManyThroughTest.php index c18b98b488d1..8f03a08c5469 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughTest.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Query\Builder; +use Illuminate\Database\QueryException; use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Carbon; use Mockery; @@ -88,6 +89,114 @@ public function testCreateOrFirstMethodRetrievesExistingRecord(): void }); } + public function testFirstOrCreateMethodCreatesNewRecord(): void + { + Model::unguarded(function () { + $parent = new HasManyThroughParent(); + $parent->id = '123'; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + ['123', 'foo'], + true, + ) + ->andReturn([]); + + $parent->getConnection()->expects('insert')->with( + 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testFirstOrCreateMethodRetrievesExistingRecord(): void + { + Model::unguarded(function () { + $parent = new HasManyThroughParent(); + $parent->id = '123'; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + ['123', 'foo'], + true, + ) + ->andReturn([[ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() + { + Model::unguarded(function () { + $parent = new HasManyThroughParent(); + $parent->id = '123'; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + ['123', 'foo'], + true, + ) + ->andReturn([]); + + $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $parent->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new QueryException('Integrity constraint violation', $sql, $bindings, new Exception())); + + // Verify that it is directly thrown without being converted into retries or custom exceptions + $this->expectException(QueryException::class); + $this->expectExceptionMessage('Integrity constraint violation'); + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + protected function mockConnectionForModel(Model $model, string $database): void { $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; From eee246b7e43853f5e50f6af255c1c55d449e328a Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 6 Oct 2023 09:52:13 +0900 Subject: [PATCH 12/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20HasManyThrou?= =?UTF-8?q?gh::updateOrCreate()=20snapshot=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatabaseEloquentHasManyThroughTest.php | 111 +++++++++++++++++- 1 file changed, 108 insertions(+), 3 deletions(-) diff --git a/tests/Database/DatabaseEloquentHasManyThroughTest.php b/tests/Database/DatabaseEloquentHasManyThroughTest.php index 8f03a08c5469..179bb9085c38 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughTest.php @@ -186,17 +186,122 @@ public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() // Verify that it is directly thrown without being converted into retries or custom exceptions $this->expectException(QueryException::class); $this->expectExceptionMessage('Integrity constraint violation'); - $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + }); + } + + public function testUpdateOrCreateMethodCreatesNewRecord() + { + Model::unguarded(function () { + $parent = new HasManyThroughParent(); + $parent->id = '123'; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + ['123', 'foo'], + true, + ) + ->andReturn([]); + + $parent->getConnection() + ->expects('insert') + ->with( + 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'baz', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + ) + ->andReturnTrue(); + + $result = $parent->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); $this->assertEquals([ - 'parent_id' => '123', 'attr' => 'foo', - 'val' => 'bar', + 'val' => 'baz', 'created_at' => '2023-01-01T00:00:00.000000Z', 'updated_at' => '2023-01-01T00:00:00.000000Z', ], $result->toArray()); }); } + public function testUpdateOrCreateMethodUpdatesExistingRecord() + { + Model::unguarded(function () { + $parent = new HasManyThroughParent(); + $parent->id = '123'; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + ['123', 'foo'], + true, + ) + ->andReturn([[ + 'id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $parent->getConnection() + ->expects('update') + ->with( + 'update "child" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', '123'], + ) + ->andReturn(1); + + $result = $parent->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertEquals([ + 'id' => '123', + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + }); + } + + public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void + { + Model::unguarded(function () { + $parent = new HasManyThroughParent(); + $parent->id = '123'; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + ['123', 'foo'], + true, + ) + ->andReturn([]); + + $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $parent->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new QueryException('Integrity constraint violation', $sql, $bindings, new Exception())); + + // Verify that it is directly thrown without being converted into retries or custom exceptions + $this->expectException(QueryException::class); + $this->expectExceptionMessage('Integrity constraint violation'); + $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + }); + } + protected function mockConnectionForModel(Model $model, string $database): void { $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; From 15e8999f91b1ea7d1a34f04014a21676c0d20d1a Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 6 Oct 2023 13:32:18 +0900 Subject: [PATCH 13/23] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20use=20createOr?= =?UTF-8?q?First=20in=20firstOrCreate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php index dcbe20ea198a..2872bcdbd656 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php @@ -277,7 +277,7 @@ public function firstOrCreate(array $attributes = [], array $values = []) return $instance; } - return $this->create(array_merge($attributes, $values)); + return $this->createOrFirst(array_merge($attributes, $values)); } /** From b79c5f0119d15a6e4686776388352718eeb8bba0 Mon Sep 17 00:00:00 2001 From: fuwasegu Date: Fri, 6 Oct 2023 13:32:38 +0900 Subject: [PATCH 14/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20fix=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DatabaseEloquentHasManyThroughTest.php | 77 ++++++++++++++----- 1 file changed, 57 insertions(+), 20 deletions(-) diff --git a/tests/Database/DatabaseEloquentHasManyThroughTest.php b/tests/Database/DatabaseEloquentHasManyThroughTest.php index 179bb9085c38..89cde2a348fc 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughTest.php @@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Query\Builder; -use Illuminate\Database\QueryException; use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Carbon; use Mockery; @@ -66,7 +65,7 @@ public function testCreateOrFirstMethodRetrievesExistingRecord(): void $parent->getConnection() ->expects('select') ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', ['123', 'foo'], true, ) @@ -101,7 +100,7 @@ public function testFirstOrCreateMethodCreatesNewRecord(): void $parent->getConnection() ->expects('select') ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', ['123', 'foo'], true, ) @@ -134,7 +133,7 @@ public function testFirstOrCreateMethodRetrievesExistingRecord(): void $parent->getConnection() ->expects('select') ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', ['123', 'foo'], true, ) @@ -169,7 +168,7 @@ public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() $parent->getConnection() ->expects('select') ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', ['123', 'foo'], true, ) @@ -181,12 +180,31 @@ public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() $parent->getConnection() ->expects('insert') ->with($sql, $bindings) - ->andThrow(new QueryException('Integrity constraint violation', $sql, $bindings, new Exception())); + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) and ("attr" = ? and "val" = ?) limit 1', + ['123', 'foo', 'foo', 'bar'], + true, + ) + ->andReturn([[ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); - // Verify that it is directly thrown without being converted into retries or custom exceptions - $this->expectException(QueryException::class); - $this->expectExceptionMessage('Integrity constraint violation'); - $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'parent_id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); }); } @@ -202,7 +220,7 @@ public function testUpdateOrCreateMethodCreatesNewRecord() $parent->getConnection() ->expects('select') ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', ['123', 'foo'], true, ) @@ -238,7 +256,7 @@ public function testUpdateOrCreateMethodUpdatesExistingRecord() $parent->getConnection() ->expects('select') ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', ['123', 'foo'], true, ) @@ -281,7 +299,7 @@ public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void $parent->getConnection() ->expects('select') ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."child_id" = "child"."id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', ['123', 'foo'], true, ) @@ -293,12 +311,31 @@ public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void $parent->getConnection() ->expects('insert') ->with($sql, $bindings) - ->andThrow(new QueryException('Integrity constraint violation', $sql, $bindings, new Exception())); + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) and ("attr" = ? and "val" = ?) limit 1', + ['123', 'foo', 'foo', 'bar'], + true, + ) + ->andReturn([[ + 'id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); - // Verify that it is directly thrown without being converted into retries or custom exceptions - $this->expectException(QueryException::class); - $this->expectExceptionMessage('Integrity constraint violation'); - $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'id' => '123', + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); }); } @@ -321,6 +358,7 @@ protected function mockConnectionForModel(Model $model, string $database): void /** * @property string $id + * @property string $pivot_id */ class HasManyThroughChild extends Model { @@ -334,7 +372,6 @@ class HasManyThroughChild extends Model /** * @property string $id * @property string $parent_id - * @property string $child_id */ class HasManyThroughPivot extends Model { @@ -362,9 +399,9 @@ public function children(): HasManyThrough HasManyThroughChild::class, HasManyThroughPivot::class, 'parent_id', + 'pivot_id', 'id', 'id', - 'child_id', ); } } From 30d462774960f925e58c33293c077d605cfb9b3d Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 13:48:57 +0900 Subject: [PATCH 15/23] =?UTF-8?q?style:=20=F0=9F=92=84=20Apply=20StyleCI?= =?UTF-8?q?=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/Database/DatabaseEloquentHasManyThroughTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Database/DatabaseEloquentHasManyThroughTest.php b/tests/Database/DatabaseEloquentHasManyThroughTest.php index 89cde2a348fc..7a2e2d20d23a 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughTest.php @@ -405,4 +405,3 @@ public function children(): HasManyThrough ); } } - From f42cd8be5d2de1c255b4b0ff80a0396d2ab97bdf Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 14:06:23 +0900 Subject: [PATCH 16/23] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20Add=20missin?= =?UTF-8?q?g=20FIXME=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/Database/DatabaseEloquentHasManyThroughTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Database/DatabaseEloquentHasManyThroughTest.php b/tests/Database/DatabaseEloquentHasManyThroughTest.php index 7a2e2d20d23a..c6ec82d699eb 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughTest.php @@ -184,6 +184,7 @@ public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() $parent->getConnection() ->expects('select') + // FIXME: duplicate conditions ->with( 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) and ("attr" = ? and "val" = ?) limit 1', ['123', 'foo', 'foo', 'bar'], @@ -315,6 +316,7 @@ public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void $parent->getConnection() ->expects('select') + // FIXME: duplicate conditions ->with( 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) and ("attr" = ? and "val" = ?) limit 1', ['123', 'foo', 'foo', 'bar'], From be2f795e813953ffa79da171a0dec32f5f50db78 Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 14:06:41 +0900 Subject: [PATCH 17/23] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20Omit=20verbose?= =?UTF-8?q?=20arguments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/Database/DatabaseEloquentHasManyThroughTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Database/DatabaseEloquentHasManyThroughTest.php b/tests/Database/DatabaseEloquentHasManyThroughTest.php index c6ec82d699eb..51c90f67780b 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughTest.php @@ -402,8 +402,6 @@ public function children(): HasManyThrough HasManyThroughPivot::class, 'parent_id', 'pivot_id', - 'id', - 'id', ); } } From 77f23119d1f13313924041273485d8963cbd5b92 Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 16:16:02 +0900 Subject: [PATCH 18/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Rename=20`Database?= =?UTF-8?q?EloquentHasManyThroughTest`=20with=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...loquentHasManyThroughCreateOrFirstTest.php | 427 ++++++++++++++++++ .../DatabaseEloquentHasManyThroughTest.php | 407 ----------------- 2 files changed, 427 insertions(+), 407 deletions(-) create mode 100644 tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php delete mode 100644 tests/Database/DatabaseEloquentHasManyThroughTest.php diff --git a/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php b/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php new file mode 100644 index 000000000000..520abee94865 --- /dev/null +++ b/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php @@ -0,0 +1,427 @@ +id = 123; + $this->mockConnectionForModel($parent, 'SQLite', [789]); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + $parent->getConnection()->expects('insert')->with( + 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $parent->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testCreateOrFirstMethodRetrievesExistingRecord(): void + { + $parent = new HasManyThroughCreateOrFirstParent(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $parent->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $parent->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodCreatesNewRecord(): void + { + $parent = new HasManyThroughCreateOrFirstParent(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite', [789]); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $parent->getConnection()->expects('insert')->with( + 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRecord(): void + { + $parent = new HasManyThroughCreateOrFirstParent(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void + { + $parent = new HasManyThroughCreateOrFirstParent(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $parent->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $parent->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) and ("attr" = ? and "val" = ?) limit 1', + [123, 'foo', 'foo', 'bar'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodCreatesNewRecord(): void + { + $parent = new HasManyThroughCreateOrFirstParent(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite', [789]); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $parent->getConnection() + ->expects('insert') + ->with( + 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'baz', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + ) + ->andReturnTrue(); + + $result = $parent->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesExistingRecord(): void + { + $parent = new HasManyThroughCreateOrFirstParent(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $parent->getConnection() + ->expects('update') + ->with( + 'update "child" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 789], + ) + ->andReturn(1); + + $result = $parent->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void + { + $parent = new HasManyThroughCreateOrFirstParent(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $parent->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $parent->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) and ("attr" = ? and "val" = ?) limit 1', + [123, 'foo', 'foo', 'bar'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + protected function mockConnectionForModel(Model $model, string $database, array $lastInsertIds = []): void + { + $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; + $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; + $grammar = new $grammarClass; + $processor = new $processorClass; + $connection = Mockery::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new Builder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + + $class = get_class($model); + $class::setConnectionResolver($resolver); + + $connection->shouldReceive('getPdo')->andReturn($pdo = Mockery::mock(PDO::class)); + + foreach ($lastInsertIds as $id) { + $pdo->expects('lastInsertId')->andReturn($id); + } + } +} + +/** + * @property int $id + * @property int $pivot_id + */ +class HasManyThroughCreateOrFirstChild extends Model +{ + protected $table = 'child'; + protected $guarded = []; +} + +/** + * @property int $id + * @property int $parent_id + */ +class HasManyThroughCreateOrFirstPivot extends Model +{ + protected $table = 'pivot'; + protected $guarded = []; +} + +/** + * @property int $id + */ +class HasManyThroughCreateOrFirstParent extends Model +{ + protected $table = 'parent'; + protected $guarded = []; + + public function children(): HasManyThrough + { + return $this->hasManyThrough( + HasManyThroughCreateOrFirstChild::class, + HasManyThroughCreateOrFirstPivot::class, + 'parent_id', + 'pivot_id', + ); + } +} diff --git a/tests/Database/DatabaseEloquentHasManyThroughTest.php b/tests/Database/DatabaseEloquentHasManyThroughTest.php deleted file mode 100644 index 51c90f67780b..000000000000 --- a/tests/Database/DatabaseEloquentHasManyThroughTest.php +++ /dev/null @@ -1,407 +0,0 @@ -mockConnectionForModel($parent, 'SQLite'); - $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - $parent->getConnection()->expects('insert')->with( - 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', - ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], - )->andReturnTrue(); - - $result = $parent->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testCreateOrFirstMethodRetrievesExistingRecord(): void - { - Model::unguarded(function () { - $parent = new HasManyThroughParent(); - $parent->id = '123'; - $this->mockConnectionForModel($parent, 'SQLite'); - $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; - $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; - - $parent->getConnection() - ->expects('insert') - ->with($sql, $bindings) - ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); - - $parent->getConnection() - ->expects('select') - ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', - ['123', 'foo'], - true, - ) - ->andReturn([[ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $result = $parent->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testFirstOrCreateMethodCreatesNewRecord(): void - { - Model::unguarded(function () { - $parent = new HasManyThroughParent(); - $parent->id = '123'; - $this->mockConnectionForModel($parent, 'SQLite'); - $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $parent->getConnection() - ->expects('select') - ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', - ['123', 'foo'], - true, - ) - ->andReturn([]); - - $parent->getConnection()->expects('insert')->with( - 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', - ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], - )->andReturnTrue(); - - $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testFirstOrCreateMethodRetrievesExistingRecord(): void - { - Model::unguarded(function () { - $parent = new HasManyThroughParent(); - $parent->id = '123'; - $this->mockConnectionForModel($parent, 'SQLite'); - $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $parent->getConnection() - ->expects('select') - ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', - ['123', 'foo'], - true, - ) - ->andReturn([[ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() - { - Model::unguarded(function () { - $parent = new HasManyThroughParent(); - $parent->id = '123'; - $this->mockConnectionForModel($parent, 'SQLite'); - $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $parent->getConnection() - ->expects('select') - ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', - ['123', 'foo'], - true, - ) - ->andReturn([]); - - $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; - $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; - - $parent->getConnection() - ->expects('insert') - ->with($sql, $bindings) - ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); - - $parent->getConnection() - ->expects('select') - // FIXME: duplicate conditions - ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) and ("attr" = ? and "val" = ?) limit 1', - ['123', 'foo', 'foo', 'bar'], - true, - ) - ->andReturn([[ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ]]); - - $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testUpdateOrCreateMethodCreatesNewRecord() - { - Model::unguarded(function () { - $parent = new HasManyThroughParent(); - $parent->id = '123'; - $this->mockConnectionForModel($parent, 'SQLite'); - $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $parent->getConnection() - ->expects('select') - ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', - ['123', 'foo'], - true, - ) - ->andReturn([]); - - $parent->getConnection() - ->expects('insert') - ->with( - 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', - ['foo', 'baz', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], - ) - ->andReturnTrue(); - - $result = $parent->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); - $this->assertEquals([ - 'attr' => 'foo', - 'val' => 'baz', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testUpdateOrCreateMethodUpdatesExistingRecord() - { - Model::unguarded(function () { - $parent = new HasManyThroughParent(); - $parent->id = '123'; - $this->mockConnectionForModel($parent, 'SQLite'); - $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $parent->getConnection() - ->expects('select') - ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', - ['123', 'foo'], - true, - ) - ->andReturn([[ - 'id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ]]); - - $parent->getConnection() - ->expects('update') - ->with( - 'update "child" set "val" = ?, "updated_at" = ? where "id" = ?', - ['baz', '2023-01-01 00:00:00', '123'], - ) - ->andReturn(1); - - $result = $parent->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); - $this->assertEquals([ - 'id' => '123', - 'attr' => 'foo', - 'val' => 'baz', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void - { - Model::unguarded(function () { - $parent = new HasManyThroughParent(); - $parent->id = '123'; - $this->mockConnectionForModel($parent, 'SQLite'); - $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $parent->getConnection() - ->expects('select') - ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', - ['123', 'foo'], - true, - ) - ->andReturn([]); - - $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; - $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; - - $parent->getConnection() - ->expects('insert') - ->with($sql, $bindings) - ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); - - $parent->getConnection() - ->expects('select') - // FIXME: duplicate conditions - ->with( - 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) and ("attr" = ? and "val" = ?) limit 1', - ['123', 'foo', 'foo', 'bar'], - true, - ) - ->andReturn([[ - 'id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ]]); - - $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - protected function mockConnectionForModel(Model $model, string $database): void - { - $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; - $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; - $grammar = new $grammarClass; - $processor = new $processorClass; - $connection = Mockery::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); - $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { - return new Builder($connection, $grammar, $processor); - }); - $connection->shouldReceive('getDatabaseName')->andReturn('database'); - $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); - $class = get_class($model); - $class::setConnectionResolver($resolver); - } -} - -/** - * @property string $id - * @property string $pivot_id - */ -class HasManyThroughChild extends Model -{ - public $incrementing = false; - - protected $table = 'child'; - - protected $keyType = 'string'; -} - -/** - * @property string $id - * @property string $parent_id - */ -class HasManyThroughPivot extends Model -{ - public $incrementing = false; - - protected $table = 'pivot'; - - protected $keyType = 'string'; -} - -/** - * @property string $id - */ -class HasManyThroughParent extends Model -{ - public $incrementing = false; - - protected $table = ''; - - protected $keyType = 'string'; - - public function children(): HasManyThrough - { - return $this->hasManyThrough( - HasManyThroughChild::class, - HasManyThroughPivot::class, - 'parent_id', - 'pivot_id', - ); - } -} From 640d39c359d7723178a64809c290391b1b8ac1a5 Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 16:34:55 +0900 Subject: [PATCH 19/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20`BelongsToMa?= =?UTF-8?q?ny::createOrFirst/firstOrCreate`=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...EloquentBelongsToManyCreateOrFirstTest.php | 392 ++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php diff --git a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php new file mode 100644 index 000000000000..5f5a45362bb8 --- /dev/null +++ b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php @@ -0,0 +1,392 @@ +id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstRelated()], + 'SQLite', + [456], + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection()->expects('insert')->with( + 'insert into "related_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $source->getConnection()->expects('insert')->with( + 'insert into "pivot_table" ("related_id", "source_id") values (?, ?)', + [456, 123], + )->andReturnTrue(); + + $result = $source->related()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testCreateOrFirstMethodAssociatesExistingRelated(): void + { + $source = new BelongsToManyCreateOrFirstSource(); + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstRelated()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "related_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $source->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $source->getConnection() + ->expects('select') + ->with('select * from "related_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $source->getConnection()->expects('insert')->with( + 'insert into "pivot_table" ("related_id", "source_id") values (?, ?)', + [456, 123], + )->andReturnTrue(); + + $result = $source->related()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + // Pivot is not loaded when related model is newly created. + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRelatedAlreadyAssociated(): void + { + $source = new BelongsToManyCreateOrFirstSource(); + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstRelated()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection() + ->expects('select') + ->with( + 'select "related_table".*, "pivot_table"."source_id" as "pivot_source_id", "pivot_table"."related_id" as "pivot_related_id" from "related_table" inner join "pivot_table" on "related_table"."id" = "pivot_table"."related_id" where "pivot_table"."source_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + 'pivot_source_id' => 123, + 'pivot_related_id' => 456, + ]]); + + $result = $source->related()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + 'pivot' => [ + 'source_id' => 123, + 'related_id' => 456, + ], + ], $result->toArray()); + } + + public function testCreateOrFirstMethodRetrievesExistingRelatedAssociatedJustNow(): void + { + $source = new BelongsToManyCreateOrFirstSource(); + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstRelated()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "related_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $source->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $source->getConnection() + ->expects('select') + ->with('select * from "related_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $sql = 'insert into "pivot_table" ("related_id", "source_id") values (?, ?)'; + $bindings = [456, 123]; + + $source->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $source->getConnection() + ->expects('select') + ->with( + 'select "related_table".*, "pivot_table"."source_id" as "pivot_source_id", "pivot_table"."related_id" as "pivot_related_id" from "related_table" inner join "pivot_table" on "related_table"."id" = "pivot_table"."related_id" where "pivot_table"."source_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + false, + ) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + 'pivot_source_id' => 123, + 'pivot_related_id' => 456, + ]]); + + $result = $source->related()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + 'pivot' => [ + 'source_id' => 123, + 'related_id' => 456, + ], + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRelatedAndAssociatesIt(): void + { + $source = new BelongsToManyCreateOrFirstSource(); + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstRelated()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection() + ->expects('select') + ->with( + 'select "related_table".*, "pivot_table"."source_id" as "pivot_source_id", "pivot_table"."related_id" as "pivot_related_id" from "related_table" inner join "pivot_table" on "related_table"."id" = "pivot_table"."related_id" where "pivot_table"."source_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $source->getConnection() + ->expects('select') + ->with( + 'select * from "related_table" where ("attr" = ?) limit 1', + ['foo'], + true, + ) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $source->getConnection() + ->expects('insert') + ->with( + 'insert into "pivot_table" ("related_id", "source_id") values (?, ?)', + [456, 123], + ) + ->andReturnTrue(); + + $result = $source->related()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + // Pivot is not loaded when related model is newly created. + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodFallsBackToCreateOrFirst(): void + { + $source = new class() extends BelongsToManyCreateOrFirstSource + { + protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null): BelongsToMany + { + $relation = Mockery::mock(BelongsToMany::class)->makePartial(); + $relation->__construct(...func_get_args()); + $relation + ->expects('createOrFirst') + ->with(['attr' => 'foo'], ['val' => 'bar'], [], true) + ->andReturn(new BelongsToManyCreateOrFirstRelated([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ])); + + return $relation; + } + }; + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstRelated()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection() + ->expects('select') + ->with( + 'select "related_table".*, "pivot_table"."source_id" as "pivot_source_id", "pivot_table"."related_id" as "pivot_related_id" from "related_table" inner join "pivot_table" on "related_table"."id" = "pivot_table"."related_id" where "pivot_table"."source_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $source->getConnection() + ->expects('select') + ->with( + 'select * from "related_table" where ("attr" = ?) limit 1', + ['foo'], + true, + ) + ->andReturn([]); + + $result = $source->related()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + protected function mockConnectionForModels(array $models, string $database, array $lastInsertIds = []): void + { + $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; + $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; + $grammar = new $grammarClass; + $processor = new $processorClass; + $connection = Mockery::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new BaseBuilder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + + foreach ($models as $model) { + /** @var Model $model */ + $class = get_class($model); + $class::setConnectionResolver($resolver); + } + + $connection->shouldReceive('getPdo')->andReturn($pdo = Mockery::mock(PDO::class)); + + foreach ($lastInsertIds as $id) { + $pdo->expects('lastInsertId')->andReturn($id); + } + } +} + +/** + * @property int $id + */ +class BelongsToManyCreateOrFirstRelated extends Model +{ + protected $table = 'related_table'; + protected $guarded = []; +} + +/** + * @property int $id + */ +class BelongsToManyCreateOrFirstSource extends Model +{ + protected $table = 'source_table'; + protected $guarded = []; + + public function related(): BelongsToMany + { + return $this->belongsToMany( + BelongsToManyCreateOrFirstRelated::class, + 'pivot_table', + 'source_id', + 'related_id', + ); + } +} From 29356029d8ffd85bd60a82acc8075e991b2c694e Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 16:55:21 +0900 Subject: [PATCH 20/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Extract=20`Databas?= =?UTF-8?q?eEloquentHasManyTest`=20cases=20with=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tabaseEloquentHasManyCreateOrFirstTest.php | 367 ++++++++++++++++++ .../Database/DatabaseEloquentHasManyTest.php | 349 ----------------- 2 files changed, 367 insertions(+), 349 deletions(-) create mode 100755 tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php diff --git a/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php new file mode 100755 index 000000000000..d4bf27a30023 --- /dev/null +++ b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php @@ -0,0 +1,367 @@ +id = 123; + $this->mockConnectionForModel($model, 'SQLite', [456]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testCreateOrFirstMethodRetrievesExistingRecord(): void + { + $model = new HasManyCreateOrFirstParent(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; + $bindings = ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], false) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodCreatesNewRecord(): void + { + $model = new HasManyCreateOrFirstParent(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite', [456]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRecord(): void + { + $model = new HasManyCreateOrFirstParent(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $result = $model->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void + { + $model = new HasManyCreateOrFirstParent(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([]); + + $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; + $bindings = ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) and ("attr" = ?) limit 1', [123, 'foo', 'foo'], false) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodCreatesNewRecord(): void + { + $model = new HasManyCreateOrFirstParent(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite', [456]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesExistingRecord(): void + { + $model = new HasManyCreateOrFirstParent(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $model->getConnection()->expects('update')->with( + 'update "child_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 456], + )->andReturn(1); + + $result = $model->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void + { + $model = new HasManyCreateOrFirstParent(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([]); + + $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; + $bindings = ['foo', 'baz', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) and ("attr" = ?) limit 1', [123, 'foo', 'foo'], false) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection()->expects('update')->with( + 'update "child_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 456], + )->andReturn(1); + + $result = $model->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + protected function mockConnectionForModel(Model $model, string $database, array $lastInsertIds = []): void + { + $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; + $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; + $grammar = new $grammarClass; + $processor = new $processorClass; + $connection = Mockery::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new Builder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + + $class = get_class($model); + $class::setConnectionResolver($resolver); + + $connection->shouldReceive('getPdo')->andReturn($pdo = Mockery::mock(PDO::class)); + + foreach ($lastInsertIds as $id) { + $pdo->expects('lastInsertId')->andReturn($id); + } + } +} + +/** + * @property int $id + */ +class HasManyCreateOrFirstParent extends Model +{ + protected $table = 'parent_table'; + protected $guarded = []; + + public function children(): HasMany + { + return $this->hasMany(HasManyCreateOrFirstChild::class, 'parent_id'); + } +} + +/** + * @property int $id + * @property int $parent_id + */ +class HasManyCreateOrFirstChild extends Model +{ + protected $table = 'child_table'; + protected $guarded = []; +} diff --git a/tests/Database/DatabaseEloquentHasManyTest.php b/tests/Database/DatabaseEloquentHasManyTest.php index d87479dea16f..caa8b1d8196a 100755 --- a/tests/Database/DatabaseEloquentHasManyTest.php +++ b/tests/Database/DatabaseEloquentHasManyTest.php @@ -3,15 +3,11 @@ namespace Illuminate\Tests\Database; use Exception; -use Illuminate\Database\ConnectionInterface; -use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Database\UniqueConstraintViolationException; -use Illuminate\Support\Carbon; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass; @@ -345,312 +341,6 @@ public function testCreateManyCreatesARelatedModelForEachRecord() $this->assertEquals($colin, $instances[1]); } - public function testCreateOrFirstMethodCreatesNewRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentHasManyModelStubWithRelation(); - $model->id = '123'; - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection()->expects('insert')->with( - 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', - ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], - )->andReturnTrue(); - - $result = $model->child()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testCreateOrFirstMethodRetrievesExistingRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentHasManyModelStubWithRelation(); - $model->id = '123'; - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; - $bindings = ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; - - $model->getConnection() - ->expects('insert') - ->with($sql, $bindings) - ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); - - $model->getConnection() - ->expects('select') - ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], false) - ->andReturn([[ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $result = $model->child()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testFirstOrCreateMethodCreatesNewRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentHasManyModelStubWithRelation(); - $model->id = '123'; - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) - ->andReturn([]); - - $model->getConnection()->expects('insert')->with( - 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', - ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], - )->andReturnTrue(); - - $result = $model->child()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testFirstOrCreateMethodRetrievesExistingRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentHasManyModelStubWithRelation(); - $model->id = '123'; - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) - ->andReturn([[ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ]]); - - $result = $model->child()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentHasManyModelStubWithRelation(); - $model->id = '123'; - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) - ->andReturn([]); - - $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; - $bindings = ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; - - $model->getConnection() - ->expects('insert') - ->with($sql, $bindings) - ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); - - $model->getConnection() - ->expects('select') - // FIXME: duplicate conditions - ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) and ("attr" = ?) limit 1', ['123', 'foo', 'foo'], false) - ->andReturn([[ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $result = $model->child()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testUpdateOrCreateMethodCreatesNewRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentHasManyModelStubWithRelation(); - $model->id = '123'; - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) - ->andReturn([]); - - $model->getConnection()->expects('insert')->with( - 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', - ['foo', 'bar', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], - )->andReturnTrue(); - - $result = $model->child()->updateOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testUpdateOrCreateMethodUpdatesExistingRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentHasManyModelStubWithRelation(); - $model->id = '123'; - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) - ->andReturn([[ - 'id' => '123', - 'parent_id' => '456', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ]]); - - $model->getConnection()->expects('update')->with( - 'update "child_table" set "val" = ?, "updated_at" = ? where "id" = ?', - ['baz', '2023-01-01 00:00:00', '123'], - )->andReturn(1); - - $result = $model->child()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); - $this->assertEquals([ - 'id' => '123', - 'parent_id' => '456', - 'attr' => 'foo', - 'val' => 'baz', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentHasManyModelStubWithRelation(); - $model->id = '123'; - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', ['123', 'foo'], true) - ->andReturn([]); - - $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; - $bindings = ['foo', 'baz', '123', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; - - $model->getConnection() - ->expects('insert') - ->with($sql, $bindings) - ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); - - $model->getConnection() - ->expects('select') - // FIXME: duplicate conditions - ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) and ("attr" = ?) limit 1', ['123', 'foo', 'foo'], false) - ->andReturn([[ - 'id' => '456', - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $model->getConnection()->expects('update')->with( - 'update "child_table" set "val" = ?, "updated_at" = ? where "id" = ?', - ['baz', '2023-01-01 00:00:00', '456'], - )->andReturn(1); - - $result = $model->child()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); - $this->assertEquals([ - 'id' => '456', - 'parent_id' => '123', - 'attr' => 'foo', - 'val' => 'baz', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - protected function getRelation() { $builder = m::mock(Builder::class); @@ -666,22 +356,6 @@ protected function getRelation() return new HasMany($builder, $parent, 'table.foreign_key', 'id'); } - protected function mockConnectionForModel($model, $database) - { - $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; - $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; - $grammar = new $grammarClass; - $processor = new $processorClass; - $connection = m::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); - $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { - return new BaseBuilder($connection, $grammar, $processor); - }); - $connection->shouldReceive('getDatabaseName')->andReturn('database'); - $resolver = m::mock(ConnectionResolverInterface::class, ['connection' => $connection]); - $class = get_class($model); - $class::setConnectionResolver($resolver); - } - protected function expectNewModel($relation, $attributes = null) { $model = $this->getMockBuilder(Model::class)->onlyMethods(['setAttribute', 'save'])->getMock(); @@ -716,26 +390,3 @@ class EloquentHasManyModelStub extends Model { public $foreign_key = 'foreign.value'; } - -class EloquentHasManyModelStubWithRelation extends Model -{ - public $incrementing = false; - - protected $table = 'parent_table'; - - protected $keyType = 'string'; - - public function child() - { - return $this->hasMany(EloquentHasManyChildModelStub::class, 'parent_id'); - } -} - -class EloquentHasManyChildModelStub extends Model -{ - public $incrementing = false; - - protected $table = 'child_table'; - - protected $keyType = 'string'; -} From 2419b1c15be2bc77d7db25c8c40987e5c48fd0a1 Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 17:13:19 +0900 Subject: [PATCH 21/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Extract=20`Databas?= =?UTF-8?q?eEloquentBuilderTest`=20cases=20with=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tabaseEloquentBuilderCreateOrFirstTest.php | 333 ++++++++++++++++++ .../Database/DatabaseEloquentBuilderTest.php | 293 --------------- 2 files changed, 333 insertions(+), 293 deletions(-) create mode 100755 tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php diff --git a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php new file mode 100755 index 000000000000..991b49a1c4e6 --- /dev/null +++ b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php @@ -0,0 +1,333 @@ +mockConnectionForModel($model, 'SQLite', [123]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection()->expects('insert')->with( + 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testCreateOrFirstMethodRetrievesExistingRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestStub(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], false) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->newQuery()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestStub(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodCreatesNewRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestStub(); + $this->mockConnectionForModel($model, 'SQLite', [123]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void + { + $model = new EloquentBuilderCreateOrFirstTestStub(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $sql = 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with('select * from "table" where ("attr" = ?) and ("attr" = ?) limit 1', ['foo', 'foo'], false) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesExistingRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestStub(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection() + ->expects('update') + ->with( + 'update "table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 123], + ) + ->andReturn(1); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodCreatesNewRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestStub(); + $this->mockConnectionForModel($model, 'SQLite', [123]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void + { + $model = new EloquentBuilderCreateOrFirstTestStub(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $sql = 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'baz', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + // FIXME: duplicate conditions + ->with('select * from "table" where ("attr" = ?) and ("attr" = ?) limit 1', ['foo', 'foo'], false) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection() + ->expects('update') + ->with( + 'update "table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 123], + ) + ->andReturn(1); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + protected function mockConnectionForModel(Model $model, string $database, array $lastInsertIds = []): void + { + $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; + $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; + $grammar = new $grammarClass; + $processor = new $processorClass; + $connection = Mockery::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new Builder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + + $class = get_class($model); + $class::setConnectionResolver($resolver); + + $connection->shouldReceive('getPdo')->andReturn($pdo = Mockery::mock(PDO::class)); + + foreach ($lastInsertIds as $id) { + $pdo->expects('lastInsertId')->andReturn($id); + } + } +} + +class EloquentBuilderCreateOrFirstTestStub extends Model +{ + protected $table = 'table'; + protected $guarded = []; +} diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index d9426adf67bc..470214f20a5c 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -4,7 +4,6 @@ use BadMethodCallException; use Closure; -use Exception; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Eloquent\Builder; @@ -17,7 +16,6 @@ use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Database\Query\Grammars\Grammar; use Illuminate\Database\Query\Processors\Processor; -use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Carbon; use Illuminate\Support\Collection as BaseCollection; use Mockery as m; @@ -2137,297 +2135,6 @@ public function testUpdateWithAliasWithQualifiedTimestampValue() Carbon::setTestNow(null); } - public function testCreateOrFirstMethodCreatesNewRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentBuilderTestStubStringPrimaryKey(); - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection()->expects('insert')->with( - 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', - ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], - )->andReturnTrue(); - - $result = $model->newQuery()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testCreateOrFirstMethodRetrievesExistingRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentBuilderTestStubStringPrimaryKey(); - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $sql = 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; - $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; - - $model->getConnection() - ->expects('insert') - ->with($sql, $bindings) - ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); - - $model->getConnection() - ->expects('select') - ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], false) - ->andReturn([[ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $result = $model->newQuery()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testFirstOrCreateMethodRetrievesExistingRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentBuilderTestStubStringPrimaryKey(); - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) - ->andReturn([[ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testFirstOrCreateMethodCreatesNewRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentBuilderTestStubStringPrimaryKey(); - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) - ->andReturn([]); - - $model->getConnection()->expects('insert')->with( - 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', - ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], - )->andReturnTrue(); - - $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentBuilderTestStubStringPrimaryKey(); - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) - ->andReturn([]); - - $sql = 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; - $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; - - $model->getConnection() - ->expects('insert') - ->with($sql, $bindings) - ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); - - $model->getConnection() - ->expects('select') - // FIXME: duplicate conditions - ->with('select * from "foo_table" where ("attr" = ?) and ("attr" = ?) limit 1', ['foo', 'foo'], false) - ->andReturn([[ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testUpdateOrCreateMethodUpdatesExistingRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentBuilderTestStubStringPrimaryKey(); - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) - ->andReturn([[ - 'id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $model->getConnection() - ->expects('update') - ->with( - 'update "foo_table" set "val" = ?, "updated_at" = ? where "id" = ?', - ['baz', '2023-01-01 00:00:00', '123'], - ) - ->andReturn(1); - - $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); - $this->assertEquals([ - 'id' => '123', - 'attr' => 'foo', - 'val' => 'baz', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testUpdateOrCreateMethodCreatesNewRecord() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentBuilderTestStubStringPrimaryKey(); - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) - ->andReturn([]); - - $model->getConnection()->expects('insert')->with( - 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', - ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], - )->andReturnTrue(); - - $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'bar']); - $this->assertEquals([ - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - - public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow() - { - Carbon::setTestNow('2023-01-01 00:00:00'); - - Model::unguarded(function () { - $model = new EloquentBuilderTestStubStringPrimaryKey(); - $this->mockConnectionForModel($model, 'SQLite'); - $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); - $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); - - $model->getConnection() - ->expects('select') - ->with('select * from "foo_table" where ("attr" = ?) limit 1', ['foo'], true) - ->andReturn([]); - - $sql = 'insert into "foo_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; - $bindings = ['foo', 'baz', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; - - $model->getConnection() - ->expects('insert') - ->with($sql, $bindings) - ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); - - $model->getConnection() - ->expects('select') - // FIXME: duplicate conditions - ->with('select * from "foo_table" where ("attr" = ?) and ("attr" = ?) limit 1', ['foo', 'foo'], false) - ->andReturn([[ - 'id' => '123', - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01 00:00:00', - 'updated_at' => '2023-01-01 00:00:00', - ]]); - - $model->getConnection() - ->expects('update') - ->with( - 'update "foo_table" set "val" = ?, "updated_at" = ? where "id" = ?', - ['baz', '2023-01-01 00:00:00', '123'], - ) - ->andReturn(1); - - $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); - $this->assertEquals([ - 'id' => '123', - 'attr' => 'foo', - 'val' => 'baz', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ], $result->toArray()); - }); - } - public function testUpsert() { Carbon::setTestNow($now = '2017-10-10 10:10:10'); From 4cefa9c9bf4bea1667f3b34514edcb6812b3937b Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 17:28:18 +0900 Subject: [PATCH 22/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20refactoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...EloquentBelongsToManyCreateOrFirstTest.php | 34 +++++++++---------- ...tabaseEloquentBuilderCreateOrFirstTest.php | 20 +++++------ ...tabaseEloquentHasManyCreateOrFirstTest.php | 24 ++++++------- ...loquentHasManyThroughCreateOrFirstTest.php | 28 +++++++-------- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php index 5f5a45362bb8..ec55a0bf908a 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php @@ -21,21 +21,21 @@ class DatabaseEloquentBelongsToManyCreateOrFirstTest extends TestCase { public function setUp(): void { - parent::setUp(); Carbon::setTestNow('2023-01-01 00:00:00'); } protected function tearDown(): void { + Carbon::setTestNow(); Mockery::close(); } public function testCreateOrFirstMethodCreatesNewRelated(): void { - $source = new BelongsToManyCreateOrFirstSource(); + $source = new BelongsToManyCreateOrFirstTestSourceModel(); $source->id = 123; $this->mockConnectionForModels( - [$source, new BelongsToManyCreateOrFirstRelated()], + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], 'SQLite', [456], ); @@ -65,10 +65,10 @@ public function testCreateOrFirstMethodCreatesNewRelated(): void public function testCreateOrFirstMethodAssociatesExistingRelated(): void { - $source = new BelongsToManyCreateOrFirstSource(); + $source = new BelongsToManyCreateOrFirstTestSourceModel(); $source->id = 123; $this->mockConnectionForModels( - [$source, new BelongsToManyCreateOrFirstRelated()], + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], 'SQLite', ); $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -112,10 +112,10 @@ public function testCreateOrFirstMethodAssociatesExistingRelated(): void public function testFirstOrCreateMethodRetrievesExistingRelatedAlreadyAssociated(): void { - $source = new BelongsToManyCreateOrFirstSource(); + $source = new BelongsToManyCreateOrFirstTestSourceModel(); $source->id = 123; $this->mockConnectionForModels( - [$source, new BelongsToManyCreateOrFirstRelated()], + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], 'SQLite', ); $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -155,10 +155,10 @@ public function testFirstOrCreateMethodRetrievesExistingRelatedAlreadyAssociated public function testCreateOrFirstMethodRetrievesExistingRelatedAssociatedJustNow(): void { - $source = new BelongsToManyCreateOrFirstSource(); + $source = new BelongsToManyCreateOrFirstTestSourceModel(); $source->id = 123; $this->mockConnectionForModels( - [$source, new BelongsToManyCreateOrFirstRelated()], + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], 'SQLite', ); $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -225,10 +225,10 @@ public function testCreateOrFirstMethodRetrievesExistingRelatedAssociatedJustNow public function testFirstOrCreateMethodRetrievesExistingRelatedAndAssociatesIt(): void { - $source = new BelongsToManyCreateOrFirstSource(); + $source = new BelongsToManyCreateOrFirstTestSourceModel(); $source->id = 123; $this->mockConnectionForModels( - [$source, new BelongsToManyCreateOrFirstRelated()], + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], 'SQLite', ); $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -280,7 +280,7 @@ public function testFirstOrCreateMethodRetrievesExistingRelatedAndAssociatesIt() public function testFirstOrCreateMethodFallsBackToCreateOrFirst(): void { - $source = new class() extends BelongsToManyCreateOrFirstSource + $source = new class() extends BelongsToManyCreateOrFirstTestSourceModel { protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null): BelongsToMany { @@ -289,7 +289,7 @@ protected function newBelongsToMany(Builder $query, Model $parent, $table, $fore $relation ->expects('createOrFirst') ->with(['attr' => 'foo'], ['val' => 'bar'], [], true) - ->andReturn(new BelongsToManyCreateOrFirstRelated([ + ->andReturn(new BelongsToManyCreateOrFirstTestRelatedModel([ 'id' => 456, 'attr' => 'foo', 'val' => 'bar', @@ -302,7 +302,7 @@ protected function newBelongsToMany(Builder $query, Model $parent, $table, $fore }; $source->id = 123; $this->mockConnectionForModels( - [$source, new BelongsToManyCreateOrFirstRelated()], + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], 'SQLite', ); $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -366,7 +366,7 @@ protected function mockConnectionForModels(array $models, string $database, arra /** * @property int $id */ -class BelongsToManyCreateOrFirstRelated extends Model +class BelongsToManyCreateOrFirstTestRelatedModel extends Model { protected $table = 'related_table'; protected $guarded = []; @@ -375,7 +375,7 @@ class BelongsToManyCreateOrFirstRelated extends Model /** * @property int $id */ -class BelongsToManyCreateOrFirstSource extends Model +class BelongsToManyCreateOrFirstTestSourceModel extends Model { protected $table = 'source_table'; protected $guarded = []; @@ -383,7 +383,7 @@ class BelongsToManyCreateOrFirstSource extends Model public function related(): BelongsToMany { return $this->belongsToMany( - BelongsToManyCreateOrFirstRelated::class, + BelongsToManyCreateOrFirstTestRelatedModel::class, 'pivot_table', 'source_id', 'related_id', diff --git a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php index 991b49a1c4e6..30ebf57e9b66 100755 --- a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php @@ -17,18 +17,18 @@ class DatabaseEloquentBuilderCreateOrFirstTest extends TestCase { public function setUp(): void { - parent::setUp(); Carbon::setTestNow('2023-01-01 00:00:00'); } protected function tearDown(): void { + Carbon::setTestNow(); Mockery::close(); } public function testCreateOrFirstMethodCreatesNewRecord(): void { - $model = new EloquentBuilderCreateOrFirstTestStub(); + $model = new EloquentBuilderCreateOrFirstTestModel(); $this->mockConnectionForModel($model, 'SQLite', [123]); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); @@ -51,7 +51,7 @@ public function testCreateOrFirstMethodCreatesNewRecord(): void public function testCreateOrFirstMethodRetrievesExistingRecord(): void { - $model = new EloquentBuilderCreateOrFirstTestStub(); + $model = new EloquentBuilderCreateOrFirstTestModel(); $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); @@ -88,7 +88,7 @@ public function testCreateOrFirstMethodRetrievesExistingRecord(): void public function testFirstOrCreateMethodRetrievesExistingRecord(): void { - $model = new EloquentBuilderCreateOrFirstTestStub(); + $model = new EloquentBuilderCreateOrFirstTestModel(); $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); @@ -117,7 +117,7 @@ public function testFirstOrCreateMethodRetrievesExistingRecord(): void public function testFirstOrCreateMethodCreatesNewRecord(): void { - $model = new EloquentBuilderCreateOrFirstTestStub(); + $model = new EloquentBuilderCreateOrFirstTestModel(); $this->mockConnectionForModel($model, 'SQLite', [123]); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); @@ -145,7 +145,7 @@ public function testFirstOrCreateMethodCreatesNewRecord(): void public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void { - $model = new EloquentBuilderCreateOrFirstTestStub(); + $model = new EloquentBuilderCreateOrFirstTestModel(); $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); @@ -188,7 +188,7 @@ public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void public function testUpdateOrCreateMethodUpdatesExistingRecord(): void { - $model = new EloquentBuilderCreateOrFirstTestStub(); + $model = new EloquentBuilderCreateOrFirstTestModel(); $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); @@ -225,7 +225,7 @@ public function testUpdateOrCreateMethodUpdatesExistingRecord(): void public function testUpdateOrCreateMethodCreatesNewRecord(): void { - $model = new EloquentBuilderCreateOrFirstTestStub(); + $model = new EloquentBuilderCreateOrFirstTestModel(); $this->mockConnectionForModel($model, 'SQLite', [123]); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); @@ -253,7 +253,7 @@ public function testUpdateOrCreateMethodCreatesNewRecord(): void public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void { - $model = new EloquentBuilderCreateOrFirstTestStub(); + $model = new EloquentBuilderCreateOrFirstTestModel(); $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); @@ -326,7 +326,7 @@ protected function mockConnectionForModel(Model $model, string $database, array } } -class EloquentBuilderCreateOrFirstTestStub extends Model +class EloquentBuilderCreateOrFirstTestModel extends Model { protected $table = 'table'; protected $guarded = []; diff --git a/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php index d4bf27a30023..e3b12fa52f31 100755 --- a/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php @@ -18,18 +18,18 @@ class DatabaseEloquentHasManyCreateOrFirstTest extends TestCase { public function setUp(): void { - parent::setUp(); Carbon::setTestNow('2023-01-01 00:00:00'); } protected function tearDown(): void { + Carbon::setTestNow(); Mockery::close(); } public function testCreateOrFirstMethodCreatesNewRecord(): void { - $model = new HasManyCreateOrFirstParent(); + $model = new HasManyCreateOrFirstTestParentModel(); $model->id = 123; $this->mockConnectionForModel($model, 'SQLite', [456]); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -54,7 +54,7 @@ public function testCreateOrFirstMethodCreatesNewRecord(): void public function testCreateOrFirstMethodRetrievesExistingRecord(): void { - $model = new HasManyCreateOrFirstParent(); + $model = new HasManyCreateOrFirstTestParentModel(); $model->id = 123; $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -94,7 +94,7 @@ public function testCreateOrFirstMethodRetrievesExistingRecord(): void public function testFirstOrCreateMethodCreatesNewRecord(): void { - $model = new HasManyCreateOrFirstParent(); + $model = new HasManyCreateOrFirstTestParentModel(); $model->id = 123; $this->mockConnectionForModel($model, 'SQLite', [456]); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -124,7 +124,7 @@ public function testFirstOrCreateMethodCreatesNewRecord(): void public function testFirstOrCreateMethodRetrievesExistingRecord(): void { - $model = new HasManyCreateOrFirstParent(); + $model = new HasManyCreateOrFirstTestParentModel(); $model->id = 123; $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -156,7 +156,7 @@ public function testFirstOrCreateMethodRetrievesExistingRecord(): void public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void { - $model = new HasManyCreateOrFirstParent(); + $model = new HasManyCreateOrFirstTestParentModel(); $model->id = 123; $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -202,7 +202,7 @@ public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void public function testUpdateOrCreateMethodCreatesNewRecord(): void { - $model = new HasManyCreateOrFirstParent(); + $model = new HasManyCreateOrFirstTestParentModel(); $model->id = 123; $this->mockConnectionForModel($model, 'SQLite', [456]); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -232,7 +232,7 @@ public function testUpdateOrCreateMethodCreatesNewRecord(): void public function testUpdateOrCreateMethodUpdatesExistingRecord(): void { - $model = new HasManyCreateOrFirstParent(); + $model = new HasManyCreateOrFirstTestParentModel(); $model->id = 123; $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -269,7 +269,7 @@ public function testUpdateOrCreateMethodUpdatesExistingRecord(): void public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void { - $model = new HasManyCreateOrFirstParent(); + $model = new HasManyCreateOrFirstTestParentModel(); $model->id = 123; $this->mockConnectionForModel($model, 'SQLite'); $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -345,14 +345,14 @@ protected function mockConnectionForModel(Model $model, string $database, array /** * @property int $id */ -class HasManyCreateOrFirstParent extends Model +class HasManyCreateOrFirstTestParentModel extends Model { protected $table = 'parent_table'; protected $guarded = []; public function children(): HasMany { - return $this->hasMany(HasManyCreateOrFirstChild::class, 'parent_id'); + return $this->hasMany(HasManyCreateOrFirstTestChildModel::class, 'parent_id'); } } @@ -360,7 +360,7 @@ public function children(): HasMany * @property int $id * @property int $parent_id */ -class HasManyCreateOrFirstChild extends Model +class HasManyCreateOrFirstTestChildModel extends Model { protected $table = 'child_table'; protected $guarded = []; diff --git a/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php b/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php index 520abee94865..dac5d821d56a 100644 --- a/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php @@ -20,18 +20,18 @@ class DatabaseEloquentHasManyThroughCreateOrFirstTest extends TestCase { public function setUp(): void { - parent::setUp(); Carbon::setTestNow('2023-01-01 00:00:00'); } protected function tearDown(): void { + Carbon::setTestNow(); Mockery::close(); } public function testCreateOrFirstMethodCreatesNewRecord(): void { - $parent = new HasManyThroughCreateOrFirstParent(); + $parent = new HasManyThroughCreateOrFirstTestParentModel(); $parent->id = 123; $this->mockConnectionForModel($parent, 'SQLite', [789]); $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -54,7 +54,7 @@ public function testCreateOrFirstMethodCreatesNewRecord(): void public function testCreateOrFirstMethodRetrievesExistingRecord(): void { - $parent = new HasManyThroughCreateOrFirstParent(); + $parent = new HasManyThroughCreateOrFirstTestParentModel(); $parent->id = 123; $this->mockConnectionForModel($parent, 'SQLite'); $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -100,7 +100,7 @@ public function testCreateOrFirstMethodRetrievesExistingRecord(): void public function testFirstOrCreateMethodCreatesNewRecord(): void { - $parent = new HasManyThroughCreateOrFirstParent(); + $parent = new HasManyThroughCreateOrFirstTestParentModel(); $parent->id = 123; $this->mockConnectionForModel($parent, 'SQLite', [789]); $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -133,7 +133,7 @@ public function testFirstOrCreateMethodCreatesNewRecord(): void public function testFirstOrCreateMethodRetrievesExistingRecord(): void { - $parent = new HasManyThroughCreateOrFirstParent(); + $parent = new HasManyThroughCreateOrFirstTestParentModel(); $parent->id = 123; $this->mockConnectionForModel($parent, 'SQLite'); $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -171,7 +171,7 @@ public function testFirstOrCreateMethodRetrievesExistingRecord(): void public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void { - $parent = new HasManyThroughCreateOrFirstParent(); + $parent = new HasManyThroughCreateOrFirstTestParentModel(); $parent->id = 123; $this->mockConnectionForModel($parent, 'SQLite'); $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -227,7 +227,7 @@ public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void public function testUpdateOrCreateMethodCreatesNewRecord(): void { - $parent = new HasManyThroughCreateOrFirstParent(); + $parent = new HasManyThroughCreateOrFirstTestParentModel(); $parent->id = 123; $this->mockConnectionForModel($parent, 'SQLite', [789]); $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -263,7 +263,7 @@ public function testUpdateOrCreateMethodCreatesNewRecord(): void public function testUpdateOrCreateMethodUpdatesExistingRecord(): void { - $parent = new HasManyThroughCreateOrFirstParent(); + $parent = new HasManyThroughCreateOrFirstTestParentModel(); $parent->id = 123; $this->mockConnectionForModel($parent, 'SQLite'); $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -309,7 +309,7 @@ public function testUpdateOrCreateMethodUpdatesExistingRecord(): void public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void { - $parent = new HasManyThroughCreateOrFirstParent(); + $parent = new HasManyThroughCreateOrFirstTestParentModel(); $parent->id = 123; $this->mockConnectionForModel($parent, 'SQLite'); $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); @@ -391,7 +391,7 @@ protected function mockConnectionForModel(Model $model, string $database, array * @property int $id * @property int $pivot_id */ -class HasManyThroughCreateOrFirstChild extends Model +class HasManyThroughCreateOrFirstTestChildModel extends Model { protected $table = 'child'; protected $guarded = []; @@ -401,7 +401,7 @@ class HasManyThroughCreateOrFirstChild extends Model * @property int $id * @property int $parent_id */ -class HasManyThroughCreateOrFirstPivot extends Model +class HasManyThroughCreateOrFirstTestPivotModel extends Model { protected $table = 'pivot'; protected $guarded = []; @@ -410,7 +410,7 @@ class HasManyThroughCreateOrFirstPivot extends Model /** * @property int $id */ -class HasManyThroughCreateOrFirstParent extends Model +class HasManyThroughCreateOrFirstTestParentModel extends Model { protected $table = 'parent'; protected $guarded = []; @@ -418,8 +418,8 @@ class HasManyThroughCreateOrFirstParent extends Model public function children(): HasManyThrough { return $this->hasManyThrough( - HasManyThroughCreateOrFirstChild::class, - HasManyThroughCreateOrFirstPivot::class, + HasManyThroughCreateOrFirstTestChildModel::class, + HasManyThroughCreateOrFirstTestPivotModel::class, 'parent_id', 'pivot_id', ); From fab4ee30b95e072a79ee37c5ee8a0327cff29e15 Mon Sep 17 00:00:00 2001 From: mpyw Date: Fri, 6 Oct 2023 17:53:02 +0900 Subject: [PATCH 23/23] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20`BelongsToMa?= =?UTF-8?q?ny::updateOrCreate`=20snapshot=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...EloquentBelongsToManyCreateOrFirstTest.php | 120 +++++++++++++++++- 1 file changed, 113 insertions(+), 7 deletions(-) diff --git a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php index ec55a0bf908a..047dc138a5e9 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php @@ -286,16 +286,24 @@ protected function newBelongsToMany(Builder $query, Model $parent, $table, $fore { $relation = Mockery::mock(BelongsToMany::class)->makePartial(); $relation->__construct(...func_get_args()); + $instance = new BelongsToManyCreateOrFirstTestRelatedModel([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + 'pivot' => [ + 'source_id' => 123, + 'related_id' => 456, + ], + ]); + $instance->exists = true; + $instance->wasRecentlyCreated = false; + $instance->syncOriginal(); $relation ->expects('createOrFirst') ->with(['attr' => 'foo'], ['val' => 'bar'], [], true) - ->andReturn(new BelongsToManyCreateOrFirstTestRelatedModel([ - 'id' => 456, - 'attr' => 'foo', - 'val' => 'bar', - 'created_at' => '2023-01-01T00:00:00.000000Z', - 'updated_at' => '2023-01-01T00:00:00.000000Z', - ])); + ->andReturn($instance); return $relation; } @@ -333,6 +341,104 @@ protected function newBelongsToMany(Builder $query, Model $parent, $table, $fore 'val' => 'bar', 'created_at' => '2023-01-01T00:00:00.000000Z', 'updated_at' => '2023-01-01T00:00:00.000000Z', + 'pivot' => [ + 'source_id' => 123, + 'related_id' => 456, + ], + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodCreatesNewRelated(): void + { + $source = new class() extends BelongsToManyCreateOrFirstTestSourceModel + { + protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null): BelongsToMany + { + $relation = Mockery::mock(BelongsToMany::class)->makePartial(); + $relation->__construct(...func_get_args()); + $instance = new BelongsToManyCreateOrFirstTestRelatedModel([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]); + $instance->exists = true; + $instance->wasRecentlyCreated = true; + $instance->syncOriginal(); + $relation + ->expects('firstOrCreate') + ->with(['attr' => 'foo'], ['val' => 'baz'], [], true) + ->andReturn($instance); + + return $relation; + } + }; + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + + $result = $source->related()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesExistingRelated(): void + { + $source = new class() extends BelongsToManyCreateOrFirstTestSourceModel + { + protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null): BelongsToMany + { + $relation = Mockery::mock(BelongsToMany::class)->makePartial(); + $relation->__construct(...func_get_args()); + $instance = new BelongsToManyCreateOrFirstTestRelatedModel([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]); + $instance->exists = true; + $instance->wasRecentlyCreated = false; + $instance->syncOriginal(); + $relation + ->expects('firstOrCreate') + ->with(['attr' => 'foo'], ['val' => 'baz'], [], true) + ->andReturn($instance); + + return $relation; + } + }; + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection() + ->expects('update') + ->with( + 'update "related_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 456], + ) + ->andReturn(1); + + $result = $source->related()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', ], $result->toArray()); }