Skip to content

Commit c6302f3

Browse files
authored
feat(database)!: improve orm experience (#1458)
1 parent 62d2c1a commit c6302f3

File tree

194 files changed

+13481
-3077
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

194 files changed

+13481
-3077
lines changed

packages/auth/src/CanAuthenticate.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
namespace Tempest\Auth;
66

7-
use Tempest\Database\Id;
7+
use Tempest\Database\PrimaryKey;
88

99
interface CanAuthenticate
1010
{
11-
public ?Id $id {
11+
public ?PrimaryKey $id {
1212
get;
1313
}
1414
}

packages/auth/src/Install/Permission.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66

77
use BackedEnum;
88
use Tempest\Database\IsDatabaseModel;
9+
use Tempest\Database\PrimaryKey;
910
use UnitEnum;
1011

1112
final class Permission
1213
{
1314
use IsDatabaseModel;
1415

16+
public PrimaryKey $id;
17+
1518
public function __construct(
1619
public string $name,
1720
) {}

packages/auth/src/Install/User.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Tempest\Auth\CanAuthenticate;
1010
use Tempest\Auth\CanAuthorize;
1111
use Tempest\Database\IsDatabaseModel;
12+
use Tempest\Database\PrimaryKey;
1213
use UnitEnum;
1314

1415
use function Tempest\Support\arr;
@@ -17,6 +18,8 @@ final class User implements CanAuthenticate, CanAuthorize
1718
{
1819
use IsDatabaseModel;
1920

21+
public PrimaryKey $id;
22+
2023
public string $password;
2124

2225
public function __construct(
@@ -78,7 +81,9 @@ private function resolvePermission(string|UnitEnum|Permission $permission): Perm
7881
$permission instanceof UnitEnum => $permission->name,
7982
};
8083

81-
$permission = Permission::select()->whereField('name', $name)->first();
84+
$permission = Permission::select()
85+
->where('name', $name)
86+
->first();
8287

8388
return $permission ?? new Permission($name)->save();
8489
}

packages/auth/src/Install/UserPermission.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
namespace Tempest\Auth\Install;
66

77
use Tempest\Database\IsDatabaseModel;
8+
use Tempest\Database\PrimaryKey;
89

910
final class UserPermission
1011
{
1112
use IsDatabaseModel;
1213

14+
public PrimaryKey $id;
15+
1316
public User $user;
1417

1518
public Permission $permission;

packages/database/src/BelongsTo.php

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Attribute;
88
use Tempest\Database\Builder\ModelInspector;
9+
use Tempest\Database\Exceptions\ModelDidNotHavePrimaryColumn;
910
use Tempest\Database\QueryStatements\FieldStatement;
1011
use Tempest\Database\QueryStatements\JoinStatement;
1112
use Tempest\Reflection\PropertyReflector;
@@ -46,14 +47,19 @@ public function getOwnerFieldName(): string
4647
}
4748
}
4849

49-
$relationModel = model($this->property->getType()->asClass());
50+
$relationModel = inspect($this->property->getType()->asClass());
51+
$primaryKey = $relationModel->getPrimaryKey();
5052

51-
return str($relationModel->getTableName())->singularizeLastWord() . '_' . $relationModel->getPrimaryKey();
53+
if ($primaryKey === null) {
54+
throw ModelDidNotHavePrimaryColumn::neededForRelation($relationModel->getName(), 'BelongsTo');
55+
}
56+
57+
return str($relationModel->getTableName())->singularizeLastWord() . '_' . $primaryKey;
5258
}
5359

5460
public function getSelectFields(): ImmutableArray
5561
{
56-
$relationModel = model($this->property->getType()->asClass());
62+
$relationModel = inspect($this->property->getType()->asClass());
5763

5864
return $relationModel
5965
->getSelectFields()
@@ -70,8 +76,8 @@ public function getSelectFields(): ImmutableArray
7076

7177
public function getJoinStatement(): JoinStatement
7278
{
73-
$relationModel = model($this->property->getType()->asClass());
74-
$ownerModel = model($this->property->getClass());
79+
$relationModel = inspect($this->property->getType()->asClass());
80+
$ownerModel = inspect($this->property->getClass());
7581

7682
$relationJoin = $this->getRelationJoin($relationModel);
7783
$ownerJoin = $this->getOwnerJoin($ownerModel);
@@ -97,11 +103,13 @@ private function getRelationJoin(ModelInspector $relationModel): string
97103
return $relationJoin;
98104
}
99105

100-
return sprintf(
101-
'%s.%s',
102-
$relationModel->getTableName(),
103-
$relationModel->getPrimaryKey(),
104-
);
106+
$primaryKey = $relationModel->getPrimaryKey();
107+
108+
if ($primaryKey === null) {
109+
throw ModelDidNotHavePrimaryColumn::neededForRelation($relationModel->getName(), 'BelongsTo');
110+
}
111+
112+
return sprintf('%s.%s', $relationModel->getTableName(), $primaryKey);
105113
}
106114

107115
private function getOwnerJoin(ModelInspector $ownerModel): string

packages/database/src/Builder/ModelInspector.php

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
use Tempest\Database\BelongsTo;
77
use Tempest\Database\Config\DatabaseConfig;
88
use Tempest\Database\Eager;
9+
use Tempest\Database\Exceptions\ModelDidNotHavePrimaryColumn;
10+
use Tempest\Database\Exceptions\ModelHadMultiplePrimaryColumns;
911
use Tempest\Database\HasMany;
1012
use Tempest\Database\HasOne;
11-
use Tempest\Database\Id;
13+
use Tempest\Database\PrimaryKey;
1214
use Tempest\Database\Relation;
1315
use Tempest\Database\Table;
1416
use Tempest\Database\Virtual;
@@ -21,7 +23,7 @@
2123
use Tempest\Validation\SkipValidation;
2224
use Tempest\Validation\Validator;
2325

24-
use function Tempest\Database\model;
26+
use function Tempest\Database\inspect;
2527
use function Tempest\get;
2628
use function Tempest\Support\arr;
2729
use function Tempest\Support\str;
@@ -292,7 +294,7 @@ public function resolveRelations(string $relationString, string $parent = ''): a
292294

293295
unset($relationNames[0]);
294296

295-
$relationModel = model($currentRelation);
297+
$relationModel = inspect($currentRelation);
296298

297299
$newRelationString = implode('.', $relationNames);
298300
$currentRelation->setParent($parent);
@@ -334,7 +336,7 @@ public function resolveEagerRelations(string $parent = ''): array
334336
$currentRelationName,
335337
), '.');
336338

