Skip to content

Commit 02abd4a

Browse files
committed
refactor(database): support hybrid where
1 parent b1aba5c commit 02abd4a

File tree

13 files changed

+172
-54
lines changed

13 files changed

+172
-54
lines changed

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

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Tempest\Database\Builder\WhereOperator;
99
use Tempest\DateTime\DateTime;
1010
use Tempest\DateTime\DateTimeInterface;
11+
use Tempest\Support\Str;
1112
use UnitEnum;
1213

1314
/**
@@ -97,6 +98,19 @@ protected function buildCondition(string $fieldDefinition, WhereOperator $operat
9798
];
9899
}
99100

101+
private function looksLikeWhereRawStatement(string $statement, array $bindings): bool
102+
{
103+
if (count($bindings) === 2 && $bindings[1] instanceof WhereOperator) {
104+
return false;
105+
}
106+
107+
if (! Str\contains($statement, [' ', ...array_map(fn (WhereOperator $op) => $op->value, WhereOperator::cases())])) {
108+
return false;
109+
}
110+
111+
return true;
112+
}
113+
100114
/**
101115
* Adds a `WHERE IN` condition.
102116
*
@@ -106,7 +120,7 @@ protected function buildCondition(string $fieldDefinition, WhereOperator $operat
106120
*/
107121
public function whereIn(string $field, string|UnitEnum|array|ArrayAccess $values): self
108122
{
109-
return $this->where($field, $values, WhereOperator::IN);
123+
return $this->whereField($field, $values, WhereOperator::IN);
110124
}
111125

112126
/**
@@ -118,7 +132,7 @@ public function whereIn(string $field, string|UnitEnum|array|ArrayAccess $values
118132
*/
119133
public function whereNotIn(string $field, string|UnitEnum|array|ArrayAccess $values): self
120134
{
121-
return $this->where($field, $values, WhereOperator::NOT_IN);
135+
return $this->whereField($field, $values, WhereOperator::NOT_IN);
122136
}
123137

124138
/**
@@ -128,7 +142,7 @@ public function whereNotIn(string $field, string|UnitEnum|array|ArrayAccess $val
128142
*/
129143
public function whereBetween(string $field, DateTimeInterface|string|float|int|Countable $min, DateTimeInterface|string|float|int|Countable $max): self
130144
{
131-
return $this->where($field, [$min, $max], WhereOperator::BETWEEN);
145+
return $this->whereField($field, [$min, $max], WhereOperator::BETWEEN);
132146
}
133147

134148
/**
@@ -138,7 +152,7 @@ public function whereBetween(string $field, DateTimeInterface|string|float|int|C
138152
*/
139153
public function whereNotBetween(string $field, DateTimeInterface|string|float|int|Countable $min, DateTimeInterface|string|float|int|Countable $max): self
140154
{
141-
return $this->where($field, [$min, $max], WhereOperator::NOT_BETWEEN);
155+
return $this->whereField($field, [$min, $max], WhereOperator::NOT_BETWEEN);
142156
}
143157

144158
/**
@@ -148,7 +162,7 @@ public function whereNotBetween(string $field, DateTimeInterface|string|float|in
148162
*/
149163
public function whereNull(string $field): self
150164
{
151-
return $this->where($field, null, WhereOperator::IS_NULL);
165+
return $this->whereField($field, null, WhereOperator::IS_NULL);
152166
}
153167

154168
/**
@@ -158,7 +172,7 @@ public function whereNull(string $field): self
158172
*/
159173
public function whereNotNull(string $field): self
160174
{
161-
return $this->where($field, null, WhereOperator::IS_NOT_NULL);
175+
return $this->whereField($field, null, WhereOperator::IS_NOT_NULL);
162176
}
163177

164178
/**
@@ -168,7 +182,7 @@ public function whereNotNull(string $field): self
168182
*/
169183
public function whereNot(string $field, mixed $value): self
170184
{
171-
return $this->where($field, $value, WhereOperator::NOT_EQUALS);
185+
return $this->whereField($field, $value, WhereOperator::NOT_EQUALS);
172186
}
173187

