Skip to content

Commit 0b52ffd

Browse files
authored
fix(database): support pagination with joins and relations (#1801)
1 parent 717c379 commit 0b52ffd

File tree

6 files changed

+130
-8
lines changed

6 files changed

+130
-8
lines changed

packages/database/src/Builder/QueryBuilders/CountQueryBuilder.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
use Tempest\Database\OnDatabase;
1010
use Tempest\Database\Query;
1111
use Tempest\Database\QueryStatements\CountStatement;
12+
use Tempest\Database\QueryStatements\JoinStatement;
1213
use Tempest\Support\Arr\ImmutableArray;
1314
use Tempest\Support\Conditions\HasConditions;
1415
use Tempest\Support\Str\ImmutableString;
1516

1617
use function Tempest\Database\inspect;
18+
use function Tempest\Support\arr;
1719

1820
/**
1921
* @template TModel of object
@@ -31,6 +33,9 @@ final class CountQueryBuilder implements BuildsQuery, SupportsWhereStatements
3133

3234
public array $bindings = [];
3335

36+
/** @var array<JoinStatement|string> */
37+
public array $joins = [];
38+
3439
public ImmutableArray $wheres {
3540
get => $this->count->where;
3641
}
@@ -65,6 +70,16 @@ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $sou
6570
$builder->wheres[] = $where;
6671
}
6772

73+
if ($source instanceof SupportsJoins) {
74+
$builder->joins = $source->joins;
75+
}
76+
77+
if ($source instanceof SupportsRelations) {
78+
foreach ($source->getResolvedRelations() as $relation) {
79+
$builder->joins[] = $relation->getJoinStatement();
80+
}
81+
}
82+
6883
return $builder;
6984
}
7085

@@ -122,6 +137,11 @@ public function toRawSql(): ImmutableString
122137

123138
public function build(mixed ...$bindings): Query
124139
{
140+
if ($this->joins !== []) {
141+
$this->count->joins = arr($this->joins)
142+
->map(fn (JoinStatement|string $join) => $join instanceof JoinStatement ? $join : new JoinStatement($join));
143+
}
144+
125145
return new Query($this->count, [...$this->bindings, ...$bindings])->onDatabase($this->onDatabase);
126146
}
127147
}

packages/database/src/Builder/QueryBuilders/SelectQueryBuilder.php

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Tempest\Database\QueryStatements\OrderByStatement;
2121
use Tempest\Database\QueryStatements\RawStatement;
2222
use Tempest\Database\QueryStatements\SelectStatement;
23+
use Tempest\Database\Relation;
2324
use Tempest\Support\Arr\ImmutableArray;
2425
use Tempest\Support\Conditions\HasConditions;
2526
use Tempest\Support\Paginator\PaginatedData;
@@ -33,19 +34,21 @@
3334
* @template TModel of object
3435
* @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery<TModel>
3536
* @implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements<TModel>
37+
* @implements \Tempest\Database\Builder\QueryBuilders\SupportsJoins<TModel>
38+
* @implements \Tempest\Database\Builder\QueryBuilders\SupportsRelations<TModel>
3639
* @use \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods<TModel>
3740
*/
38-
final class SelectQueryBuilder implements BuildsQuery, SupportsWhereStatements
41+
final class SelectQueryBuilder implements BuildsQuery, SupportsWhereStatements, SupportsJoins, SupportsRelations
3942
{
4043
use HasConditions, OnDatabase, HasWhereQueryBuilderMethods, TransformsQueryBuilder;
4144

4245
public ModelInspector $model;
4346

4447
private SelectStatement $select;
4548

46-
private array $joins = [];
49+
public array $joins = [];
4750

48-
private array $relations = [];
51+
public array $relations = [];
4952

5053
public array $bindings = [];
5154

@@ -133,7 +136,7 @@ public function get(PrimaryKey $id): mixed
133136
*
134137
* @template TSourceModel of object
135138
* @param (BuildsQuery<TSourceModel>&SupportsWhereStatements<TSourceModel>) $source
136-
* @return UpdateQueryBuilder<TSourceModel>
139+
* @return SelectQueryBuilder<TSourceModel>
137140
*/
138141
public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $source, mixed ...$fields): SelectQueryBuilder
139142
{
@@ -144,6 +147,16 @@ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $sou
144147
$builder->wheres[] = $where;
145148
}
146149

150+
if ($source instanceof SupportsJoins) {
151+
$builder->joins = $source->joins;
152+
}
153+
154+
if ($source instanceof SupportsRelations) {
155+
foreach ($source->getResolvedRelations() as $relation) {
156+
$builder->joins[] = $relation->getJoinStatement();
157+
}
158+
}
159+
147160
return $builder;
148161
}
149162