337-
foreach (model($currentRelation)->resolveEagerRelations($newParent) as $name => $nestedEagerRelation) {
339+
foreach (inspect($currentRelation)->resolveEagerRelations($newParent) as $name => $nestedEagerRelation) {
338340
$relations[$name] = $nestedEagerRelation;
339341
}
340342
}
@@ -357,6 +359,10 @@ public function validate(mixed ...$data): void
357359
continue;
358360
}
359361

362+
if ($property->getType()->getName() === PrimaryKey::class) {
363+
continue;
364+
}
365+
360366
$failingRulesForProperty = $this->validator->validateValueForProperty(
361367
$property,
362368
$value,
@@ -381,17 +387,45 @@ public function getName(): string
381387
return $this->instance;
382388
}
383389

384-
public function getPrimaryFieldName(): string
390+
public function getQualifiedPrimaryKey(): ?string
385391
{
386-
return $this->getTableDefinition()->name . '.' . $this->getPrimaryKey();
392+
$primaryKey = $this->getPrimaryKey();
393+
394+
return $primaryKey !== null
395+
? ($this->getTableDefinition()->name . '.' . $primaryKey)
396+
: null;
387397
}
388398

389-
public function getPrimaryKey(): string
399+
public function getPrimaryKey(): ?string
390400
{
391-
return 'id';
401+
return $this->getPrimaryKeyProperty()?->getName();
402+
}
403+
404+
public function hasPrimaryKey(): bool
405+
{
406+
return $this->getPrimaryKeyProperty() !== null;
407+
}
408+
409+
public function getPrimaryKeyProperty(): ?PropertyReflector
410+
{
411+
if (! $this->isObjectModel()) {
412+
return null;
413+
}
414+
415+
$primaryKeys = arr($this->reflector->getProperties())
416+
->filter(fn (PropertyReflector $property) => $property->getType()->matches(PrimaryKey::class));
417+
418+
return match ($primaryKeys->count()) {
419+
0 => null,
420+
1 => $primaryKeys->first(),
421+
default => throw ModelHadMultiplePrimaryColumns::found(
422+
model: $this->model,
423+
properties: $primaryKeys->map(fn (PropertyReflector $property) => $property->getName())->toArray(),
424+
),
425+
};
392426
}
393427