174188
/**
@@ -178,7 +192,7 @@ public function whereNot(string $field, mixed $value): self
178192
*/
179193
public function whereLike(string $field, string $value): self
180194
{
181-
return $this->where($field, $value, WhereOperator::LIKE);
195+
return $this->whereField($field, $value, WhereOperator::LIKE);
182196
}
183197

184198
/**
@@ -188,7 +202,7 @@ public function whereLike(string $field, string $value): self
188202
*/
189203
public function whereNotLike(string $field, string $value): self
190204
{
191-
return $this->where($field, $value, WhereOperator::NOT_LIKE);
205+
return $this->whereField($field, $value, WhereOperator::NOT_LIKE);
192206
}
193207

194208
/**
@@ -388,7 +402,7 @@ public function whereLastYear(string $field): self
388402
*/
389403
public function whereAfter(string $field, DateTimeInterface|string $date): self
390404
{
391-
return $this->where($field, DateTime::parse($date), WhereOperator::GREATER_THAN);
405+
return $this->whereField($field, DateTime::parse($date), WhereOperator::GREATER_THAN);
392406
}
393407

394408
/**
@@ -398,7 +412,7 @@ public function whereAfter(string $field, DateTimeInterface|string $date): self
398412
*/
399413
public function whereBefore(string $field, DateTimeInterface|string $date): self
400414
{
401-
return $this->where($field, DateTime::parse($date), WhereOperator::LESS_THAN);
415+
return $this->whereField($field, DateTime::parse($date), WhereOperator::LESS_THAN);
402416
}
403417

404418
/**
@@ -486,7 +500,7 @@ public function orWhereBefore(string $field, DateTimeInterface|string $date): se
486500
*
487501
* @return self<TModel>
488502
*/
489-
abstract public function where(string $field, mixed $value, string|WhereOperator $operator = WhereOperator::EQUALS): self;
503+
abstract public function whereField(string $field, mixed $value, string|WhereOperator $operator = WhereOperator::EQUALS): self;
490504

