Skip to content

Commit 55204a0

Browse files
authored
feat(database): add ability to create a query builder from another one (#1725)
1 parent 30229ee commit 55204a0

15 files changed

+285
-85
lines changed

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,49 @@
22

33
namespace Tempest\Database\Builder\QueryBuilders;
44

5+
use Tempest\Database\Builder\ModelInspector;
56
use Tempest\Database\Query;
67

78
/**
89
* @template TModel
910
*/
1011
interface BuildsQuery
1112
{
13+
/**
14+
* The current bindings for this query builder.
15+
*
16+
* @return array<mixed>
17+
*/
18+
public array $bindings {
19+
get;
20+
}
21+
22+
/**
23+
* The model inspector for this query builder.
24+
*/
25+
public ModelInspector $model {
26+
get;
27+
}
28+
29+
/**
30+
* Creates a {@see Query} instance with the specified optional bindings.
31+
*
32+
* ### Example
33+
* ```php
34+
* $builder->build(id: $id);
35+
* ```
36+
*/
1237
public function build(mixed ...$bindings): Query;
1338

14-
/** @return self<TModel> */
39+
/**
40+
* Registers the specified bindings for this query.
41+
*
42+
* ### Example
43+
* ```php
44+
* $builder->bind(id: $id);
45+
* ```
46+
*
47+
* @return self<TModel>
48+
*/
1549
public function bind(mixed ...$bindings): self;
1650
}

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

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use Tempest\Database\OnDatabase;
1010
use Tempest\Database\Query;
1111
use Tempest\Database\QueryStatements\CountStatement;
12-
use Tempest\Database\QueryStatements\HasWhereStatements;
12+
use Tempest\Support\Arr\ImmutableArray;
1313
use Tempest\Support\Conditions\HasConditions;
1414
use Tempest\Support\Str\ImmutableString;
1515

@@ -18,17 +18,22 @@
1818
/**
1919
* @template TModel of object
2020
* @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery<TModel>
21+
* @implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements<TModel>
2122
* @use \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods<TModel>
2223
*/
23-
final class CountQueryBuilder implements BuildsQuery
24+
final class CountQueryBuilder implements BuildsQuery, SupportsWhereStatements
2425
{
2526
use HasConditions, OnDatabase, HasWhereQueryBuilderMethods, TransformsQueryBuilder;
2627

2728
private CountStatement $count;
2829

29-
private array $bindings = [];
30+
public ModelInspector $model;
3031

31-
private ModelInspector $model;
32+
public array $bindings = [];
33+
34+
public ImmutableArray $wheres {
35+
get => $this->count->where;
36+
}
3237

3338
/**
3439
* @param class-string<TModel>|string|TModel $model
@@ -43,6 +48,26 @@ public function __construct(string|object $model, ?string $column = null)
4348
);
4449
}
4550

51+
/**
52+
* Creates an instance from another query builder, inheriting conditions and bindings.
53+
*
54+
* @template TSourceModel of object
55+
* @param (BuildsQuery<TSourceModel>&SupportsWhereStatements<TSourceModel>) $source
56+
* @param string|null $column
57+
* @return CountQueryBuilder<TSourceModel>
58+
*/
59+
public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $source, ?string $column = null): CountQueryBuilder
60+
{
61+
$builder = new self($source->model->model, $column);
62+
$builder->bind(...$source->bindings);
63+
64+
foreach ($source->wheres as $where) {
65+
$builder->wheres[] = $where;
66+
}
67+
68+
return $builder;
69+
}
70+
4671
/**
4772
* Executes the count query and returns the number of matching records.
4873
*/
@@ -99,14 +124,4 @@ public function build(mixed ...$bindings): Query
99124
{
100125
return new Query($this->count, [...$this->bindings, ...$bindings])->onDatabase($this->onDatabase);
101126
}
102-
103-
private function getStatementForWhere(): HasWhereStatements
104-
{
105-
return $this->count;
106-
}
107-
108-
private function getModel(): ModelInspector
109-
{
110-
return $this->model;
111-
}
112127
}

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

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use Tempest\Database\OnDatabase;
77
use Tempest\Database\Query;
88
use Tempest\Database\QueryStatements\DeleteStatement;
9-
use Tempest\Database\QueryStatements\HasWhereStatements;
9+
use Tempest\Support\Arr\ImmutableArray;
1010
use Tempest\Support\Conditions\HasConditions;
1111
use Tempest\Support\Str\ImmutableString;
1212

