From 4c9ea8e55db29e9e37737b38e61b894574fd53ff Mon Sep 17 00:00:00 2001 From: joelharkes Date: Sun, 26 Mar 2023 17:16:31 +0200 Subject: [PATCH 1/6] Add hasMany and HasOne to join models --- src/Illuminate/Database/Eloquent/Builder.php | 10 +- .../Eloquent/Concerns/HasGlobalScopes.php | 2 +- .../Eloquent/Concerns/JoinsModels.php | 92 +++++++++++++++++++ src/Illuminate/Database/Eloquent/Model.php | 2 +- .../DatabaseEloquentJoinsModelsTest.php | 76 +++++++++++++++ 5 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php create mode 100644 tests/Database/DatabaseEloquentJoinsModelsTest.php diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 55223ed9a874..cdecc8b296ac 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Database\Eloquent\Builder as BuilderContract; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Concerns\BuildsQueries; +use Illuminate\Database\Eloquent\Concerns\JoinsModels; use Illuminate\Database\Eloquent\Concerns\QueriesRelationships; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Relation; @@ -29,7 +30,7 @@ */ class Builder implements BuilderContract { - use BuildsQueries, ForwardsCalls, QueriesRelationships { + use BuildsQueries, ForwardsCalls, QueriesRelationships, JoinsModels { BuildsQueries::sole as baseSole; } @@ -1290,6 +1291,13 @@ public function applyScopes() return $builder; } + /** + * @return array + */ + public function getScopes(): array { + return $this->scopes; + } + /** * Apply the given scope on the current builder instance. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php index 72afb178897b..f26265e68c41 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php @@ -62,7 +62,7 @@ public static function getGlobalScope($scope) /** * Get the global scopes for this class instance. * - * @return array + * @return array */ public function getGlobalScopes() { diff --git a/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php b/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php new file mode 100644 index 000000000000..3cb35176a346 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php @@ -0,0 +1,92 @@ +|Model|Builder $model + * @param string $joinType + * @param string|null $overrideJoinColumnName + * @return static + */ + public function joinMany($model, string $joinType = 'inner', ?string $overrideJoinColumnName = null): static { + $modelToJoin = is_string($model) ? new $model() : $model; + if($model instanceof Builder){ + $scopes = $model->getScopes(); + $modelToJoin = $model->getModel(); + } else { + $scopes = $modelToJoin->getGlobalScopes(); + } + + $this->joinManyOn($this->getModel(), $modelToJoin, $joinType,null, $overrideJoinColumnName); + $this->applyScopesWith($scopes, $modelToJoin); + + return $this; + } + + /** + * @param class-string|Model $model + * @param string $joinType + * @param string|null $overrideBaseColumn + * @return static + */ + public function joinOne($model, string $joinType = 'inner', ?string $overrideBaseColumn = null): static { + $modelToJoin = is_string($model) ? new $model() : $model; + if($model instanceof Builder){ + $scopes = $model->getScopes(); + $modelToJoin = $model->getModel(); + } else { + $scopes = $modelToJoin->getGlobalScopes(); + } + $this->joinOneOn($this->getModel(), $modelToJoin, $joinType, $overrideBaseColumn); + $this->applyScopesWith($scopes, $modelToJoin); + + return $this; + } + + + private function joinManyOn(Model $baseModel, Model $modelToJoin, ?string $joinType = 'inner', ?string $overrideBaseColumnName = null, ?string $overrideJoinColumnName = null): static + { + $manyJoinColumnName = $overrideJoinColumnName ?? (Str::singular($baseModel->getTable()). '_' . $baseModel->getKeyName()); + return $this->join( + $modelToJoin->getTable(), + $modelToJoin->qualifyColumn($manyJoinColumnName), + '=', + $baseModel->qualifyColumn($overrideBaseColumnName ?? $baseModel->getKeyName()), + $joinType + ); + } + + private function joinOneOn(Model $baseModel, Model $modelToJoin, string $joinType = 'inner', string $overrideBaseColumnName = null, string $overrideJoinColumnName = null): static + { + $manyJoinColumnName = $overrideBaseColumnName ?? (Str::singular($modelToJoin->getTable()). '_' . $modelToJoin->getKeyName()); + return $this->join( + $modelToJoin->getTable(), + $modelToJoin->qualifyColumn($overrideJoinColumnName ?? $modelToJoin->getKeyName()), + '=', + $baseModel->qualifyColumn($manyJoinColumnName), + $joinType + ); + } + + /** + * @param Scope $scopes + * @param Model $model + * @return static + */ + private function applyScopesWith(array $scopes, Model $model): static + { + foreach($scopes as $scope){ + $scope->apply($this, $model); + } + return $this; + } +} diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 3a5173117573..5bad9661c0bf 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -149,7 +149,7 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt /** * The array of global scopes on the model. * - * @var array + * @var array */ protected static $globalScopes = []; diff --git a/tests/Database/DatabaseEloquentJoinsModelsTest.php b/tests/Database/DatabaseEloquentJoinsModelsTest.php new file mode 100644 index 000000000000..a273ce72d114 --- /dev/null +++ b/tests/Database/DatabaseEloquentJoinsModelsTest.php @@ -0,0 +1,76 @@ +addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + + $db->bootEloquent(); + $db->setAsGlobal(); + $this->db = $db; + } + protected function tearDown(): void + { + \Mockery::close(); + } + public function testJoinMany(){ + $mock = \Mockery::mock(Builder::class, [$this->db->getDatabaseManager()->query()])->makePartial(); + $mock->shouldReceive('join')->withArgs(['comments', 'comments.blog_id', '=', 'blogs.id', 'inner'])->andReturn($mock)->once(); + $query =$mock->setModel(new Blog()); + $query->joinMany(Comment::class); + \Mockery::close(); + } + + public function testJoinOne(){ + $mock = \Mockery::mock(Builder::class, [$this->db->getDatabaseManager()->query()])->makePartial(); + $mock->shouldReceive('join')->withArgs(['blogs', 'blogs.id', '=', 'comments.blog_id', 'inner'])->andReturn($mock)->once(); + $query = $mock->setModel(new Comment()); + $query->joinOne(Blog::class); + \Mockery::close(); + } + + public function testRunsScopes(){ + $blog = new Blog(); + $query = $blog->newQuery()->joinMany(DeletableComment::class)->toSql(); + $this->assertSame('select * from "blogs" inner join "deletable_comments" on "deletable_comments"."blog_id" = "blogs"."id" where "deletable_comments"."deleted_at" is null', $query); + } + + public function testCanJoinBuilder(){ + $blog = new Blog(); + $query = $blog->newQuery()->joinMany(DeletableComment::withTrashed())->toSql(); + $this->assertSame('select * from "blogs" inner join "deletable_comments" on "deletable_comments"."blog_id" = "blogs"."id"', $query); + } + + public function testAddWhereStatements(){ + $blog = new Blog(); + $query = $blog->newQuery()->joinMany(Comment::query()->whereNull('deleted_at'))->toSql(); + $this->assertSame('select * from "blogs" inner join "comments" on "comments"."blog_id" = "blogs"."id" where "comments"."deleted_at" is null', $query); + } +} + + +class Blog extends Model {} +class Comment extends Model {} + + +class DeletableComment extends Model { + use SoftDeletes; +} From bc7f5c45d3d5982d8a8e1ab14ba637ebc9eadf2d Mon Sep 17 00:00:00 2001 From: joelharkes Date: Sun, 26 Mar 2023 19:39:03 +0200 Subject: [PATCH 2/6] Fix sql grammar for nested joins --- src/Illuminate/Database/Query/Grammars/Grammar.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index e15c55644070..67fdd15d339e 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -504,9 +504,10 @@ protected function whereNested(Builder $query, $where) // Here we will calculate what portion of the string we need to remove. If this // is a join clause query, we need to remove the "on" portion of the SQL and // if it is a normal query we need to take the leading "where" of queries. - $offset = $query instanceof JoinClause ? 3 : 6; + $whereSql = $this->compileWheres($where['query']); + $offset = $query instanceof JoinClause && str_starts_with($whereSql, 'on ') ? 3 : 6; - return '('.substr($this->compileWheres($where['query']), $offset).')'; + return '('.substr($whereSql, $offset).')'; } /** From 1c219ca66eeea1a4228a75b2fc0dc373f5458408 Mon Sep 17 00:00:00 2001 From: joelharkes Date: Sun, 26 Mar 2023 19:39:43 +0200 Subject: [PATCH 3/6] Fix proper scoping in join statements to make left joins work properly --- .../Eloquent/Concerns/JoinsModels.php | 82 +++++++++++-------- .../DatabaseEloquentJoinsModelsTest.php | 6 +- 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php b/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php index 3cb35176a346..af5b9b80303f 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php +++ b/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php @@ -6,7 +6,9 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Scope; +use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Str; trait JoinsModels @@ -18,18 +20,15 @@ trait JoinsModels * @return static */ public function joinMany($model, string $joinType = 'inner', ?string $overrideJoinColumnName = null): static { - $modelToJoin = is_string($model) ? new $model() : $model; - if($model instanceof Builder){ - $scopes = $model->getScopes(); - $modelToJoin = $model->getModel(); - } else { - $scopes = $modelToJoin->getGlobalScopes(); - } - - $this->joinManyOn($this->getModel(), $modelToJoin, $joinType,null, $overrideJoinColumnName); - $this->applyScopesWith($scopes, $modelToJoin); + /** @var Builder $builder */ + $builder = match(true) { + is_string($model) => (new $model())->newQuery(), + $model instanceof Builder => $model, + $model instanceof Model => $model->newQuery(), + $model instanceof Relation => $model->getQuery(), + }; - return $this; + return $this->joinManyOn($this->getModel(), $builder, $joinType,null, $overrideJoinColumnName); } /** @@ -39,46 +38,57 @@ public function joinMany($model, string $joinType = 'inner', ?string $overrideJo * @return static */ public function joinOne($model, string $joinType = 'inner', ?string $overrideBaseColumn = null): static { - $modelToJoin = is_string($model) ? new $model() : $model; - if($model instanceof Builder){ - $scopes = $model->getScopes(); - $modelToJoin = $model->getModel(); - } else { - $scopes = $modelToJoin->getGlobalScopes(); - } - $this->joinOneOn($this->getModel(), $modelToJoin, $joinType, $overrideBaseColumn); - $this->applyScopesWith($scopes, $modelToJoin); + $builder = match(true) { + is_string($model) => (new $model())->newQuery(), + $model instanceof Builder => $model, + $model instanceof Model => $model->newQuery(), + $model instanceof Relation => $model->getQuery(), + }; + + $this->joinOneOn($this->getModel(), $builder, $joinType, $overrideBaseColumn); return $this; } - private function joinManyOn(Model $baseModel, Model $modelToJoin, ?string $joinType = 'inner', ?string $overrideBaseColumnName = null, ?string $overrideJoinColumnName = null): static + private function joinManyOn(Model $baseModel, Builder $builderToJoin, ?string $joinType = 'inner', ?string $overrideBaseColumnName = null, ?string $overrideJoinColumnName = null): static { + $modelToJoin = $builderToJoin->getModel(); $manyJoinColumnName = $overrideJoinColumnName ?? (Str::singular($baseModel->getTable()). '_' . $baseModel->getKeyName()); - return $this->join( - $modelToJoin->getTable(), - $modelToJoin->qualifyColumn($manyJoinColumnName), - '=', - $baseModel->qualifyColumn($overrideBaseColumnName ?? $baseModel->getKeyName()), - $joinType + $baseColumnName = $overrideBaseColumnName ?? $baseModel->getKeyName(); + $this->join( + $modelToJoin->getTable(), fn(JoinClause $join) => + $join->on( + $modelToJoin->qualifyColumn($manyJoinColumnName), + '=', + $baseModel->qualifyColumn($baseColumnName), + )->addNestedWhereQuery($builderToJoin->applyScopes()->getQuery()), + type: $joinType ); + + return $this; } - private function joinOneOn(Model $baseModel, Model $modelToJoin, string $joinType = 'inner', string $overrideBaseColumnName = null, string $overrideJoinColumnName = null): static + private function joinOneOn(Model $baseModel, Builder $builderToJoin, string $joinType = 'inner', string $overrideBaseColumnName = null, string $overrideJoinColumnName = null): static { - $manyJoinColumnName = $overrideBaseColumnName ?? (Str::singular($modelToJoin->getTable()). '_' . $modelToJoin->getKeyName()); - return $this->join( - $modelToJoin->getTable(), - $modelToJoin->qualifyColumn($overrideJoinColumnName ?? $modelToJoin->getKeyName()), - '=', - $baseModel->qualifyColumn($manyJoinColumnName), - $joinType + $modelToJoin = $builderToJoin->getModel(); + $joinColumnName = $overrideBaseColumnName ?? $modelToJoin->getKeyName(); + $baseColumnName = $overrideJoinColumnName ?? (Str::singular($baseModel->getTable()). '_' . $baseModel->getKeyName()); + $this->join( + $modelToJoin->getTable(), fn(JoinClause $join) => + $join->on( + $modelToJoin->qualifyColumn($joinColumnName), + '=', + $baseModel->qualifyColumn($baseColumnName), + )->addNestedWhereQuery($builderToJoin->getQuery()), + type: $joinType ); + $this->applyScopesWith($builderToJoin->getScopes(), $modelToJoin); + return $this; } /** - * @param Scope $scopes + * @param Scope[] $scopes * @param Model $model * @return static */ diff --git a/tests/Database/DatabaseEloquentJoinsModelsTest.php b/tests/Database/DatabaseEloquentJoinsModelsTest.php index a273ce72d114..8cf4a27ab2d2 100644 --- a/tests/Database/DatabaseEloquentJoinsModelsTest.php +++ b/tests/Database/DatabaseEloquentJoinsModelsTest.php @@ -50,7 +50,7 @@ public function testJoinOne(){ public function testRunsScopes(){ $blog = new Blog(); $query = $blog->newQuery()->joinMany(DeletableComment::class)->toSql(); - $this->assertSame('select * from "blogs" inner join "deletable_comments" on "deletable_comments"."blog_id" = "blogs"."id" where "deletable_comments"."deleted_at" is null', $query); + $this->assertSame('select * from "blogs" inner join "deletable_comments" on "deletable_comments"."blog_id" = "blogs"."id" and ("deletable_comments"."deleted_at" is null)', $query); } public function testCanJoinBuilder(){ @@ -61,8 +61,8 @@ public function testCanJoinBuilder(){ public function testAddWhereStatements(){ $blog = new Blog(); - $query = $blog->newQuery()->joinMany(Comment::query()->whereNull('deleted_at'))->toSql(); - $this->assertSame('select * from "blogs" inner join "comments" on "comments"."blog_id" = "blogs"."id" where "comments"."deleted_at" is null', $query); + $query = $blog->newQuery()->joinMany(Comment::query()->whereNull('comments.deleted_at'))->toSql(); + $this->assertSame('select * from "blogs" inner join "comments" on "comments"."blog_id" = "blogs"."id" and ("comments"."deleted_at" is null)', $query); } } From 7832ce304b20aff3723866e1e6038ea65507d5ba Mon Sep 17 00:00:00 2001 From: joelharkes Date: Sun, 26 Mar 2023 19:52:32 +0200 Subject: [PATCH 4/6] Add test cases for primary key name --- .../Eloquent/Concerns/JoinsModels.php | 2 +- .../DatabaseEloquentJoinsModelsTest.php | 44 ++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php b/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php index af5b9b80303f..61a8ec692079 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php +++ b/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php @@ -73,7 +73,7 @@ private function joinOneOn(Model $baseModel, Builder $builderToJoin, string $joi { $modelToJoin = $builderToJoin->getModel(); $joinColumnName = $overrideBaseColumnName ?? $modelToJoin->getKeyName(); - $baseColumnName = $overrideJoinColumnName ?? (Str::singular($baseModel->getTable()). '_' . $baseModel->getKeyName()); + $baseColumnName = $overrideJoinColumnName ?? (Str::singular($modelToJoin->getTable()). '_' . $modelToJoin->getKeyName()); $this->join( $modelToJoin->getTable(), fn(JoinClause $join) => $join->on( diff --git a/tests/Database/DatabaseEloquentJoinsModelsTest.php b/tests/Database/DatabaseEloquentJoinsModelsTest.php index 8cf4a27ab2d2..dc6ba02a9630 100644 --- a/tests/Database/DatabaseEloquentJoinsModelsTest.php +++ b/tests/Database/DatabaseEloquentJoinsModelsTest.php @@ -31,35 +31,60 @@ protected function tearDown(): void { \Mockery::close(); } - public function testJoinMany(){ + public function testJoinMany() + { $mock = \Mockery::mock(Builder::class, [$this->db->getDatabaseManager()->query()])->makePartial(); - $mock->shouldReceive('join')->withArgs(['comments', 'comments.blog_id', '=', 'blogs.id', 'inner'])->andReturn($mock)->once(); + $mock->shouldReceive('join')->withSomeOfArgs('comments')->andReturn($mock)->once(); $query =$mock->setModel(new Blog()); $query->joinMany(Comment::class); \Mockery::close(); } - public function testJoinOne(){ + public function testJoinOne() + { $mock = \Mockery::mock(Builder::class, [$this->db->getDatabaseManager()->query()])->makePartial(); - $mock->shouldReceive('join')->withArgs(['blogs', 'blogs.id', '=', 'comments.blog_id', 'inner'])->andReturn($mock)->once(); + $mock->shouldReceive('join')->withSomeOfArgs('blogs')->andReturn($mock)->once(); $query = $mock->setModel(new Comment()); $query->joinOne(Blog::class); \Mockery::close(); } - public function testRunsScopes(){ + public function testSimpleHasMany() + { + $blog = new Blog(); + $query = $blog->newQuery()->joinMany(Comment::class)->toSql(); + $this->assertSame('select * from "blogs" inner join "comments" on "comments"."blog_id" = "blogs"."id"', $query); + } + + public function testSimpleHasOne() + { + $query = (new Comment())->newQuery()->joinOne(Blog::class)->toSql(); + $this->assertSame('select * from "comments" inner join "blogs" on "blogs"."id" = "comments"."blog_id"', $query); + } + + public function testSimpleHasManyAlternativePrimaryKeyName() + { + $blog = new Alternative(); + $query = $blog->newQuery()->joinMany(Blog::class)->toSql(); + $this->assertSame('select * from "alternatives" inner join "blogs" on "blogs"."alternative_key" = "alternatives"."key"', $query); + } + + public function testIncludeScopesInJoin() + { $blog = new Blog(); $query = $blog->newQuery()->joinMany(DeletableComment::class)->toSql(); $this->assertSame('select * from "blogs" inner join "deletable_comments" on "deletable_comments"."blog_id" = "blogs"."id" and ("deletable_comments"."deleted_at" is null)', $query); } - public function testCanJoinBuilder(){ + public function testCanJoinBuilder() + { $blog = new Blog(); $query = $blog->newQuery()->joinMany(DeletableComment::withTrashed())->toSql(); $this->assertSame('select * from "blogs" inner join "deletable_comments" on "deletable_comments"."blog_id" = "blogs"."id"', $query); } - public function testAddWhereStatements(){ + public function testAddWhereStatements() + { $blog = new Blog(); $query = $blog->newQuery()->joinMany(Comment::query()->whereNull('comments.deleted_at'))->toSql(); $this->assertSame('select * from "blogs" inner join "comments" on "comments"."blog_id" = "blogs"."id" and ("comments"."deleted_at" is null)', $query); @@ -74,3 +99,8 @@ class Comment extends Model {} class DeletableComment extends Model { use SoftDeletes; } + +class Alternative extends Model +{ + protected $primaryKey = 'key'; +} From ee5f8cbd89e78936fc8efb7a32f051e6580b0ef2 Mon Sep 17 00:00:00 2001 From: joelharkes Date: Sun, 26 Mar 2023 20:12:11 +0200 Subject: [PATCH 5/6] Fix docs --- src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php b/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php index 61a8ec692079..f0f167efba19 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php +++ b/src/Illuminate/Database/Eloquent/Concerns/JoinsModels.php @@ -32,7 +32,7 @@ public function joinMany($model, string $joinType = 'inner', ?string $overrideJo } /** - * @param class-string|Model $model + * @param class-string|Model|Builder $model * @param string $joinType * @param string|null $overrideBaseColumn * @return static From 80e5ff927c647a353897797452ba56ac8b506729 Mon Sep 17 00:00:00 2001 From: joelharkes Date: Mon, 27 Mar 2023 08:44:16 +0200 Subject: [PATCH 6/6] Add test for relationship --- .../Database/DatabaseEloquentJoinsModelsTest.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/Database/DatabaseEloquentJoinsModelsTest.php b/tests/Database/DatabaseEloquentJoinsModelsTest.php index dc6ba02a9630..23ffbfa63b4a 100644 --- a/tests/Database/DatabaseEloquentJoinsModelsTest.php +++ b/tests/Database/DatabaseEloquentJoinsModelsTest.php @@ -89,12 +89,23 @@ public function testAddWhereStatements() $query = $blog->newQuery()->joinMany(Comment::query()->whereNull('comments.deleted_at'))->toSql(); $this->assertSame('select * from "blogs" inner join "comments" on "comments"."blog_id" = "blogs"."id" and ("comments"."deleted_at" is null)', $query); } + + public function testAddingOnRelation() + { + $blog = new Blog(); + $query = $blog->comments()->joinOne(User::class)->toSql(); + $this->assertSame('select * from "comments" inner join "users" on "users"."id" = "comments"."user_id" where "comments"."blog_id" is null and "comments"."blog_id" is not null', $query); + } } -class Blog extends Model {} +class Blog extends Model { + public function comments(){ + return $this->hasMany(Comment::class); + } +} class Comment extends Model {} - +class User extends Model {} class DeletableComment extends Model { use SoftDeletes;