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
30 changes: 30 additions & 0 deletions packages/database/src/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,55 @@

use Tempest\Database\Builder\QueryBuilders\BuildsQuery;
use Tempest\Database\Config\DatabaseDialect;
use Tempest\Support\Str\ImmutableString;
use UnitEnum;

/**
* Represents a database that can execute queries.
*/
interface Database
{
/**
* The dialect of this database.
*/
public DatabaseDialect $dialect {
get;
}

/**
* The tag associated with this database, if any.
*/
public null|string|UnitEnum $tag {
get;
}

/**
* Executes the given query.
*/
public function execute(BuildsQuery|Query $query): void;

/**
* Returns the last inserted primary key, if any.
*/
public function getLastInsertId(): ?PrimaryKey;

/**
* Fetches all results for the given query.
*/
public function fetch(BuildsQuery|Query $query): array;

/**
* Fetches the first result for the given query.
*/
public function fetchFirst(BuildsQuery|Query $query): ?array;

/**
* Executes the given callback within a transaction.
*/
public function withinTransaction(callable $callback): bool;

/**
* Returns the raw SQL representation of the given query for debugging purposes.
*/
public function getRawSql(Query $query): ImmutableString;
}
11 changes: 11 additions & 0 deletions packages/database/src/GenericDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Tempest\Database\Exceptions\QueryWasInvalid;
use Tempest\Database\Transactions\TransactionManager;
use Tempest\Mapper\SerializerFactory;
use Tempest\Support\Str\ImmutableString;
use Throwable;
use UnitEnum;

Expand Down Expand Up @@ -127,6 +128,16 @@ public function withinTransaction(callable $callback): bool
return true;
}

public function getRawSql(Query $query): ImmutableString
{
return new RawSql(
dialect: $this->dialect,
sql: (string) $query->compile(),
bindings: $query->bindings,
serializerFactory: $this->serializerFactory,
)->toImmutableString();
}