@@ -336,7 +349,7 @@ public function build(mixed ...$bindings): Query
336349
$select = $select->withJoin(new JoinStatement($join));
337350
}
338351

339-
foreach ($this->getIncludedRelations() as $relation) {
352+
foreach ($this->getResolvedRelations() as $relation) {
340353
$select = $select
341354
->withFields($relation->getSelectFields())
342355
->withJoin($relation->getJoinStatement());
@@ -350,8 +363,12 @@ private function clone(): self
350363
return clone $this;
351364
}
352365

353-
/** @return \Tempest\Database\Relation[] */
354-
private function getIncludedRelations(): array
366+
/**
367+
* Gets all resolved relations with their join statements.
368+
*
369+
* @return Relation[]
370+
*/
371+
public function getResolvedRelations(): array
355372
{
356373
$definition = inspect($this->model->getName());
357374

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Tempest\Database\Builder\QueryBuilders;
4+
5+
use Tempest\Database\QueryStatements\JoinStatement;
6+
7+
/**
8+
* @template TModel of object
9+
*/
10+
interface SupportsJoins
11+
{
12+
/**
13+
* The current JOIN statements for this query builder.
14+
*
15+
* @return array<JoinStatement|string>
16+
*/
17+
public array $joins {
18+
get;
19+
}
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Tempest\Database\Builder\QueryBuilders;
4+
5+
use Tempest\Database\Relation;
6+
7+
/**
8+
* @template TModel of object
9+
*/
10+
interface SupportsRelations
11+
{
12+
/**
13+
* The current relation names requested for eager loading.
14+
*
15+
* @return array<string>
16+
*/
17+
public array $relations {
18+
get;
19+
}
20+
21+
/**
22+
* Gets all resolved relations with their join statements.
23+
*
24+
* @return Relation[]
25+
*/
26+
public function getResolvedRelations(): array;
27+
}

packages/database/src/QueryStatements/CountStatement.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ final class CountStatement implements QueryStatement, HasWhereStatements
1313
{
1414
/**
1515
* @param ImmutableArray<WhereStatement> $where
16+
* @param ImmutableArray<JoinStatement> $joins
1617
*/
1718
public function __construct(
1819
public readonly TableDefinition $table,
1920
public ?string $column = null,
2021
public ImmutableArray $where = new ImmutableArray(),
2122
public bool $distinct = false,
23+
public ImmutableArray $joins = new ImmutableArray(),
2224
) {}
2325

2426
public function compile(DatabaseDialect $dialect): string
@@ -34,6 +36,12 @@ public function compile(DatabaseDialect $dialect): string
3436
sprintf('FROM `%s`', $this->table->name),
3537
]);
3638

39+
if ($this->joins->isNotEmpty()) {
40+
foreach ($this->joins as $join) {
41+
$query[] = $join->compile($dialect);
42+
}
43+
}
44+
3745
if ($this->where->isNotEmpty()) {
3846
$query[] = 'WHERE ' . $this->where
3947
->map(fn (QueryStatement $where) => $where->compile($dialect))

tests/Integration/Database/Builder/SelectQueryBuilderTest.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,9 +595,39 @@ public function test_select_query_builder_from_another_query_builder(): void
595595
$this->assertSame('LOTR 1.3', $results[2]->title);
596596
}
597597

598+
public function test_paginate_preserves_relations(): void
599+
{
600+
$this->seed();
601+
602+
$page1 = query(Chapter::class)
603+
->select()
604+
->with('book')
605+
->whereRaw('books.title = ?', 'LOTR 1')
606+
->paginate(itemsPerPage: 5, currentPage: 1);
607+
608+
$this->assertSame(3, $page1->totalItems);
609+
$this->assertCount(3, $page1->data);
610+
$this->assertSame('LOTR 1', $page1->data[0]->book->title);
611+
}
612+
613+
public function test_paginate_with_nested_relations(): void
614+
{
615+
$this->seed();
616+
617+
$page1 = Chapter::select()
618+
->with('book.author')
619+
->paginate(itemsPerPage: 5, currentPage: 1);
620+
621+
$this->assertSame(13, $page1->totalItems);
622+
$this->assertCount(5, $page1->data);
623+
624+
$this->assertInstanceOf(Author::class, $page1->data[0]->book->author);
625+
$this->assertSame('Tolkien', $page1->data[0]->book->author->name);
626+
}
627+
598628
private function seed(): void
599629
{
600-
$this->migrate(
630+
$this->database->migrate(
601631
CreateMigrationsTable::class,
602632
CreatePublishersTable::class,
603633
CreateAuthorTable::class,

0 commit comments

Comments
 (0)