491505
/**
492506
* Abstract method that must be implemented by classes using this trait.

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Tempest\Database\Builder\WhereOperator;
88
use Tempest\Database\QueryStatements\HasWhereStatements;
99
use Tempest\Database\QueryStatements\WhereStatement;
10+
use Tempest\Support\Str;
1011

1112
use function Tempest\Support\str;
1213

@@ -23,12 +24,31 @@ abstract private function getModel(): ModelInspector;
2324

2425
abstract private function getStatementForWhere(): HasWhereStatements;
2526

27+
/**
28+
* 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.
29+
*
30+
* **Example**
31+
* ```php
32+
* ->where('price > ?', $value); // calls `whereRaw`
33+
* ->where('price', $value); // calls `whereField`
34+
* ```
35+
* @return self<TModel>
36+
*/
37+
public function where(string $statement, mixed ...$bindings): self
38+
{
39+
if ($this->looksLikeWhereRawStatement($statement, $bindings)) {
40+
return $this->whereRaw($statement, ...$bindings);
41+
}
42+
43+
return $this->whereField($statement, value: $bindings[0], operator: $bindings[1] ?? WhereOperator::EQUALS);
44+
}
45+
2646
/**
2747
* Adds a where condition to the query.
2848
*
2949
* @return self<TModel>
3050
*/
31-
public function where(string $field, mixed $value, string|WhereOperator $operator = WhereOperator::EQUALS): self
51+
public function whereField(string $field, mixed $value, string|WhereOperator $operator = WhereOperator::EQUALS): self
3252
{
3353
$operator = WhereOperator::fromOperator($operator);
3454
$fieldDefinition = $this->getModel()->getFieldDefinition($field);
@@ -83,13 +103,13 @@ public function orWhere(string $field, mixed $value, WhereOperator $operator = W
83103
*
84104
* @return self<TModel>
85105
*/
86-
public function whereRaw(string $rawCondition, mixed ...$bindings): self
106+
public function whereRaw(string $statement, mixed ...$bindings): self
87107
{
88-
if ($this->getStatementForWhere()->where->isNotEmpty() && ! str($rawCondition)->trim()->startsWith(['AND', 'OR'])) {
89-
return $this->andWhereRaw($rawCondition, ...$bindings);
108+
if ($this->getStatementForWhere()->where->isNotEmpty() && ! str($statement)->trim()->startsWith(['AND', 'OR'])) {
109+
return $this->andWhereRaw($statement, ...$bindings);
90110
}
91111

92-
$this->getStatementForWhere()->where[] = new WhereStatement($rawCondition);
112+
$this->getStatementForWhere()->where[] = new WhereStatement($statement);
93113
$this->bind(...$bindings);
94114

95115
return $this;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ public function find(mixed ...$conditions): SelectQueryBuilder
233233
$query = $this->select();
234234

235235
foreach ($conditions as $field => $value) {
236-
$query->where($field, $value);
236+
$query->whereField($field, $value);
237237
}
238238

239239
return $query;
@@ -290,7 +290,7 @@ public function findOrNew(array $find, array $update): object
290290
$existing = $this->select();
291291

292292
foreach ($find as $key => $value) {
293-
$existing = $existing->where($key, $value);
293+
$existing = $existing->whereField($key, $value);
294294
}
295295

296296
$model = $existing->first() ?? $this->new(...$find);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public function get(PrimaryKey $id): mixed
119119
throw ModelDidNotHavePrimaryColumn::neededForMethod($this->model->getName(), 'get');
120120
}
121121

122-
return $this->where($this->model->getPrimaryKey(), $id)->first();
122+
return $this->whereField($this->model->getPrimaryKey(), $id)->first();
123123
}
124124

125125
/**

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ private function deleteExistingHasManyRelations($hasMany, PrimaryKey $parentId):
350350
: $this->getDefaultForeignKeyName();
351351

352352
new DeleteQueryBuilder($relatedModel->getName())
353-
->where($foreignKey, $parentId->value)
353+
->whereField($foreignKey, $parentId->value)
354354
->build()
355355
->onDatabase($this->onDatabase)
356356
->execute();
@@ -377,7 +377,7 @@ private function deleteCustomHasOneRelation($hasOne, PrimaryKey $parentId): void
377377
$foreignKeyColumn = $hasOne->relationJoin ?? $this->removeTablePrefix($hasOne->ownerJoin);
378378

379379
$result = new SelectQueryBuilder($ownerModel->getName(), new ImmutableArray([$foreignKeyColumn]))
380-
->where($ownerModel->getPrimaryKey(), $parentId->value)
380+
->whereField($ownerModel->getPrimaryKey(), $parentId->value)
381381
->build()
382382
->onDatabase($this->onDatabase)
383383
->fetchFirst();
@@ -389,13 +389,13 @@ private function deleteCustomHasOneRelation($hasOne, PrimaryKey $parentId): void
389389
$relatedId = $result[$foreignKeyColumn];
390390

391391
new DeleteQueryBuilder($relatedModel->getName())
392-
->where($relatedModel->getPrimaryKey(), $relatedId)
392+
->whereField($relatedModel->getPrimaryKey(), $relatedId)
393393
->build()
394394
->onDatabase($this->onDatabase)
395395
->execute();
396396

397397
new UpdateQueryBuilder($ownerModel->getName(), [$foreignKeyColumn => null], $this->serializerFactory)
398-
->where($ownerModel->getPrimaryKey(), $parentId->value)
398+
->whereField($ownerModel->getPrimaryKey(), $parentId->value)
399399
->build()
400400
->onDatabase($this->onDatabase)
401401
->execute();
@@ -411,7 +411,7 @@ private function deleteStandardHasOneRelation($hasOne, PrimaryKey $parentId): vo
411411
$foreignKeyColumn = Intl\singularize($ownerModel->getTableName()) . '_' . $ownerModel->getPrimaryKey();
412412

413413
new DeleteQueryBuilder($relatedModel->getName())
414-
->where($foreignKeyColumn, $parentId->value)
414+
->whereField($foreignKeyColumn, $parentId->value)
415415
->build()
416416
->onDatabase($this->onDatabase)
417417
->execute();
@@ -431,7 +431,7 @@ private function handleCustomHasOneRelation($hasOne, object|array $relation, Pri
431431
$foreignKeyColumn = $hasOne->relationJoin ?? $this->removeTablePrefix($hasOne->ownerJoin);
432432

433433
new UpdateQueryBuilder($ownerModel->getName(), [$foreignKeyColumn => $relatedModelId->value], $this->serializerFactory)
434-
->where($ownerModel->getPrimaryKey(), $parentId->value)
434+
->whereField($ownerModel->getPrimaryKey(), $parentId->value)
435435
->build()
436436
->onDatabase($this->onDatabase)
437437
->execute();
@@ -510,7 +510,7 @@ private function removeTablePrefix(string $column): string
510510
*
511511
* @return self<TModel>
512512
*/
513-
public function where(string $field, mixed $value, string|WhereOperator $operator = WhereOperator::EQUALS): self
513+
public function whereField(string $field, mixed $value, string|WhereOperator $operator = WhereOperator::EQUALS): self
514514
{
515515
$operator = WhereOperator::fromOperator($operator);
516516

@@ -586,7 +586,7 @@ private function setWhereForObjectModel(): void
586586
}
587587

588588
if ($primaryKeyValue = $this->model->getPrimaryKeyValue()) {
589-
$this->where($this->model->getPrimaryKey(), $primaryKeyValue->value);
589+
$this->whereField($this->model->getPrimaryKey(), $primaryKeyValue->value);
590590
}
591591
}
592592
}

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Tempest\Database\Builder\WhereOperator;
88
use Tempest\Database\QueryStatements\WhereGroupStatement;
99
use Tempest\Database\QueryStatements\WhereStatement;
10+
use Tempest\Support\Str;
1011

