Skip to content

Commit 53f5cdf

Browse files
committed
Add lateral join support to Query Builder
1 parent f7c57c4 commit 53f5cdf

File tree

10 files changed

+546
-0
lines changed

10 files changed

+546
-0
lines changed

src/Illuminate/Database/Query/Builder.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,39 @@ public function joinSub($query, $as, $first, $operator = null, $second = null, $
586586
return $this->join(new Expression($expression), $first, $operator, $second, $type, $where);
587587
}
588588

589+
/**
590+
* Add a lateral join clause to the query.
591+
*
592+
* @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query
593+
* @param string $as
594+
* @param string $type
595+
* @return $this
596+
*/
597+
public function joinLateral($query, string $as, string $type = 'inner'): static
598+
{
599+
[$query, $bindings] = $this->createSub($query);
600+
601+
$expression = '('.$query.') as '.$this->grammar->wrapTable($as);
602+
603+
$this->addBinding($bindings, 'join');
604+
605+
$this->joins[] = $this->newJoinLateralClause($this, $type, new Expression($expression));
606+
607+
return $this;
608+
}
609+
610+
/**
611+
* Add a lateral left join to the query.
612+
*
613+
* @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query
614+
* @param string $as
615+
* @return $this
616+
*/
617+
public function leftJoinLateral($query, string $as): static
618+
{
619+
return $this->joinLateral($query, $as, 'left');
620+
}
621+
589622
/**
590623
* Add a left join to the query.
591624
*
@@ -725,6 +758,19 @@ protected function newJoinClause(self $parentQuery, $type, $table)
725758
return new JoinClause($parentQuery, $type, $table);
726759
}
727760

761+
/**
762+
* Get a new join lateral clause.
763+
*
764+
* @param \Illuminate\Database\Query\Builder $parentQuery
765+
* @param string $type
766+
* @param string $table
767+
* @return \Illuminate\Database\Query\JoinLateralClause
768+
*/
769+
protected function newJoinLateralClause(self $parentQuery, $type, $table)
770+
{
771+
return new JoinLateralClause($parentQuery, $type, $table);
772+
}
773+
728774
/**
729775
* Merge an array of where clauses and bindings.
730776
*

src/Illuminate/Database/Query/Grammars/Grammar.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Database\Grammar as BaseGrammar;
88
use Illuminate\Database\Query\Builder;
99
use Illuminate\Database\Query\JoinClause;
10+
use Illuminate\Database\Query\JoinLateralClause;
1011
use Illuminate\Support\Arr;
1112
use RuntimeException;
1213

@@ -182,10 +183,28 @@ protected function compileJoins(Builder $query, $joins)
182183

183184
$tableAndNestedJoins = is_null($join->joins) ? $table : '('.$table.$nestedJoins.')';
184185

186+
if ($join instanceof JoinLateralClause) {
187+
return $this->compileJoinLateral($join, $tableAndNestedJoins);
188+
}
189+
185190
return trim("{$join->type} join {$tableAndNestedJoins} {$this->compileWheres($join)}");
186191
})->implode(' ');
187192
}
188193

194+
/**
195+
* Compile a "lateral join" clause.
196+
*
197+
* @param \Illuminate\Database\Query\JoinLateralClause $join
198+
* @param string $expression
199+
* @return string
200+
*
201+
* @throws \RuntimeException
202+
*/
203+
public function compileJoinLateral(JoinLateralClause $join, string $expression): string
204+
{
205+
throw new RuntimeException('This database engine does not support lateral joins.');
206+
}
207+
189208
/**
190209
* Compile the "where" portions of the query.
191210
*

src/Illuminate/Database/Query/Grammars/MySqlGrammar.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Illuminate\Database\Query\Grammars;
44

55
use Illuminate\Database\Query\Builder;
6+
use Illuminate\Database\Query\JoinLateralClause;
67
use Illuminate\Support\Str;
78

89
class MySqlGrammar extends Grammar
@@ -233,6 +234,18 @@ protected function compileUpdateColumns(Builder $query, array $values)
233234
})->implode(', ');
234235
}
235236

237+
/**
238+
* Compile a "lateral join" clause.
239+
*
240+
* @param \Illuminate\Database\Query\JoinLateralClause $join
241+
* @param string $expression
242+
* @return string
243+
*/
244+
public function compileJoinLateral(JoinLateralClause $join, string $expression): string
245+
{
246+
return trim("{$join->type} join lateral {$expression} on true");
247+
}
248+
236249
/**
237250
* Compile an "upsert" statement into SQL.
238251
*

src/Illuminate/Database/Query/Grammars/PostgresGrammar.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Illuminate\Database\Query\Grammars;
44

55
use Illuminate\Database\Query\Builder;
6+
use Illuminate\Database\Query\JoinLateralClause;
67
use Illuminate\Support\Arr;
78
use Illuminate\Support\Str;
89

@@ -385,6 +386,18 @@ protected function compileUpdateColumns(Builder $query, array $values)
385386
})->implode(', ');
386387
}
387388

389+
/**
390+
* Compile a "lateral join" clause.
391+
*
392+
* @param \Illuminate\Database\Query\JoinLateralClause $join
393+
* @param string $expression
394+
* @return string
395+
*/
396+
public function compileJoinLateral(JoinLateralClause $join, string $expression): string
397+
{
398+
return trim("{$join->type} join lateral {$expression} on true");
399+
}
400+
388401
/**
389402
* Compile an "upsert" statement into SQL.
390403
*

src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Illuminate\Database\Query\Grammars;
44

55
use Illuminate\Database\Query\Builder;
6+
use Illuminate\Database\Query\JoinLateralClause;
67
use Illuminate\Support\Arr;
78
use Illuminate\Support\Str;
89

@@ -386,6 +387,20 @@ protected function compileUpdateWithJoins(Builder $query, $table, $columns, $whe
386387
return "update {$alias} set {$columns} from {$table} {$joins} {$where}";
387388
}
388389

390+
/**
391+
* Compile a "lateral join" clause.
392+
*
393+
* @param \Illuminate\Database\Query\JoinLateralClause $join
394+
* @param string $expression
395+
* @return string
396+
*/
397+
public function compileJoinLateral(JoinLateralClause $join, string $expression): string
398+
{
399+
$type = $join->type == 'left' ? 'outer' : 'cross';
400+
401+
return trim("{$type} apply {$expression}");
402+
}
403+
389404
/**
390405
* Compile an "upsert" statement into SQL.
391406
*
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Query;
4+
5+
class JoinLateralClause extends JoinClause
6+
{
7+
//
8+
}

tests/Database/DatabaseQueryBuilderTest.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,6 +2526,117 @@ public function testRightJoinSub()
25262526
$builder->from('users')->rightJoinSub(['foo'], 'sub', 'users.id', '=', 'sub.id');
25272527
}
25282528

2529+
public function testJoinLateral()
2530+
{
2531+
$builder = $this->getMySqlBuilder();
2532+
$builder->getConnection()->shouldReceive('getDatabaseName');
2533+
$builder->from('users')->joinLateral('select * from `contacts` where `contracts`.`user_id` = `users`.`id`', 'sub');
2534+
$this->assertSame('select * from `users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql());
2535+
2536+
$builder = $this->getMySqlBuilder();
2537+
$builder->getConnection()->shouldReceive('getDatabaseName');
2538+
$builder->from('users')->joinLateral(function ($q) {
2539+
$q->from('contacts')->whereColumn('contracts.user_id', 'users.id');
2540+
}, 'sub');
2541+
$this->assertSame('select * from `users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql());
2542+
2543+
$builder = $this->getMySqlBuilder();
2544+
$builder->getConnection()->shouldReceive('getDatabaseName');
2545+
$sub = $this->getMySqlBuilder();
2546+
$sub->getConnection()->shouldReceive('getDatabaseName');
2547+
$eloquentBuilder = new EloquentBuilder($sub->from('contacts')->whereColumn('contracts.user_id', 'users.id'));
2548+
$builder->from('users')->joinLateral($eloquentBuilder, 'sub');
2549+
$this->assertSame('select * from `users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql());
2550+
2551+
$sub1 = $this->getMySqlBuilder();
2552+
$sub1->getConnection()->shouldReceive('getDatabaseName');
2553+
$sub1 = $sub1->from('contacts')->whereColumn('contracts.user_id', 'users.id')->where('name', 'foo');
2554+
2555+
$sub2 = $this->getMySqlBuilder();
2556+
$sub2->getConnection()->shouldReceive('getDatabaseName');
2557+
$sub2 = $sub2->from('contacts')->whereColumn('contracts.user_id', 'users.id')->where('name', 'bar');
2558+
2559+
$builder = $this->getMySqlBuilder();
2560+
$builder->getConnection()->shouldReceive('getDatabaseName');
2561+
$builder->from('users')->joinLateral($sub1, 'sub1')->joinLateral($sub2, 'sub2');
2562+
2563+
$expected = 'select * from `users` ';
2564+
$expected .= 'inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id` and `name` = ?) as `sub1` on true ';
2565+
$expected .= 'inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id` and `name` = ?) as `sub2` on true';
2566+
2567+
$this->assertEquals($expected, $builder->toSql());
2568+
$this->assertEquals(['foo', 'bar'], $builder->getRawBindings()['join']);
2569+
2570+
$this->expectException(InvalidArgumentException::class);
2571+
$builder = $this->getMySqlBuilder();
2572+
$builder->from('users')->joinLateral(['foo'], 'sub');
2573+
}
2574+
2575+
public function testJoinLateralSQLite()
2576+
{
2577+
$this->expectException(RuntimeException::class);
2578+
$builder = $this->getSQLiteBuilder();
2579+
$builder->getConnection()->shouldReceive('getDatabaseName');
2580+
$builder->from('users')->joinLateral(function ($q) {
2581+
$q->from('contacts')->whereColumn('contracts.user_id', 'users.id');
2582+
}, 'sub')->toSql();
2583+
}
2584+
2585+
public function testJoinLateralPostgres()
2586+
{
2587+
$builder = $this->getPostgresBuilder();
2588+
$builder->getConnection()->shouldReceive('getDatabaseName');
2589+
$builder->from('users')->joinLateral(function ($q) {
2590+
$q->from('contacts')->whereColumn('contracts.user_id', 'users.id');
2591+
}, 'sub');
2592+
$this->assertSame('select * from "users" inner join lateral (select * from "contacts" where "contracts"."user_id" = "users"."id") as "sub" on true', $builder->toSql());
2593+
}
2594+
2595+
public function testJoinLateralSqlServer()
2596+
{
2597+
$builder = $this->getSqlServerBuilder();
2598+
$builder->getConnection()->shouldReceive('getDatabaseName');
2599+
$builder->from('users')->joinLateral(function ($q) {
2600+
$q->from('contacts')->whereColumn('contracts.user_id', 'users.id');
2601+
}, 'sub');
2602+
$this->assertSame('select * from [users] cross apply (select * from [contacts] where [contracts].[user_id] = [users].[id]) as [sub]', $builder->toSql());
2603+
}
2604+
2605+
public function testJoinLateralWithPrefix()
2606+
{
2607+
$builder = $this->getMySqlBuilder();
2608+
$builder->getConnection()->shouldReceive('getDatabaseName');
2609+
$builder->getGrammar()->setTablePrefix('prefix_');
2610+
$builder->from('users')->joinLateral('select * from `contacts` where `contracts`.`user_id` = `users`.`id`', 'sub');
2611+
$this->assertSame('select * from `prefix_users` inner join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `prefix_sub` on true', $builder->toSql());
2612+
}
2613+
2614+
public function testLeftJoinLateral()
2615+
{
2616+
$builder = $this->getMySqlBuilder();
2617+
$builder->getConnection()->shouldReceive('getDatabaseName');
2618+
2619+
$sub = $this->getMySqlBuilder();
2620+
$sub->getConnection()->shouldReceive('getDatabaseName');
2621+
2622+
$builder->from('users')->leftJoinLateral($sub->from('contacts')->whereColumn('contracts.user_id', 'users.id'), 'sub');
2623+
$this->assertSame('select * from `users` left join lateral (select * from `contacts` where `contracts`.`user_id` = `users`.`id`) as `sub` on true', $builder->toSql());
2624+
2625+
$this->expectException(InvalidArgumentException::class);
2626+
$builder = $this->getBuilder();
2627+
$builder->from('users')->leftJoinLateral(['foo'], 'sub');
2628+
}
2629+
2630+
public function testLeftJoinLateralSqlServer()
2631+
{
2632+
$builder = $this->getSqlServerBuilder();
2633+
$builder->getConnection()->shouldReceive('getDatabaseName');
2634+
$builder->from('users')->leftJoinLateral(function ($q) {
2635+
$q->from('contacts')->whereColumn('contracts.user_id', 'users.id');
2636+
}, 'sub');
2637+
$this->assertSame('select * from [users] outer apply (select * from [contacts] where [contracts].[user_id] = [users].[id]) as [sub]', $builder->toSql());
2638+
}
2639+
25292640
public function testRawExpressionsInSelect()
25302641
{
25312642
$builder = $this->getBuilder();

0 commit comments

Comments
 (0)