Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions packages/database/src/Builder/QueryBuilders/BuildsQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Tempest\Database\Builder\ModelInspector;
use Tempest\Database\Query;
use UnitEnum;

/**
* @template TModel
Expand All @@ -26,6 +27,13 @@ interface BuildsQuery
get;
}

/**
* The database tag for targeting a specific database connection.
*/
public null|string|UnitEnum $onDatabase {
get;
}

/**
* Creates a {@see Query} instance with the specified optional bindings.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $sou
}
}

$builder->onDatabase = $source->onDatabase;

/** @var CountQueryBuilder<TSourceModel> $builder */
return $builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $sou
$builder->appendWhere($where);
}

$builder->onDatabase = $source->onDatabase;

/** @var DeleteQueryBuilder<TSourceModel> $builder */
return $builder;
}
Expand Down
22 changes: 12 additions & 10 deletions packages/database/src/Builder/QueryBuilders/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Tempest\Database\Builder\QueryBuilders;

use Tempest\Database\Exceptions\ModelDidNotHavePrimaryColumn;
use Tempest\Database\OnDatabase;
use Tempest\Database\PrimaryKey;
use Tempest\Mapper\SerializerFactory;

Expand All @@ -15,13 +16,15 @@
/**
* @template TModel of object
*/
final readonly class QueryBuilder
final class QueryBuilder
{
use OnDatabase;

/**
* @param class-string<TModel>|string|TModel $model
*/
public function __construct(
private string|object $model,
private readonly string|object $model,
) {}

/**
Expand All @@ -41,7 +44,7 @@ public function select(string ...$columns): SelectQueryBuilder
return new SelectQueryBuilder(
model: $this->model,
fields: $columns !== [] ? arr($columns)->unique() : null,
);
)->onDatabase($this->onDatabase);
}

/**
Expand All @@ -66,7 +69,7 @@ public function insert(mixed ...$values): InsertQueryBuilder
model: $this->model,
rows: $values,
serializerFactory: get(SerializerFactory::class),
);
)->onDatabase($this->onDatabase);
}

/**
Expand All @@ -88,7 +91,7 @@ public function update(mixed ...$values): UpdateQueryBuilder
model: $this->model,
values: $values,
serializerFactory: get(SerializerFactory::class),
);
)->onDatabase($this->onDatabase);
}

/**
Expand All @@ -106,7 +109,7 @@ public function update(mixed ...$values): UpdateQueryBuilder
*/
public function delete(): DeleteQueryBuilder
{
return new DeleteQueryBuilder($this->model);
return new DeleteQueryBuilder($this->model)->onDatabase($this->onDatabase);
}

/**
Expand All @@ -124,7 +127,7 @@ public function count(?string $column = null): CountQueryBuilder
return new CountQueryBuilder(
model: $this->model,
column: $column,
);
)->onDatabase($this->onDatabase);
}

/**
Expand Down Expand Up @@ -255,9 +258,7 @@ public function create(mixed ...$params): object

$model = $this->new(...$params);

$id = query($this->model)
->insert($model)
->execute();
$id = $this->insert($model)->execute();

$inspector = inspect($this->model);
$primaryKeyProperty = $inspector->getPrimaryKeyProperty();
Expand Down Expand Up @@ -338,6 +339,7 @@ public function updateOrCreate(array $find, array $update): object
}

query($model)
->onDatabase($this->onDatabase)
->update(...$update)
->execute();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $sou
}
}

$builder->onDatabase = $source->onDatabase;

/** @var SelectQueryBuilder<TSourceModel> $builder */
return $builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public static function fromQueryBuilder(BuildsQuery&SupportsWhereStatements $sou
$builder->appendWhere($where);
}

$builder->onDatabase = $source->onDatabase;

/** @var UpdateQueryBuilder<TSourceModel> $builder */
return $builder;
}
Expand Down
41 changes: 38 additions & 3 deletions packages/database/src/IsDatabaseModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Tempest\Reflection\PropertyReflector;
use Tempest\Router\IsBindingValue;
use Tempest\Validation\SkipValidation;
use UnitEnum;

use function Tempest\Support\arr;
use function Tempest\Support\str;
Expand All @@ -22,6 +23,31 @@ trait IsDatabaseModel
#[IsBindingValue, SkipValidation]
public PrimaryKey $id;

#[SkipValidation, Virtual]
private null|string|UnitEnum $onDatabase = null;

/**
* Returns a query builder targeting the specified database connection.
*
* @return QueryBuilder<static>
*/
public static function on(null|string|UnitEnum $databaseTag): QueryBuilder
{
return static::queryBuilder()->onDatabase(databaseTag: $databaseTag);
}

/**
* Targets a specific database connection for this model instance.
*/
public function onDatabase(null|string|UnitEnum $databaseTag): static
{
$clone = clone $this;

$clone->onDatabase = $databaseTag;

return $clone;
}

/**
* @return QueryBuilder<static>
*/
Expand Down Expand Up @@ -185,7 +211,9 @@ public function refresh(): static
$primaryKeyProperty = $model->getPrimaryKeyProperty();
$primaryKeyValue = $primaryKeyProperty->getValue($this);