@@ -15,17 +15,22 @@
1515
/**
1616
* @template TModel of object
1717
* @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery<TModel>
18+
* @implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereStatements<TModel>
1819
* @use \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods<TModel>
1920
*/
20-
final class DeleteQueryBuilder implements BuildsQuery
21+
final class DeleteQueryBuilder implements BuildsQuery, SupportsWhereStatements
2122
{
2223
use HasConditions, OnDatabase, HasWhereQueryBuilderMethods, TransformsQueryBuilder;
2324

2425
private DeleteStatement $delete;
2526

26-
private array $bindings = [];
27+
public array $bindings = [];
2728

28-
private ModelInspector $model;
29+
public ModelInspector $model;
30+
31+
public ImmutableArray $wheres {
32+
get => $this->delete->where;
33+
}
2934

3035
/**
3136
* @param class-string<TModel>|string|TModel $model
@@ -36,6 +41,25 @@ public function __construct(string|object $model)
3641
$this->delete = new DeleteStatement($this->model->getTableDefinition());
3742
}
3843

44+
/**
45+
* Creates an instance from another query builder, inheriting conditions and bindings.
46+
*
47+
* @template TSourceModel of object
48+
* @param (BuildsQuery<TSourceModel>&SupportsWhereStatements<TSourceModel>) $source
49+
* @return DeleteQueryBuilder<TSourceModel>
50+
*/
51+
public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $source): DeleteQueryBuilder
52+
{
53+
$builder = new self($source->model->model);
54+
$builder->bind(...$source->bindings);
55+
56+
foreach ($source->wheres as $where) {
57+
$builder->wheres[] = $where;
58+
}
59+
60+
return $builder;
61+
}
62+
3963
/**
4064
* Executes the delete query, removing matching records from the database.
4165
*/
@@ -96,14 +120,4 @@ public function build(mixed ...$bindings): Query
96120

97121
return new Query($this->delete, [...$this->bindings, ...$bindings])->onDatabase($this->onDatabase);
98122
}
99-
100-
private function getStatementForWhere(): HasWhereStatements
101-
{
102-
return $this->delete;
103-
}
104-
105-
private function getModel(): ModelInspector
106-
{
107-
return $this->model;
108-
}
109123
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
/**
1515
* @template TModel of object
16+
* @phpstan-require-implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereConditions
1617
*
1718
* Shared methods for building WHERE conditions and convenience WHERE methods.
1819
*/

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

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,21 @@
33
namespace Tempest\Database\Builder\QueryBuilders;
44

55
use Closure;
6-
use Tempest\Database\Builder\ModelInspector;
76
use Tempest\Database\Builder\WhereOperator;
8-
use Tempest\Database\QueryStatements\HasWhereStatements;
97
use Tempest\Database\QueryStatements\WhereStatement;
10-
use Tempest\Support\Str;
118

129
use function Tempest\Support\str;
1310

