Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
6074d8a
feat(database): add more convenient `where` methods and support group…
innocenzi Aug 2, 2025
b033b39
fix(database): handle boolean serialization based on database dialect
innocenzi Aug 3, 2025
716e95a
feat(database): add `toRawSql` to query builders
innocenzi Aug 3, 2025
a403a06
test(core): accept stringable in `assertSameWithoutBackticks`
innocenzi Aug 3, 2025
352faed
test(database): ignore expected phpstan false positive
innocenzi Aug 3, 2025
31cf73f
refactor(database): put fixture in individual test files
innocenzi Aug 3, 2025
54323cc
refactor(database): rename `model` to `inspect`
innocenzi Aug 3, 2025
37017fa
feat(database): extract `IsDatabaseModel` in a model query builder
innocenzi Aug 4, 2025
605c74f
refactor(database): support custom primary keys and enforce their pre…
innocenzi Aug 4, 2025
c47178e
refactor(database): rename `Id` to `PrimaryKey` and extract it from `…
innocenzi Aug 4, 2025
1f35fac
feat(database): support custom primary keys when loading relationships
innocenzi Aug 4, 2025
bc2d7db
feat(database): add inline documentation on query builders
innocenzi Aug 4, 2025
c860de0
feat(database): add `orderBy` with field and direction support
innocenzi Aug 4, 2025
1692ef1
test(database): remove redundant assertions
innocenzi Aug 4, 2025
c198a2d
test(database): move legacy orm tests in database tests
innocenzi Aug 4, 2025
e1f4d2e
refactor(database): use consistent template naming in query builders
innocenzi Aug 4, 2025
fe9c966
feat(database): add `object` alias to `json` and `dto`
innocenzi Aug 4, 2025
99eb916
feat(database): add a standalone `foreignKey` method when creating ta…
innocenzi Aug 4, 2025
b73511c
feat(database): support nested data transfer object casting and seria…
innocenzi Aug 4, 2025
3c60c7d
refactor(database): improve typings on query builders
innocenzi Aug 4, 2025
ffb427d
refactor(mapper): clean up dto caster and serializer
innocenzi Aug 4, 2025
a2e184e
test(database): improve dto serialization coverage
innocenzi Aug 4, 2025
2058ecb
fix(database): implement `DatabaseException` on primary column except…
innocenzi Aug 4, 2025
4ddf13b
refactor(mapper): clean up array to object mapper
innocenzi Aug 4, 2025
4c3ac88
refactor(database): handle more primary key cases
innocenzi Aug 5, 2025
810ea9d
test(database): use proper relation on test model
innocenzi Aug 6, 2025
22dd611
feat(database): support inserting and updating relationships
innocenzi Aug 6, 2025
0176f66
fix(database): handle object serialization using `insert`
innocenzi Aug 6, 2025
27ff95e
fix(database): support insertion of relations using `create` on models
innocenzi Aug 6, 2025
bb68c86
fix(database): support dto serialization on model builder update
innocenzi Aug 6, 2025
9c21f77
test(datetime): reduce flakiness of time sensitive test
innocenzi Aug 6, 2025
837e931
feat(database): add `foreignId` alias to `belongsTo`
innocenzi Aug 6, 2025
500fc89
refactor(database): remove `resolve` query builder method
innocenzi Aug 7, 2025
0d34a2f
refactor(database): clean up pkey resolution
innocenzi Aug 7, 2025
9ecf8ec
refactor(database): remove more `resolve` calls
innocenzi Aug 7, 2025
7a7c07a
refactor(database): rename `toSql` to compile and extract raw sql com…
innocenzi Aug 7, 2025
63bd1b7
refactor(database): remove setter from `HasWhereStatements`
innocenzi Aug 7, 2025
f97a9f8
refactor(database): add back `resolve` to models
innocenzi Aug 7, 2025
c4eb32c
refactor(database): merge query builder and model query builder
innocenzi Aug 7, 2025
0922233
fix(database): fix default foreign key names
innocenzi Aug 7, 2025
ba4b5d5
refactor(database): support relation-only updates
innocenzi Aug 7, 2025
7b066d2
fix(database): handle uninitialized pkey edge case
innocenzi Aug 7, 2025
f814d79
refactor(database): rename `toSql` to `compile` in query builders
innocenzi Aug 7, 2025
437f82d
refactor(database): use Tempest reflection instead of built-in one
innocenzi Aug 7, 2025
708ef96
refactor(database): clean up update query builder
innocenzi Aug 7, 2025
b5d146d
feat(database): add `transform` to query builders
innocenzi Aug 7, 2025
218760d
fix(database): properly pass through generics to query builder traits
innocenzi Aug 7, 2025
2a34424
fix(database): support virtual properties
innocenzi Aug 10, 2025
ba68baf
refactor(database): support hybrid `where`
innocenzi Aug 11, 2025
fcaa7a8
fix(database): reload loaded relations when using `refresh`
innocenzi Aug 13, 2025
8d9a53e
test: remove redundant assertion
innocenzi Aug 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/auth/src/CanAuthenticate.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

namespace Tempest\Auth;

use Tempest\Database\Id;
use Tempest\Database\PrimaryKey;

interface CanAuthenticate
{
public ?Id $id {
public ?PrimaryKey $id {
get;
}
}
3 changes: 3 additions & 0 deletions packages/auth/src/Install/Permission.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

use BackedEnum;
use Tempest\Database\IsDatabaseModel;
use Tempest\Database\PrimaryKey;
use UnitEnum;

final class Permission
{
use IsDatabaseModel;

public PrimaryKey $id;

public function __construct(
public string $name,
) {}
Expand Down
7 changes: 6 additions & 1 deletion packages/auth/src/Install/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Tempest\Auth\CanAuthenticate;
use Tempest\Auth\CanAuthorize;
use Tempest\Database\IsDatabaseModel;
use Tempest\Database\PrimaryKey;
use UnitEnum;

use function Tempest\Support\arr;
Expand All @@ -17,6 +18,8 @@ final class User implements CanAuthenticate, CanAuthorize
{
use IsDatabaseModel;

public PrimaryKey $id;

public string $password;

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

$permission = Permission::select()->whereField('name', $name)->first();
$permission = Permission::select()
->where('name', $name)
->first();

return $permission ?? new Permission($name)->save();
}
Expand Down
3 changes: 3 additions & 0 deletions packages/auth/src/Install/UserPermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
namespace Tempest\Auth\Install;

use Tempest\Database\IsDatabaseModel;
use Tempest\Database\PrimaryKey;

final class UserPermission
{
use IsDatabaseModel;

public PrimaryKey $id;

public User $user;

public Permission $permission;
Expand Down
28 changes: 18 additions & 10 deletions packages/database/src/BelongsTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Attribute;
use Tempest\Database\Builder\ModelInspector;
use Tempest\Database\Exceptions\ModelDidNotHavePrimaryColumn;
use Tempest\Database\QueryStatements\FieldStatement;
use Tempest\Database\QueryStatements\JoinStatement;
use Tempest\Reflection\PropertyReflector;
Expand Down Expand Up @@ -46,14 +47,19 @@ public function getOwnerFieldName(): string
}
}

$relationModel = model($this->property->getType()->asClass());
$relationModel = inspect($this->property->getType()->asClass());
$primaryKey = $relationModel->getPrimaryKey();

return str($relationModel->getTableName())->singularizeLastWord() . '_' . $relationModel->getPrimaryKey();
if ($primaryKey === null) {
throw ModelDidNotHavePrimaryColumn::neededForRelation($relationModel->getName(), 'BelongsTo');
}

return str($relationModel->getTableName())->singularizeLastWord() . '_' . $primaryKey;
}

public function getSelectFields(): ImmutableArray
{
$relationModel = model($this->property->getType()->asClass());
$relationModel = inspect($this->property->getType()->asClass());

return $relationModel
->getSelectFields()
Expand All @@ -70,8 +76,8 @@ public function getSelectFields(): ImmutableArray

public function getJoinStatement(): JoinStatement
{
$relationModel = model($this->property->getType()->asClass());
$ownerModel = model($this->property->getClass());
$relationModel = inspect($this->property->getType()->asClass());
$ownerModel = inspect($this->property->getClass());

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

return sprintf(
'%s.%s',
$relationModel->getTableName(),
$relationModel->getPrimaryKey(),
);
$primaryKey = $relationModel->getPrimaryKey();

if ($primaryKey === null) {
throw ModelDidNotHavePrimaryColumn::neededForRelation($relationModel->getName(), 'BelongsTo');
}

return sprintf('%s.%s', $relationModel->getTableName(), $primaryKey);
}

private function getOwnerJoin(ModelInspector $ownerModel): string
Expand Down
64 changes: 54 additions & 10 deletions packages/database/src/Builder/ModelInspector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
use Tempest\Database\BelongsTo;
use Tempest\Database\Config\DatabaseConfig;
use Tempest\Database\Eager;
use Tempest\Database\Exceptions\ModelDidNotHavePrimaryColumn;
use Tempest\Database\Exceptions\ModelHadMultiplePrimaryColumns;
use Tempest\Database\HasMany;
use Tempest\Database\HasOne;
use Tempest\Database\Id;
use Tempest\Database\PrimaryKey;
use Tempest\Database\Relation;
use Tempest\Database\Table;
use Tempest\Database\Virtual;
Expand All @@ -21,7 +23,7 @@
use Tempest\Validation\SkipValidation;
use Tempest\Validation\Validator;

use function Tempest\Database\model;
use function Tempest\Database\inspect;
use function Tempest\get;
use function Tempest\Support\arr;
use function Tempest\Support\str;
Expand Down Expand Up @@ -292,7 +294,7 @@ public function resolveRelations(string $relationString, string $parent = ''): a

unset($relationNames[0]);

$relationModel = model($currentRelation);
$relationModel = inspect($currentRelation);

$newRelationString = implode('.', $relationNames);
$currentRelation->setParent($parent);
Expand Down Expand Up @@ -334,7 +336,7 @@ public function resolveEagerRelations(string $parent = ''): array
$currentRelationName,
), '.');

foreach (model($currentRelation)->resolveEagerRelations($newParent) as $name => $nestedEagerRelation) {
foreach (inspect($currentRelation)->resolveEagerRelations($newParent) as $name => $nestedEagerRelation) {
$relations[$name] = $nestedEagerRelation;
}
}
Expand All @@ -357,6 +359,10 @@ public function validate(mixed ...$data): void
continue;
}

if ($property->getType()->getName() === PrimaryKey::class) {
continue;
}

$failingRulesForProperty = $this->validator->validateValueForProperty(
$property,
$value,
Expand All @@ -381,17 +387,45 @@ public function getName(): string
return $this->instance;
}

public function getPrimaryFieldName(): string
public function getQualifiedPrimaryKey(): ?string
{
return $this->getTableDefinition()->name . '.' . $this->getPrimaryKey();
$primaryKey = $this->getPrimaryKey();

return $primaryKey !== null
? ($this->getTableDefinition()->name . '.' . $primaryKey)
: null;
}

public function getPrimaryKey(): string
public function getPrimaryKey(): ?string
{
return 'id';
return $this->getPrimaryKeyProperty()?->getName();
}

public function hasPrimaryKey(): bool
{
return $this->getPrimaryKeyProperty() !== null;
}

public function getPrimaryKeyProperty(): ?PropertyReflector
{
if (! $this->isObjectModel()) {
return null;
}

$primaryKeys = arr($this->reflector->getProperties())
->filter(fn (PropertyReflector $property) => $property->getType()->matches(PrimaryKey::class));

return match ($primaryKeys->count()) {
0 => null,
1 => $primaryKeys->first(),
default => throw ModelHadMultiplePrimaryColumns::found(
model: $this->model,
properties: $primaryKeys->map(fn (PropertyReflector $property) => $property->getName())->toArray(),
),
};
}

public function getPrimaryKeyValue(): ?Id
public function getPrimaryKeyValue(): ?PrimaryKey
{
if (! $this->isObjectModel()) {
return null;
Expand All @@ -401,6 +435,16 @@ public function getPrimaryKeyValue(): ?Id
return null;
}

return $this->instance->{$this->getPrimaryKey()};
$primaryKeyProperty = $this->getPrimaryKeyProperty();

if ($primaryKeyProperty === null) {
return null;
}

if (! $primaryKeyProperty->isInitialized($this->instance)) {
return null;
}

return $primaryKeyProperty->getValue($this->instance);
}
}
4 changes: 2 additions & 2 deletions packages/database/src/Builder/QueryBuilders/BuildsQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
use Tempest\Database\Query;

/**
* @template TModelClass
* @template TModel
*/
interface BuildsQuery
{
public function build(mixed ...$bindings): Query;

/** @return self<TModelClass> */
/** @return self<TModel> */
public function bind(mixed ...$bindings): self;
}
45 changes: 34 additions & 11 deletions packages/database/src/Builder/QueryBuilders/CountQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@
use Tempest\Database\QueryStatements\CountStatement;
use Tempest\Database\QueryStatements\HasWhereStatements;
use Tempest\Support\Conditions\HasConditions;
use Tempest\Support\Str\ImmutableString;

use function Tempest\Database\model;
use function Tempest\Database\inspect;

/**
* @template T of object
* @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery<T>
* @uses \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods<T>
* @template TModel of object
* @implements \Tempest\Database\Builder\QueryBuilders\BuildsQuery<TModel>
* @use \Tempest\Database\Builder\QueryBuilders\HasWhereQueryBuilderMethods<TModel>
*/
final class CountQueryBuilder implements BuildsQuery
{
use HasConditions, OnDatabase, HasWhereQueryBuilderMethods;
use HasConditions, OnDatabase, HasWhereQueryBuilderMethods, TransformsQueryBuilder;

private CountStatement $count;

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

/**
* @param class-string<T>|string|T $model
* @param class-string<TModel>|string|TModel $model
*/
public function __construct(string|object $model, ?string $column = null)
{
$this->model = model($model);
$this->model = inspect($model);

$this->count = new CountStatement(
table: $this->model->getTableDefinition(),
column: $column,
);
}

/**
* Executes the count query and returns the number of matching records.
*/
public function execute(mixed ...$bindings): int
{
return $this->build()->fetchFirst(...$bindings)[$this->count->getKey()];
}

/** @return self<T> */
/**
* Modifies the count query to only count distinct values in the specified column.
*
* @return self<TModel>
*/
public function distinct(): self
{
if ($this->count->column === null || $this->count->column === '*') {
Expand All @@ -59,17 +67,32 @@ public function distinct(): self
return $this;
}

/** @return self<T> */
/**
* Binds the provided values to the query, allowing for parameterized queries.
*
* @return self<TModel>
*/
public function bind(mixed ...$bindings): self
{
$this->bindings = [...$this->bindings, ...$bindings];

return $this;
}

public function toSql(): string
/**
* Compile the query to a SQL statement without the bindings.
*/
public function compile(): ImmutableString
{
return $this->build()->compile();
}

/**
* Returns the SQL statement with bindings. This method may generate syntax errors, it is not recommended to use it other than for debugging.
*/
public function toRawSql(): ImmutableString
{
return $this->build()->toSql();
return $this->build()->toRawSql();
}

public function build(mixed ...$bindings): Query
Expand Down
Loading
Loading