$new = static::select()
$new = static::queryBuilder()
->onDatabase($this->onDatabase)
->select()
->with(...$loadedRelations->map(fn (Relation $relation) => $relation->name))
->get($primaryKeyValue);

Expand Down Expand Up @@ -216,7 +244,9 @@ public function load(string ...$relations): static
$primaryKeyProperty = $model->getPrimaryKeyProperty();
$primaryKeyValue = $primaryKeyProperty->getValue($this);

$new = static::get($primaryKeyValue, $relations);
$new = static::queryBuilder()
->onDatabase($this->onDatabase)
->get($primaryKeyValue, $relations);

$fieldsToUpdate = arr($relations)
->map(fn (string $relation) => str($relation)->before('.')->toString())
Expand All @@ -240,6 +270,7 @@ public function save(): static
// Models without primary keys always insert
if (! $model->hasPrimaryKey()) {
query($this::class)
->onDatabase($this->onDatabase)
->insert($this)
->execute();

Expand All @@ -254,6 +285,7 @@ public function save(): static
// to generate the id and populate the model instance with it
if ($primaryKeyValue === null) {
$id = query($this::class)
->onDatabase($this->onDatabase)
->insert($this)
->execute();

Expand All @@ -264,8 +296,9 @@ public function save(): static
return $this;
}

// Is the model was already save, we update it
// Is the model was already saved, we update it
query($this)
->onDatabase($this->onDatabase)
->update(...inspect($this)->getPropertyValues())
->execute();

Expand All @@ -282,6 +315,7 @@ public function update(mixed ...$params): static
$model->validate(...$params);

query($this)
->onDatabase($this->onDatabase)
->update(...$params)
->whereField($model->getPrimaryKey(), $model->getPrimaryKeyValue())
->execute();
Expand All @@ -299,6 +333,7 @@ public function update(mixed ...$params): static
public function delete(): void
{
query($this)
->onDatabase($this->onDatabase)
->delete()
->build()
->execute();
Expand Down
101 changes: 101 additions & 0 deletions tests/Integration/Database/Builder/IsDatabaseModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use DateTime as NativeDateTime;
use DateTimeImmutable;
use Tempest\Database\BelongsTo;
use Tempest\Database\Builder\QueryBuilders\QueryBuilder;
use Tempest\Database\Exceptions\DeleteStatementWasInvalid;
use Tempest\Database\Exceptions\RelationWasMissing;
use Tempest\Database\Exceptions\ValueWasMissing;
Expand Down Expand Up @@ -694,6 +695,100 @@ public function test_nullable_relation_save(): void
$this->assertNotNull($a->b);
$this->assertSame('b', $a->b->name);
}

public function test_on_returns_query_builder_with_database_tag(): void
{
$builder = Foo::on('analytics');

$this->assertInstanceOf(QueryBuilder::class, $builder);
$this->assertSame('analytics', $builder->onDatabase);
}

public function test_on_propagates_tag_to_select_builder(): void
{
$selectBuilder = Foo::on('analytics')->select();

$this->assertSame('analytics', $selectBuilder->onDatabase);
}

public function test_on_propagates_tag_to_insert_builder(): void
{
$insertBuilder = Foo::on('analytics')->insert();

$this->assertSame('analytics', $insertBuilder->onDatabase);
}

public function test_on_propagates_tag_to_count_builder(): void
{
$countBuilder = Foo::on('analytics')->count();

$this->assertSame('analytics', $countBuilder->onDatabase);
}

public function test_on_propagates_tag_to_delete_builder(): void
{
$deleteBuilder = Foo::on('analytics')->delete();

$this->assertSame('analytics', $deleteBuilder->onDatabase);
}

public function test_on_with_null_sets_null_tag(): void
{
$builder = Foo::on(null);

$this->assertNull($builder->onDatabase);
}

public function test_on_with_enum_tag(): void
{
$builder = Foo::on(TestDatabaseTag::Analytics);

$this->assertSame(TestDatabaseTag::Analytics, $builder->onDatabase);
}

public function test_on_database_returns_clone(): void
{
$this->database->migrate(
CreateMigrationsTable::class,
FooDatabaseMigration::class,
);

$foo = Foo::create(bar: 'baz');
$clone = $foo->onDatabase('analytics');

$this->assertNotSame($foo, $clone);
}

public function test_on_database_does_not_mutate_original(): void
{
$this->database->migrate(
CreateMigrationsTable::class,
FooDatabaseMigration::class,
);

$foo = Foo::create(bar: 'baz');
$foo->onDatabase('analytics');

// Original still works against default database
$foo->update(bar: 'updated');
$refreshed = Foo::get($foo->id);

$this->assertSame('updated', $refreshed->bar);
}

public function test_on_database_preserves_model_data(): void
{
$this->database->migrate(
CreateMigrationsTable::class,
FooDatabaseMigration::class,
);

$foo = Foo::create(bar: 'baz');
$clone = $foo->onDatabase('analytics');

$this->assertSame('baz', $clone->bar);
$this->assertEquals($foo->id, $clone->id);
}
}

final class Foo
Expand Down Expand Up @@ -1117,3 +1212,9 @@ final class ModelWithHookedVirtualProperty
get => strtoupper($this->name);
}
}

enum TestDatabaseTag
{
case Analytics;
case Reporting;
}
Loading