private function resolveBindings(Query $query): array
{
$bindings = [];
Expand Down
5 changes: 4 additions & 1 deletion packages/database/src/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

use function Tempest\get;

/**
* A database query that can be executed.
*/
final class Query
{
use OnDatabase;
Expand Down Expand Up @@ -84,7 +87,7 @@ public function compile(): ImmutableString
*/
public function toRawSql(): ImmutableString
{
return new RawSql($this->dialect, (string) $this->compile(), $this->bindings)->toImmutableString();
return $this->database->getRawSql($this);
}

public function append(string $append): self
Expand Down
51 changes: 18 additions & 33 deletions packages/database/src/RawSql.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

namespace Tempest\Database;

use BackedEnum;
use Tempest\Database\Config\DatabaseDialect;
use Tempest\Mapper\SerializerFactory;
use Tempest\Support\Str\ImmutableString;
use UnitEnum;

final class RawSql
{
private ?RawSqlDatabaseContext $context {
get => $this->context ??= new RawSqlDatabaseContext($this->dialect);
}

public function __construct(
private(set) DatabaseDialect $dialect,
private(set) string $sql,
private(set) array $bindings,
private SerializerFactory $serializerFactory,
) {}

public function compile(): string
Expand All @@ -39,9 +43,11 @@ public function __toString(): string
private function replaceNamedBindings(string $sql, array $bindings): string
{
foreach ($bindings as $key => $value) {
$placeholder = ':' . $key;
$formattedValue = $this->formatValueForSql($value);
$sql = str_replace($placeholder, $formattedValue, $sql);
$sql = str_replace(
search: ':' . $key,
replace: $this->formatValueForSql($value),
subject: $sql,
);
}

return $sql;
Expand Down Expand Up @@ -71,15 +77,14 @@ private function resolveBindingsForDisplay(): array
$bindings = [];

foreach ($this->bindings as $key => $value) {
if (is_bool($value)) {
$value = match ($this->dialect) {
DatabaseDialect::POSTGRESQL => $value ? 'true' : 'false',
default => $value ? '1' : '0',
};
if ($value instanceof Query) {
$bindings[$key] = "({$value->toRawSql()})";
continue;
}

if ($value instanceof Query) {
$value = '(' . $value->toRawSql() . ')';
if ($serializer = $this->serializerFactory->in($this->context)->forValue($value)) {
$bindings[$key] = $serializer->serialize($value);
continue;
}

$bindings[$key] = $value;
Expand All @@ -94,26 +99,6 @@ private function formatValueForSql(mixed $value): string
return 'NULL';
}

if (is_string($value)) {
if (str_starts_with($value, '(') && str_ends_with($value, ')')) {
return $value;
}

return "'" . str_replace("'", "''", $value) . "'";
}

if (is_numeric($value)) {
return (string) $value;
}

if ($value instanceof BackedEnum) {
return $value->value;
}

if ($value instanceof UnitEnum) {
return $value->name;
}

return "'" . str_replace("'", "''", (string) $value) . "'";
return (string) $value;
}
}
15 changes: 15 additions & 0 deletions packages/database/src/RawSqlDatabaseContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Tempest\Database;

use Tempest\Database\Config\DatabaseDialect;
use Tempest\Mapper\Context;

final class RawSqlDatabaseContext implements Context
{
private(set) string $name = self::class;

public function __construct(
private(set) DatabaseDialect $dialect,
) {}
}
2 changes: 2 additions & 0 deletions packages/database/src/Serializers/DateTimeSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Tempest\Database\Serializers;

use DateTimeInterface as NativeDateTimeInterface;
use Tempest\Core\Priority;
use Tempest\Database\DatabaseContext;
use Tempest\DateTime\DateTime;
use Tempest\DateTime\DateTimeInterface;
Expand All @@ -16,6 +17,7 @@
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\TypeReflector;

#[Priority(Priority::HIGH)]
#[Context(DatabaseContext::class)]
final readonly class DateTimeSerializer implements Serializer, DynamicSerializer
{
Expand Down
43 changes: 43 additions & 0 deletions packages/database/src/Serializers/RawSqlBooleanSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Tempest\Database\Serializers;

use Tempest\Database\Config\DatabaseDialect;
use Tempest\Database\RawSqlDatabaseContext;
use Tempest\Mapper\Attributes\Context;
use Tempest\Mapper\DynamicSerializer;
use Tempest\Mapper\Exceptions\ValueCouldNotBeSerialized;
use Tempest\Mapper\Serializer;
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\TypeReflector;

#[Context(RawSqlDatabaseContext::class)]
final class RawSqlBooleanSerializer implements Serializer, DynamicSerializer
{
public function __construct(
private RawSqlDatabaseContext $context,
) {}

public static function accepts(PropertyReflector|TypeReflector $type): bool
{
$type = $type instanceof PropertyReflector
? $type->getType()
: $type;

return $type->getName() === 'bool' || $type->getName() === 'boolean';
}

public function serialize(mixed $input): string
{
if (! is_bool($input)) {
throw new ValueCouldNotBeSerialized('boolean');
}

return match ($this->context->dialect) {
DatabaseDialect::POSTGRESQL => $input ? 'true' : 'false',
default => $input ? '1' : '0',
};
}
}
45 changes: 45 additions & 0 deletions packages/database/src/Serializers/RawSqlDateTimeSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Tempest\Database\Serializers;

use DateTimeInterface as NativeDateTimeInterface;
use Tempest\Core\Priority;
use Tempest\Database\RawSqlDatabaseContext;
use Tempest\DateTime\DateTime;
use Tempest\DateTime\DateTimeInterface;
use Tempest\DateTime\FormatPattern;
use Tempest\Mapper\Attributes\Context;
use Tempest\Mapper\DynamicSerializer;
use Tempest\Mapper\Exceptions\ValueCouldNotBeSerialized;
use Tempest\Mapper\Serializer;
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\TypeReflector;

#[Priority(Priority::HIGH)]
#[Context(RawSqlDatabaseContext::class)]
final class RawSqlDateTimeSerializer implements Serializer, DynamicSerializer
{
public static function accepts(PropertyReflector|TypeReflector $type): bool
{
$type = $type instanceof PropertyReflector
? $type->getType()
: $type;

return $type->matches(DateTime::class) || $type->matches(DateTimeInterface::class) || $type->matches(NativeDateTimeInterface::class);
}

public function serialize(mixed $input): string
{
if ($input instanceof NativeDateTimeInterface) {
$input = DateTime::parse($input);
}

if (! $input instanceof DateTimeInterface) {
throw new ValueCouldNotBeSerialized(DateTimeInterface::class);
}

return "'" . $input->format(FormatPattern::SQL_DATE_TIME) . "'";
}
}
43 changes: 43 additions & 0 deletions packages/database/src/Serializers/RawSqlEnumSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Tempest\Database\Serializers;

use BackedEnum;
use Tempest\Core\Priority;
use Tempest\Database\RawSqlDatabaseContext;
use Tempest\Mapper\Attributes\Context;
use Tempest\Mapper\DynamicSerializer;
use Tempest\Mapper\Exceptions\ValueCouldNotBeSerialized;
use Tempest\Mapper\Serializer;
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\TypeReflector;
use UnitEnum;

#[Priority(Priority::NORMAL)]
#[Context(RawSqlDatabaseContext::class)]
final class RawSqlEnumSerializer implements Serializer, DynamicSerializer
{
public static function accepts(PropertyReflector|TypeReflector $input): bool
{
$type = $input instanceof PropertyReflector
? $input->getType()
: $input;

return $type->matches(UnitEnum::class);
}

public function serialize(mixed $input): string
{
if ($input instanceof BackedEnum) {
return (string) $input->value;
}

if ($input instanceof UnitEnum) {
return $input->name;
}

throw new ValueCouldNotBeSerialized('enum');
}
}
37 changes: 37 additions & 0 deletions packages/database/src/Serializers/RawSqlNumberSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Tempest\Database\Serializers;

use Tempest\Core\Priority;
use Tempest\Database\RawSqlDatabaseContext;
use Tempest\Mapper\Attributes\Context;
use Tempest\Mapper\DynamicSerializer;
use Tempest\Mapper\Exceptions\ValueCouldNotBeSerialized;
use Tempest\Mapper\Serializer;
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\TypeReflector;

#[Priority(Priority::NORMAL)]
#[Context(RawSqlDatabaseContext::class)]
final class RawSqlNumberSerializer implements Serializer, DynamicSerializer
{
public static function accepts(PropertyReflector|TypeReflector $input): bool
{
$type = $input instanceof PropertyReflector
? $input->getType()
: $input;

return in_array($type->getName(), ['int', 'integer', 'float', 'double'], strict: true);
}

public function serialize(mixed $input): string
{
if (! is_int($input) && ! is_float($input)) {
throw new ValueCouldNotBeSerialized('integer or float');
}

return (string) $input;
}
}
Loading