1112
use function Tempest\Support\arr;
1213
use function Tempest\Support\str;
@@ -29,12 +30,31 @@ public function __construct(
2930
private readonly ModelInspector $model,
3031
) {}
3132

33+
/**
34+
* 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.
35+
*
36+
* **Example**
37+
* ```php
38+
* ->where('price > ?', $value); // calls `whereRaw`
39+
* ->where('price', $value); // calls `whereField`
40+
* ```
41+
* @return self<TModel>
42+
*/
43+
public function where(string $statement, mixed ...$bindings): self
44+
{
45+
if ($this->looksLikeWhereRawStatement($statement, $bindings)) {
46+
return $this->whereRaw($statement, ...$bindings);
47+
}
48+
49+
return $this->whereField($statement, value: $bindings[0], operator: $bindings[1] ?? WhereOperator::EQUALS);
50+
}
51+
3252
/**
3353
* Adds a `WHERE` condition to the group.
3454
*
3555
* @return self<TModel>
3656
*/
37-
public function where(string $field, mixed $value = null, string|WhereOperator $operator = WhereOperator::EQUALS): self
57+
public function whereField(string $field, mixed $value = null, string|WhereOperator $operator = WhereOperator::EQUALS): self
3858
{
3959
return $this->andWhere($field, $value, WhereOperator::fromOperator($operator));
4060
}

packages/database/src/IsDatabaseModel.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ public function update(mixed ...$params): self
269269

270270
query($this)
271271
->update(...$params)
272-
->where($model->getPrimaryKey(), $model->getPrimaryKeyValue())
272+
->whereField($model->getPrimaryKey(), $model->getPrimaryKeyValue())
273273
->execute();
274274

275275
foreach ($params as $key => $value) {

tests/Integration/Database/Builder/UpdateRelationsTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
use Tests\Tempest\Fixtures\Modules\Books\Models\Isbn;
2626
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
2727

28-
use function Tempest\Database\inspect;
2928
use function Tempest\Database\query;
3029

3130
final class UpdateRelationsTest extends FrameworkIntegrationTestCase

0 commit comments

Comments
 (0)