1411
/**
1512
* @template TModel of object
1613
* @phpstan-require-implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery
14+
* @phpstan-require-implements \Tempest\Database\Builder\QueryBuilders\SupportsWhereConditions
1715
* @use \Tempest\Database\Builder\QueryBuilders\HasConvenientWhereMethods<TModel>
1816
*/
1917
trait HasWhereQueryBuilderMethods
2018
{
2119
use HasConvenientWhereMethods;
2220

23-
abstract private function getModel(): ModelInspector;
24-
25-
abstract private function getStatementForWhere(): HasWhereStatements;
26-
2721
/**
2822
* Adds a SQL `WHERE` condition to the query. If the `$statement` looks like raw SQL, the method will assume it is and call `whereRaw`. Otherwise, `whereField` will be called.
2923
*
@@ -51,14 +45,14 @@ public function where(string $statement, mixed ...$bindings): self
5145
public function whereField(string $field, mixed $value, string|WhereOperator $operator = WhereOperator::EQUALS): self
5246
{
5347
$operator = WhereOperator::fromOperator($operator);
54-
$fieldDefinition = $this->getModel()->getFieldDefinition($field);
48+
$fieldDefinition = $this->model->getFieldDefinition($field);
5549
$condition = $this->buildCondition((string) $fieldDefinition, $operator, $value);
5650

57-
if ($this->getStatementForWhere()->where->isNotEmpty()) {
51+
if ($this->wheres->isNotEmpty()) {
5852
return $this->andWhere($field, $value, $operator);
5953
}
6054

61-
$this->getStatementForWhere()->where[] = new WhereStatement($condition['sql']);
55+
$this->wheres[] = new WhereStatement($condition['sql']);
6256
$this->bind(...$condition['bindings']);
6357

6458
return $this;
@@ -72,10 +66,10 @@ public function whereField(string $field, mixed $value, string|WhereOperator $op
7266
public function andWhere(string $field, mixed $value, WhereOperator $operator = WhereOperator::EQUALS): self
7367
{
7468
$operator = WhereOperator::fromOperator($operator);
75-
$fieldDefinition = $this->getModel()->getFieldDefinition($field);
69+
$fieldDefinition = $this->model->getFieldDefinition($field);
7670
$condition = $this->buildCondition((string) $fieldDefinition, $operator, $value);
7771

78-
$this->getStatementForWhere()->where[] = new WhereStatement("AND {$condition['sql']}");
72+
$this->wheres[] = new WhereStatement("AND {$condition['sql']}");
7973
$this->bind(...$condition['bindings']);
8074

8175
return $this;
@@ -89,10 +83,10 @@ public function andWhere(string $field, mixed $value, WhereOperator $operator =
8983
public function orWhere(string $field, mixed $value, WhereOperator $operator = WhereOperator::EQUALS): self
9084
{
9185
$operator = WhereOperator::fromOperator($operator);
92-
$fieldDefinition = $this->getModel()->getFieldDefinition($field);
86+
$fieldDefinition = $this->model->getFieldDefinition($field);
9387
$condition = $this->buildCondition((string) $fieldDefinition, $operator, $value);
9488

95-
$this->getStatementForWhere()->where[] = new WhereStatement("OR {$condition['sql']}");
89+
$this->wheres[] = new WhereStatement("OR {$condition['sql']}");
9690
$this->bind(...$condition['bindings']);
9791

9892
return $this;
@@ -105,11 +99,11 @@ public function orWhere(string $field, mixed $value, WhereOperator $operator = W
10599
*/
106100
public function whereRaw(string $statement, mixed ...$bindings): self
107101
{
108-
if ($this->getStatementForWhere()->where->isNotEmpty() && ! str($statement)->trim()->startsWith(['AND', 'OR'])) {
102+
if ($this->wheres->isNotEmpty() && ! str($statement)->trim()->startsWith(['AND', 'OR'])) {
109103
return $this->andWhereRaw($statement, ...$bindings);
110104
}
111105

112-
$this->getStatementForWhere()->where[] = new WhereStatement($statement);
106+
$this->wheres[] = new WhereStatement($statement);
113107
$this->bind(...$bindings);
114108

115109
return $this;
@@ -122,7 +116,7 @@ public function whereRaw(string $statement, mixed ...$bindings): self
122116
*/
123117
public function andWhereRaw(string $rawCondition, mixed ...$bindings): self
124118
{
125-
$this->getStatementForWhere()->where[] = new WhereStatement("AND {$rawCondition}");
119+
$this->wheres[] = new WhereStatement("AND {$rawCondition}");
126120
$this->bind(...$bindings);
127121

128122
return $this;
@@ -135,7 +129,7 @@ public function andWhereRaw(string $rawCondition, mixed ...$bindings): self
135129
*/
136130
public function orWhereRaw(string $rawCondition, mixed ...$bindings): self
137131
{
138-
$this->getStatementForWhere()->where[] = new WhereStatement("OR {$rawCondition}");
132+
$this->wheres[] = new WhereStatement("OR {$rawCondition}");
139133
$this->bind(...$bindings);
140134

141135
return $this;
@@ -149,12 +143,12 @@ public function orWhereRaw(string $rawCondition, mixed ...$bindings): self
149143
*/
150144
public function whereGroup(Closure $callback): self
151145
{
152-
$groupBuilder = new WhereGroupBuilder($this->getModel());
146+
$groupBuilder = new WhereGroupBuilder($this->model);
153147
$callback($groupBuilder);
154148
$group = $groupBuilder->build();
155149

156150
if (! $group->conditions->isEmpty()) {
157-
$this->getStatementForWhere()->where[] = $group;
151+
$this->wheres[] = $group;
158152
$this->bind(...$groupBuilder->getBindings());
159153
}
160154

@@ -169,8 +163,8 @@ public function whereGroup(Closure $callback): self
169163
*/
170164
public function andWhereGroup(Closure $callback): self
171165
{
172-
if ($this->getStatementForWhere()->where->isNotEmpty()) {
173-
$this->getStatementForWhere()->where[] = new WhereStatement('AND');
166+
if ($this->wheres->isNotEmpty()) {
167+
$this->wheres[] = new WhereStatement('AND');
174168
}
175169

176170
return $this->whereGroup($callback);
@@ -184,8 +178,8 @@ public function andWhereGroup(Closure $callback): self
184178
*/
185179
public function orWhereGroup(Closure $callback): self
186180
{
187-
if ($this->getStatementForWhere()->where->isNotEmpty()) {
188-
$this->getStatementForWhere()->where[] = new WhereStatement('OR');
181+
if ($this->wheres->isNotEmpty()) {
182+
$this->wheres[] = new WhereStatement('OR');
189183
}
190184

191185
return $this->whereGroup($callback);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ final class InsertQueryBuilder implements BuildsQuery
3636

3737
private array $after = [];
3838

39-
private array $bindings = [];
39+
public array $bindings = [];
4040

41-
private ModelInspector $model;
41+
public ModelInspector $model;
4242

4343
/**
4444
* @param class-string<TModel>|string|TModel $model

0 commit comments

Comments
 (0)