394-
public function getPrimaryKeyValue(): ?Id
428+
public function getPrimaryKeyValue(): ?PrimaryKey
395429
{
396430
if (! $this->isObjectModel()) {
397431
return null;
@@ -401,6 +435,16 @@ public function getPrimaryKeyValue(): ?Id
401435
return null;
402436
}
403437

404-
return $this->instance->{$this->getPrimaryKey()};
438+
$primaryKeyProperty = $this->getPrimaryKeyProperty();
439+
440+
if ($primaryKeyProperty === null) {
441+
return null;
442+
}
443+
444+
if (! $primaryKeyProperty->isInitialized($this->instance)) {
445+
return null;
446+
}
447+
448+
return $primaryKeyProperty->getValue($this->instance);
405449
}
406450
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
use Tempest\Database\Query;
66

77
/**
8-
* @template TModelClass
8+
* @template TModel
99
*/
1010
interface BuildsQuery
1111
{
1212
public function build(mixed ...$bindings): Query;
1313

14-
/** @return self<TModelClass> */
14+
/** @return self<TModel> */
1515
public function bind(mixed ...$bindings): self;
1616
}

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

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@
1111
use Tempest\Database\QueryStatements\CountStatement;
1212
use Tempest\Database\QueryStatements\HasWhereStatements;
1313
use Tempest\Support\Conditions\HasConditions;
14+
use Tempest\Support\Str\ImmutableString;
1415

15-
use function Tempest\Database\model;
16+
use function Tempest\Database\inspect;
1617

1718
/**
18-
* @template T of object
19-
* @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery<T>
20-
* @uses \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods<T>
19+
* @template TModel of object
20+
* @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery<TModel>
21+
* @use \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods<TModel>
2122
*/
2223
final class CountQueryBuilder implements BuildsQuery
2324
{
24-
use HasConditions, OnDatabase, HasWhereQueryBuilderMethods;
25+
use HasConditions, OnDatabase, HasWhereQueryBuilderMethods, TransformsQueryBuilder;
2526

2627
private CountStatement $count;
2728

@@ -30,24 +31,31 @@ final class CountQueryBuilder implements BuildsQuery
3031
private ModelInspector $model;
3132

3233
/**
33-
* @param class-string<T>|string|T $model
34+
* @param class-string<TModel>|string|TModel $model
3435
*/
3536
public function __construct(string|object $model, ?string $column = null)
3637
{
37-
$this->model = model($model);
38+
$this->model = inspect($model);
3839

3940
$this->count = new CountStatement(
4041
table: $this->model->getTableDefinition(),
4142
column: $column,
4243
);
4344
}
4445

46+
/**
47+
* Executes the count query and returns the number of matching records.
48+
*/
4549
public function execute(mixed ...$bindings): int
4650
{
4751
return $this->build()->fetchFirst(...$bindings)[$this->count->getKey()];
4852
}
4953

50-
/** @return self<T> */
54+
/**
55+
* Modifies the count query to only count distinct values in the specified column.
56+
*
57+
* @return self<TModel>
58+
*/
5159
public function distinct(): self
5260
{
5361
if ($this->count->column === null || $this->count->column === '*') {
@@ -59,17 +67,32 @@ public function distinct(): self
5967
return $this;
6068
}
6169

62-
/** @return self<T> */
70+
/**
71+
* Binds the provided values to the query, allowing for parameterized queries.
72+
*
73+
* @return self<TModel>
74+
*/
6375
public function bind(mixed ...$bindings): self
6476
{
6577
$this->bindings = [...$this->bindings, ...$bindings];
6678

6779
return $this;
6880
}
6981

70-
public function toSql(): string
82+
/**
83+
* Compile the query to a SQL statement without the bindings.
84+
*/
85+
public function compile(): ImmutableString
86+
{
87+
return $this->build()->compile();
88+
}
89+
90+
/**
91+
* Returns the SQL statement with bindings. This method may generate syntax errors, it is not recommended to use it other than for debugging.
92+
*/
93+
public function toRawSql(): ImmutableString
7194
{
72-
return $this->build()->toSql();
95+
return $this->build()->toRawSql();
7396
}
7497

7598
public function build(mixed ...$bindings): Query

0 commit comments

Comments
 (0)