diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1c5326f7a..645adc9e6 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -5,6 +5,10 @@ on: workflow_dispatch: schedule: - cron: '0 0 * * *' +env: + POSTGRES_USER: runner + POSTGRES_PASSWORD: '' + POSTGRES_DB: postgres jobs: vitest: @@ -28,7 +32,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, pcntl, fileinfo, pdo, sqlite, pdo_sqlite, pdo_mysql, intl, ftp, zip + extensions: dom, curl, libxml, mbstring, pcntl, fileinfo, pdo, sqlite, pdo_sqlite, pdo_mysql, pdo_pgsql intl, ftp, zip coverage: pcov - name: Setup Bun @@ -60,6 +64,9 @@ jobs: stability: - prefer-stable - prefer-lowest + exclude: + - os: windows-latest + database: postgres name: "Run tests: PHP ${{ matrix.php }} - ${{ matrix.database }} - ${{ matrix.stability }} - ${{ matrix.os }}" @@ -94,7 +101,10 @@ jobs: uses: ankane/setup-postgres@v1 - name: Set database config - ${{ matrix.database }} - run: php -r "file_exists('tests/Fixtures/Config/database.config.php') || copy('tests/Fixtures/Config/database.${{ matrix.database }}.php', 'tests/Fixtures/Config/database.config.php');" + run: php -r "copy('tests/Fixtures/Config/database.${{ matrix.database }}.php', 'tests/Fixtures/Config/database.config.php');" + + - name: Tempest about + run: php ./tempest about - name: List discovered locations run: php ./tempest discovery:status diff --git a/packages/container/src/GenericContainer.php b/packages/container/src/GenericContainer.php index a19a7a6fb..65fb42441 100644 --- a/packages/container/src/GenericContainer.php +++ b/packages/container/src/GenericContainer.php @@ -30,7 +30,7 @@ public function __construct( private ArrayIterator $singletons = new ArrayIterator(), /** @var ArrayIterator $initializers */ - private ArrayIterator $initializers = new ArrayIterator(), + public ArrayIterator $initializers = new ArrayIterator(), /** @var ArrayIterator $dynamicInitializers */ private ArrayIterator $dynamicInitializers = new ArrayIterator(), diff --git a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php index 4455aa707..0d5961f3f 100644 --- a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php +++ b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php @@ -34,7 +34,7 @@ public function __construct( ); } - public function execute(mixed ...$bindings): Id + public function execute(mixed ...$bindings): ?Id { return $this->build()->execute(...$bindings); } diff --git a/packages/database/src/Config/DatabaseDialect.php b/packages/database/src/Config/DatabaseDialect.php index 9f7f8b183..c2d3209be 100644 --- a/packages/database/src/Config/DatabaseDialect.php +++ b/packages/database/src/Config/DatabaseDialect.php @@ -4,6 +4,8 @@ namespace Tempest\Database\Config; +use PDOException; + enum DatabaseDialect: string { case SQLITE = 'sqlite'; @@ -18,4 +20,13 @@ public function tableNotFoundCode(): string self::SQLITE => 'HY000', }; } + + public function isTableNotFoundError(PDOException $exception): bool + { + return match ($this) { + self::MYSQL => $exception->getCode() === '42S02' && str_contains($exception->getMessage(), 'table'), + self::SQLITE => $exception->getCode() === 'HY000' && str_contains($exception->getMessage(), 'table'), + self::POSTGRESQL => $exception->getCode() === '42P01' && str_contains($exception->getMessage(), 'relation'), + }; + } } diff --git a/packages/database/src/Config/PostgresConfig.php b/packages/database/src/Config/PostgresConfig.php index 24c2756a9..13b447f4c 100644 --- a/packages/database/src/Config/PostgresConfig.php +++ b/packages/database/src/Config/PostgresConfig.php @@ -32,7 +32,7 @@ public function __construct( #[SensitiveParameter] public string $port = '5432', #[SensitiveParameter] - public string $username = '', + public string $username = 'postgres', #[SensitiveParameter] public string $password = '', #[SensitiveParameter] diff --git a/packages/database/src/Connection/CachedConnectionInitializer.php b/packages/database/src/Connection/CachedConnectionInitializer.php deleted file mode 100644 index 45706493b..000000000 --- a/packages/database/src/Connection/CachedConnectionInitializer.php +++ /dev/null @@ -1,35 +0,0 @@ -initializer->initialize($container); - - return self::$instance; - } -} diff --git a/packages/database/src/Connection/Connection.php b/packages/database/src/Connection/Connection.php index e3a590f8a..afcb58aab 100644 --- a/packages/database/src/Connection/Connection.php +++ b/packages/database/src/Connection/Connection.php @@ -16,7 +16,7 @@ public function rollback(): bool; public function lastInsertId(): false|string; - public function prepare(string $sql): false|PDOStatement; + public function prepare(string $sql): PDOStatement; public function close(): void; diff --git a/packages/database/src/Connection/PDOConnection.php b/packages/database/src/Connection/PDOConnection.php index 22edbc94b..a4b9bf696 100644 --- a/packages/database/src/Connection/PDOConnection.php +++ b/packages/database/src/Connection/PDOConnection.php @@ -8,6 +8,7 @@ use PDOStatement; use Tempest\Database\Config\DatabaseConfig; use Tempest\Database\Exceptions\ConnectionClosed; +use Throwable; final class PDOConnection implements Connection { @@ -53,13 +54,37 @@ public function lastInsertId(): false|string return $this->pdo->lastInsertId(); } - public function prepare(string $sql): false|PDOStatement + public function prepare(string $sql): PDOStatement { if ($this->pdo === null) { throw new ConnectionClosed(); } - return $this->pdo->prepare($sql); + $statement = $this->pdo->prepare($sql); + + if ($statement === false) { + throw new ConnectionClosed(); + } + + return $statement; + } + + public function ping(): bool + { + try { + $statement = $this->prepare('SELECT 1'); + $statement->execute(); + + return true; + } catch (Throwable) { + return false; + } + } + + public function reconnect(): void + { + $this->close(); + $this->connect(); } public function close(): void @@ -74,9 +99,9 @@ public function connect(): void } $this->pdo = new PDO( - $this->config->dsn, - $this->config->username, - $this->config->password, + dsn: $this->config->dsn, + username: $this->config->username, + password: $this->config->password, ); } } diff --git a/packages/database/src/Database.php b/packages/database/src/Database.php index 27e7053e8..241d6ae0d 100644 --- a/packages/database/src/Database.php +++ b/packages/database/src/Database.php @@ -8,7 +8,7 @@ interface Database { public function execute(Query $query): void; - public function getLastInsertId(): Id; + public function getLastInsertId(): ?Id; public function fetch(Query $query): array; diff --git a/packages/database/src/DatabaseDialectInitializer.php b/packages/database/src/DatabaseDialectInitializer.php new file mode 100644 index 000000000..45ccda28b --- /dev/null +++ b/packages/database/src/DatabaseDialectInitializer.php @@ -0,0 +1,16 @@ +get(DatabaseConfig::class)->dialect; + } +} diff --git a/packages/database/src/DatabaseInitializer.php b/packages/database/src/DatabaseInitializer.php index 424555b0b..cdada320e 100644 --- a/packages/database/src/DatabaseInitializer.php +++ b/packages/database/src/DatabaseInitializer.php @@ -37,6 +37,7 @@ public function initialize(ClassReflector $class, ?string $tag, Container $conta return new GenericDatabase( $connection, new GenericTransactionManager($connection), + $container->get(DatabaseConfig::class)->dialect, ); } } diff --git a/packages/database/src/Exceptions/NoLastInsertIdAvailable.php b/packages/database/src/Exceptions/NoLastInsertIdAvailable.php new file mode 100644 index 000000000..4e641d315 --- /dev/null +++ b/packages/database/src/Exceptions/NoLastInsertIdAvailable.php @@ -0,0 +1,13 @@ +resolveBindings($query); try { - $this->connection - ->prepare($query->toSql()) - ->execute($bindings); + foreach (explode(';', $query->toSql()) as $sql) { + if (! trim($sql)) { + continue; + } + + $statement = $this->connection->prepare($sql . ';'); + + $statement->execute($bindings); + + $this->lastStatement = $statement; + $this->lastQuery = $query; + } } catch (PDOException $pdoException) { throw new QueryException($query, $bindings, $pdoException); } } - public function getLastInsertId(): Id + public function getLastInsertId(): ?Id { - return new Id($this->connection->lastInsertId()); + $sql = $this->lastQuery->toSql(); + + // TODO: properly determine whether a query is an insert or not + if (! str_starts_with($sql, 'INSERT')) { + return null; + } + + if ($this->dialect === DatabaseDialect::POSTGRESQL) { + $data = $this->lastStatement->fetch(PDO::FETCH_ASSOC); + $lastInsertId = $data['id'] ?? null; + } else { + $lastInsertId = $this->connection->lastInsertId(); + } + + return Id::tryFrom($lastInsertId); } public function fetch(Query $query): array { + $bindings = $this->resolveBindings($query); + $pdoQuery = $this->connection->prepare($query->toSql()); - $pdoQuery->execute($this->resolveBindings($query)); + $pdoQuery->execute($bindings); return $pdoQuery->fetchAll(PDO::FETCH_NAMED); } @@ -60,8 +91,6 @@ public function withinTransaction(callable $callback): bool $callback(); $this->transactionManager->commit(); - } catch (PDOException) { - return false; } catch (Throwable) { $this->transactionManager->rollback(); diff --git a/packages/database/src/Id.php b/packages/database/src/Id.php index 633fcb59c..7d74aadc7 100644 --- a/packages/database/src/Id.php +++ b/packages/database/src/Id.php @@ -13,6 +13,15 @@ { public string|int $id; + public static function tryFrom(string|int|self|null $id): ?self + { + if ($id === null) { + return null; + } + + return new self($id); + } + public function __construct(string|int|self $id) { $id = ($id instanceof self) ? $id->id : $id; diff --git a/packages/database/src/IsDatabaseModel.php b/packages/database/src/IsDatabaseModel.php index bb9eb8a9b..fd17764f8 100644 --- a/packages/database/src/IsDatabaseModel.php +++ b/packages/database/src/IsDatabaseModel.php @@ -72,11 +72,15 @@ public static function create(mixed ...$params): self $model = self::new(...$params); - $model->id = query(self::class) + $id = query(self::class) ->insert($model) ->build() ->execute(); + if ($id !== null) { + $model->id = new Id($id); + } + return $model; } diff --git a/packages/database/src/Migrations/MigrationManager.php b/packages/database/src/Migrations/MigrationManager.php index fb14929fb..48ccbcb74 100644 --- a/packages/database/src/Migrations/MigrationManager.php +++ b/packages/database/src/Migrations/MigrationManager.php @@ -6,7 +6,6 @@ use PDOException; use Tempest\Database\Builder\ModelDefinition; -use Tempest\Database\Config\DatabaseConfig; use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Database; use Tempest\Database\DatabaseMigration as MigrationInterface; @@ -18,14 +17,13 @@ use Tempest\Database\QueryStatements\SetForeignKeyChecksStatement; use Tempest\Database\QueryStatements\ShowTablesStatement; use Throwable; -use UnhandledMatchError; use function Tempest\event; final readonly class MigrationManager { public function __construct( - private DatabaseConfig $databaseConfig, + private DatabaseDialect $dialect, private Database $database, private RunnableMigrations $migrations, ) {} @@ -35,7 +33,7 @@ public function up(): void try { $existingMigrations = Migration::all(); } catch (PDOException $pdoException) { - if ($pdoException->getCode() === $this->databaseConfig->dialect->tableNotFoundCode() && str_contains($pdoException->getMessage(), 'table')) { + if ($this->dialect->isTableNotFoundError($pdoException)) { $this->executeUp(new CreateMigrationsTable()); $existingMigrations = Migration::all(); } else { @@ -62,13 +60,14 @@ public function down(): void try { $existingMigrations = Migration::all(); } catch (PDOException $pdoException) { - /** @throw UnhandledMatchError */ - match ((string) $pdoException->getCode()) { - $this->databaseConfig->dialect->tableNotFoundCode() => event( - event: new MigrationFailed(name: new ModelDefinition(Migration::class)->getTableDefinition()->name, exception: new TableNotFoundException()), - ), - default => throw new UnhandledMatchError($pdoException->getMessage()), - }; + if (! $this->dialect->isTableNotFoundError($pdoException)) { + throw $pdoException; + } + + event(new MigrationFailed( + name: new ModelDefinition(Migration::class)->getTableDefinition()->name, + exception: new TableNotFoundException(), + )); return; } @@ -90,18 +89,16 @@ public function down(): void public function dropAll(): void { - $dialect = $this->databaseConfig->dialect; - try { // Get all tables - $tables = $this->getTableDefinitions($dialect); + $tables = $this->getTableDefinitions(); // Disable foreign key checks - new SetForeignKeyChecksStatement(enable: false)->execute($dialect); + new SetForeignKeyChecksStatement(enable: false)->execute($this->dialect); // Drop each table foreach ($tables as $table) { - new DropTableStatement($table->name)->execute($dialect); + new DropTableStatement($table->name)->execute($this->dialect); event(new TableDropped($table->name)); } @@ -109,7 +106,7 @@ public function dropAll(): void event(new FreshMigrationFailed($throwable)); } finally { // Enable foreign key checks - new SetForeignKeyChecksStatement(enable: true)->execute($dialect); + new SetForeignKeyChecksStatement(enable: true)->execute($this->dialect); } } @@ -180,9 +177,7 @@ public function executeUp(MigrationInterface $migration): void return; } - $dialect = $this->databaseConfig->dialect; - - $query = new Query($statement->compile($dialect)); + $query = new Query($statement->compile($this->dialect)); try { $this->database->execute($query); @@ -208,23 +203,21 @@ public function executeDown(MigrationInterface $migration): void return; } - $dialect = $this->databaseConfig->dialect; - - $query = new Query($statement->compile($dialect)); + $query = new Query($statement->compile($this->dialect)); try { // TODO: don't just disable FK checking when executing down // Disable foreign key checks - new SetForeignKeyChecksStatement(enable: false)->execute($dialect); + new SetForeignKeyChecksStatement(enable: false)->execute($this->dialect); $this->database->execute($query); // Disable foreign key checks - new SetForeignKeyChecksStatement(enable: true)->execute($dialect); + new SetForeignKeyChecksStatement(enable: true)->execute($this->dialect); } catch (PDOException $pdoException) { // Disable foreign key checks - new SetForeignKeyChecksStatement(enable: true)->execute($dialect); + new SetForeignKeyChecksStatement(enable: true)->execute($this->dialect); event(new MigrationFailed($migration->name, $pdoException)); @@ -253,14 +246,15 @@ public function executeDown(MigrationInterface $migration): void /** * @return \Tempest\Database\Migrations\TableMigrationDefinition[] */ - private function getTableDefinitions(DatabaseDialect $dialect): array + private function getTableDefinitions(): array { return array_map( - fn (array $item) => match ($dialect) { + fn (array $item) => match ($this->dialect) { DatabaseDialect::SQLITE => new TableMigrationDefinition($item['name']), - default => new TableMigrationDefinition(array_values($item)[0]), + DatabaseDialect::POSTGRESQL => new TableMigrationDefinition($item['table_name']), + DatabaseDialect::MYSQL => new TableMigrationDefinition(array_values($item)[0]), }, - new ShowTablesStatement()->fetch($dialect), + new ShowTablesStatement()->fetch($this->dialect), ); } @@ -278,7 +272,7 @@ private function getMinifiedSqlFromStatement(?QueryStatement $statement): string return ''; } - $query = new Query($statement->compile($this->databaseConfig->dialect)); + $query = new Query($statement->compile($this->dialect)); // Remove comments $sql = preg_replace('/--.*$/m', '', $query->toSql()); // Remove SQL single-line comments diff --git a/packages/database/src/Query.php b/packages/database/src/Query.php index 1457be5d3..1b1df535e 100644 --- a/packages/database/src/Query.php +++ b/packages/database/src/Query.php @@ -5,6 +5,7 @@ namespace Tempest\Database; use Tempest\Database\Config\DatabaseConfig; +use Tempest\Database\Config\DatabaseDialect; use function Tempest\get; @@ -17,7 +18,7 @@ public function __construct( public array $executeAfter = [], ) {} - public function execute(mixed ...$bindings): Id + public function execute(mixed ...$bindings): ?Id { $this->bindings = [...$this->bindings, ...$bindings]; @@ -48,8 +49,14 @@ public function toSql(): string { $sql = $this->sql; + $dialect = $this->getDatabaseConfig()->dialect; + if ($sql instanceof QueryStatement) { - return $sql->compile($this->getDatabaseConfig()->dialect); + $sql = $sql->compile($dialect); + } + + if ($dialect === DatabaseDialect::POSTGRESQL) { + $sql = str_replace('`', '', $sql); } return $sql; diff --git a/packages/database/src/QueryStatements/BelongsToStatement.php b/packages/database/src/QueryStatements/BelongsToStatement.php index 199f2043c..0fc543ab7 100644 --- a/packages/database/src/QueryStatements/BelongsToStatement.php +++ b/packages/database/src/QueryStatements/BelongsToStatement.php @@ -22,29 +22,42 @@ public function compile(DatabaseDialect $dialect): string [$localTable, $localKey] = explode('.', $this->local); [$foreignTable, $foreignKey] = explode('.', $this->foreign); - return match ($dialect) { - DatabaseDialect::MYSQL, DatabaseDialect::POSTGRESQL => new ConstraintStatement( - ConstraintNameStatement::fromString( - sprintf( - 'fk_%s_%s_%s', - strtolower($foreignTable), - strtolower($localTable), - strtolower($localKey), - ), - ), - new RawStatement( - sprintf( - 'FOREIGN KEY %s(%s) REFERENCES %s(%s) %s %s', - $localTable, - $localKey, - $foreignTable, - $foreignKey, - 'ON DELETE ' . $this->onDelete->value, - 'ON UPDATE ' . $this->onUpdate->value, - ), - ), - )->compile($dialect), + $constraintName = ConstraintNameStatement::fromString( + sprintf( + 'fk_%s_%s_%s', + strtolower($foreignTable), + strtolower($localTable), + strtolower($localKey), + ), + ); + + $statement = match ($dialect) { + DatabaseDialect::MYSQL => new ConstraintStatement( + $constraintName, + new RawStatement(sprintf( + 'FOREIGN KEY %s(%s) REFERENCES %s(%s) %s %s', + $localTable, + $localKey, + $foreignTable, + $foreignKey, + 'ON DELETE ' . $this->onDelete->value, + 'ON UPDATE ' . $this->onUpdate->value, + )), + ), + DatabaseDialect::POSTGRESQL => new ConstraintStatement( + $constraintName, + new RawStatement(sprintf( + 'FOREIGN KEY(%s) REFERENCES %s(%s) %s %s', + $localKey, + $foreignTable, + $foreignKey, + 'ON DELETE ' . $this->onDelete->value, + 'ON UPDATE ' . $this->onUpdate->value, + )), + ), DatabaseDialect::SQLITE => throw new UnsupportedDialect(), }; + + return $statement->compile($dialect); } } diff --git a/packages/database/src/QueryStatements/BooleanStatement.php b/packages/database/src/QueryStatements/BooleanStatement.php index 14e0bf39b..52c624088 100644 --- a/packages/database/src/QueryStatements/BooleanStatement.php +++ b/packages/database/src/QueryStatements/BooleanStatement.php @@ -17,10 +17,19 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { + $default = null; + + if ($this->default !== null) { + $default = match ($dialect) { + DatabaseDialect::POSTGRESQL => $this->default ? 'true' : 'false', + default => $this->default ? '1' : '0', + }; + } + return sprintf( '`%s` BOOLEAN %s %s', $this->name, - $this->default ? "DEFAULT {$this->default}" : '', + $default !== null ? "DEFAULT {$default}" : '', $this->nullable ? '' : 'NOT NULL', ); } diff --git a/packages/database/src/QueryStatements/CanExecuteStatement.php b/packages/database/src/QueryStatements/CanExecuteStatement.php index 6340ea679..a06a4e235 100644 --- a/packages/database/src/QueryStatements/CanExecuteStatement.php +++ b/packages/database/src/QueryStatements/CanExecuteStatement.php @@ -10,8 +10,14 @@ trait CanExecuteStatement { - public function execute(DatabaseDialect $dialect): Id + public function execute(DatabaseDialect $dialect): ?Id { - return new Query($this->compile($dialect))->execute(); + $sql = $this->compile($dialect); + + if (! $sql) { + return null; + } + + return new Query($sql)->execute(); } } diff --git a/packages/database/src/QueryStatements/CharStatement.php b/packages/database/src/QueryStatements/CharStatement.php index 65274b1da..8e1fe53db 100644 --- a/packages/database/src/QueryStatements/CharStatement.php +++ b/packages/database/src/QueryStatements/CharStatement.php @@ -20,7 +20,7 @@ public function compile(DatabaseDialect $dialect): string return sprintf( '`%s` CHAR %s %s', $this->name, - $this->default !== null ? "DEFAULT \"{$this->default}\"" : '', + $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', ); } diff --git a/packages/database/src/QueryStatements/CompoundStatement.php b/packages/database/src/QueryStatements/CompoundStatement.php new file mode 100644 index 000000000..84984b74b --- /dev/null +++ b/packages/database/src/QueryStatements/CompoundStatement.php @@ -0,0 +1,29 @@ +statements = $statements; + } + + public function compile(DatabaseDialect $dialect): string + { + return arr($this->statements) + ->map(fn (QueryStatement $statement) => $statement->compile($dialect)) + ->implode(';' . PHP_EOL) + ->append(';') + ->toString(); + } +} diff --git a/packages/database/src/QueryStatements/ConstraintStatement.php b/packages/database/src/QueryStatements/ConstraintStatement.php index d344b2bf1..e700ad2da 100644 --- a/packages/database/src/QueryStatements/ConstraintStatement.php +++ b/packages/database/src/QueryStatements/ConstraintStatement.php @@ -6,7 +6,6 @@ use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\QueryStatement; -use Tempest\Database\UnsupportedDialect; final readonly class ConstraintStatement implements QueryStatement { diff --git a/packages/database/src/QueryStatements/CountStatement.php b/packages/database/src/QueryStatements/CountStatement.php index 182cecb10..d94a2e90c 100644 --- a/packages/database/src/QueryStatements/CountStatement.php +++ b/packages/database/src/QueryStatements/CountStatement.php @@ -21,11 +21,14 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { + $countField = new FieldStatement(sprintf( + 'COUNT(%s) AS %s', + $this->getCountArgument(), + $this->getKey(), + )); + $query = arr([ - sprintf( - 'SELECT COUNT(%s)', - $this->getCountArgument(), - ), + sprintf('SELECT %s', $countField->compile($dialect)), sprintf('FROM `%s`', $this->table->name), ]); @@ -51,6 +54,6 @@ public function getCountArgument(): string public function getKey(): string { - return "COUNT({$this->getCountArgument()})"; + return 'count'; } } diff --git a/packages/database/src/QueryStatements/CreateEnumTypeStatement.php b/packages/database/src/QueryStatements/CreateEnumTypeStatement.php index 3439ca5e0..97f5933c5 100644 --- a/packages/database/src/QueryStatements/CreateEnumTypeStatement.php +++ b/packages/database/src/QueryStatements/CreateEnumTypeStatement.php @@ -29,12 +29,8 @@ public function compile(DatabaseDialect $dialect): string return match ($dialect) { DatabaseDialect::MYSQL, DatabaseDialect::SQLITE => '', DatabaseDialect::POSTGRESQL => sprintf( - <<enumClass)->replace('\\\\', '_'), $cases->implode(', '), diff --git a/packages/database/src/QueryStatements/CreateTableStatement.php b/packages/database/src/QueryStatements/CreateTableStatement.php index a966b0b04..26c840f77 100644 --- a/packages/database/src/QueryStatements/CreateTableStatement.php +++ b/packages/database/src/QueryStatements/CreateTableStatement.php @@ -5,7 +5,6 @@ namespace Tempest\Database\QueryStatements; use BackedEnum; -use Symfony\Component\VarDumper\Cloner\Data; use Tempest\Database\Builder\ModelDefinition; use Tempest\Database\Builder\TableDefinition; use Tempest\Database\Config\DatabaseDialect; @@ -208,8 +207,6 @@ public function enum( bool $nullable = false, null|UnitEnum|BackedEnum $default = null, ): self { - $this->statements[] = new CreateEnumTypeStatement($enumClass); - $this->statements[] = new EnumStatement( name: $name, enumClass: $enumClass, diff --git a/packages/database/src/QueryStatements/DateStatement.php b/packages/database/src/QueryStatements/DateStatement.php index a7839a784..d3f6bdf78 100644 --- a/packages/database/src/QueryStatements/DateStatement.php +++ b/packages/database/src/QueryStatements/DateStatement.php @@ -20,7 +20,7 @@ public function compile(DatabaseDialect $dialect): string return sprintf( '`%s` DATE %s %s', $this->name, - $this->default !== null ? "DEFAULT \"{$this->default}\"" : '', + $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', ); } diff --git a/packages/database/src/QueryStatements/DatetimeStatement.php b/packages/database/src/QueryStatements/DatetimeStatement.php index ad9626ca2..99ed570dd 100644 --- a/packages/database/src/QueryStatements/DatetimeStatement.php +++ b/packages/database/src/QueryStatements/DatetimeStatement.php @@ -17,11 +17,19 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { - return sprintf( - '`%s` DATETIME %s %s', - $this->name, - $this->default !== null ? "DEFAULT \"{$this->default}\"" : '', - $this->nullable ? '' : 'NOT NULL', - ); + return match ($dialect) { + DatabaseDialect::POSTGRESQL => sprintf( + '`%s` TIMESTAMP %s %s', + $this->name, + $this->default !== null ? "DEFAULT '{$this->default}'" : '', + $this->nullable ? '' : 'NOT NULL', + ), + default => sprintf( + '`%s` DATETIME %s %s', + $this->name, + $this->default !== null ? "DEFAULT '{$this->default}'" : '', + $this->nullable ? '' : 'NOT NULL', + ), + }; } } diff --git a/packages/database/src/QueryStatements/DropEnumTypeStatement.php b/packages/database/src/QueryStatements/DropEnumTypeStatement.php new file mode 100644 index 000000000..a765ac426 --- /dev/null +++ b/packages/database/src/QueryStatements/DropEnumTypeStatement.php @@ -0,0 +1,34 @@ + */ + private string $enumClass, + ) {} + + public function compile(DatabaseDialect $dialect): string + { + return match ($dialect) { + DatabaseDialect::MYSQL, DatabaseDialect::SQLITE => '', + DatabaseDialect::POSTGRESQL => sprintf( + <<<'PSQL' + DROP TYPE IF EXISTS "%s"; + PSQL, + str($this->enumClass)->replace('\\\\', '_'), + ), + }; + } +} diff --git a/packages/database/src/QueryStatements/DropTableStatement.php b/packages/database/src/QueryStatements/DropTableStatement.php index 90aa89df4..c52959221 100644 --- a/packages/database/src/QueryStatements/DropTableStatement.php +++ b/packages/database/src/QueryStatements/DropTableStatement.php @@ -39,7 +39,10 @@ public function compile(DatabaseDialect $dialect): string $statements[] = $dropReference->compile($dialect); } - $statements[] = sprintf('DROP TABLE IF EXISTS `%s`', $this->tableName); + $statements[] = match ($dialect) { + DatabaseDialect::POSTGRESQL => sprintf('DROP TABLE IF EXISTS `%s` CASCADE', $this->tableName), + default => sprintf('DROP TABLE IF EXISTS `%s`', $this->tableName), + }; return implode('; ', $statements) . ';'; } diff --git a/packages/database/src/QueryStatements/EnumStatement.php b/packages/database/src/QueryStatements/EnumStatement.php index d3af47a40..e9c91934b 100644 --- a/packages/database/src/QueryStatements/EnumStatement.php +++ b/packages/database/src/QueryStatements/EnumStatement.php @@ -50,10 +50,10 @@ public function compile(DatabaseDialect $dialect): string $this->nullable ? '' : 'NOT NULL', ), DatabaseDialect::POSTGRESQL => sprintf( - '`%s` %s %s %s', + '"%s" "%s" %s %s', $this->name, str($this->enumClass)->replace('\\\\', '_'), - $defaultValue !== null ? "DEFAULT (\"{$defaultValue}\")" : '', + $defaultValue !== null ? "DEFAULT ('{$defaultValue}')" : '', $this->nullable ? '' : 'NOT NULL', ), }; diff --git a/packages/database/src/QueryStatements/FieldStatement.php b/packages/database/src/QueryStatements/FieldStatement.php index 8ee8d6c29..9492b1397 100644 --- a/packages/database/src/QueryStatements/FieldStatement.php +++ b/packages/database/src/QueryStatements/FieldStatement.php @@ -47,9 +47,17 @@ public function compile(DatabaseDialect $dialect): string $field = arr(explode('.', $field)) ->map(fn (string $part) => trim($part, '` ')) ->map( - fn (string $part) => match ($dialect) { - DatabaseDialect::SQLITE => $part, - default => sprintf('`%s`', $part), + function (string $part) use ($dialect) { + // Function calls are never wrapped in backticks. + if (str_contains($part, '(')) { + return $part; + } + + if ($dialect === DatabaseDialect::SQLITE) { + return $part; + } + + return sprintf('`%s`', $part); }, ) ->implode('.'); @@ -58,7 +66,10 @@ public function compile(DatabaseDialect $dialect): string return $field; } - return sprintf('%s AS `%s`', $field, trim($alias, '`')); + return match ($dialect) { + DatabaseDialect::POSTGRESQL => sprintf('%s AS "%s"', $field, trim($alias, '`')), + default => sprintf('%s AS `%s`', $field, trim($alias, '`')), + }; } public function withAliasPrefix(?string $prefix = null): self diff --git a/packages/database/src/QueryStatements/InsertStatement.php b/packages/database/src/QueryStatements/InsertStatement.php index 24f4bab87..9ac158c14 100644 --- a/packages/database/src/QueryStatements/InsertStatement.php +++ b/packages/database/src/QueryStatements/InsertStatement.php @@ -46,7 +46,7 @@ public function compile(DatabaseDialect $dialect): string }) ->implode(', '); - return sprintf( + $sql = sprintf( <<map(fn (string $column) => "`{$column}`")->implode(', '), $entryPlaceholders, ); + + if ($dialect === DatabaseDialect::POSTGRESQL) { + $sql .= ' RETURNING *'; + } + + return $sql; } } diff --git a/packages/database/src/QueryStatements/JsonStatement.php b/packages/database/src/QueryStatements/JsonStatement.php index 131d4a490..c78c8da20 100644 --- a/packages/database/src/QueryStatements/JsonStatement.php +++ b/packages/database/src/QueryStatements/JsonStatement.php @@ -37,7 +37,7 @@ public function compile(DatabaseDialect $dialect): string DatabaseDialect::POSTGRESQL => sprintf( '`%s` JSONB %s %s', $this->name, - $this->default !== null ? "DEFAULT (\"{$this->default}\")" : '', + $this->default !== null ? "DEFAULT ('{$this->default}')" : '', $this->nullable ? '' : 'NOT NULL', ), }; diff --git a/packages/database/src/QueryStatements/SetForeignKeyChecksStatement.php b/packages/database/src/QueryStatements/SetForeignKeyChecksStatement.php index bca4dc70c..0b1d214fc 100644 --- a/packages/database/src/QueryStatements/SetForeignKeyChecksStatement.php +++ b/packages/database/src/QueryStatements/SetForeignKeyChecksStatement.php @@ -20,7 +20,7 @@ public function compile(DatabaseDialect $dialect): string return match ($dialect) { DatabaseDialect::MYSQL => sprintf('SET FOREIGN_KEY_CHECKS = %s', $this->enable ? '1' : '0'), DatabaseDialect::SQLITE => sprintf('PRAGMA foreign_keys = %s', $this->enable ? '1' : '0'), - DatabaseDialect::POSTGRESQL => sprintf("SET session_replication_role = '%s';", $this->enable ? 'origin' : 'replica'), + DatabaseDialect::POSTGRESQL => '', }; } } diff --git a/packages/database/src/QueryStatements/ShowTablesStatement.php b/packages/database/src/QueryStatements/ShowTablesStatement.php index 0dc73b161..4bff0f725 100644 --- a/packages/database/src/QueryStatements/ShowTablesStatement.php +++ b/packages/database/src/QueryStatements/ShowTablesStatement.php @@ -23,7 +23,8 @@ public function compile(DatabaseDialect $dialect): string return match ($dialect) { DatabaseDialect::MYSQL => "SHOW FULL TABLES WHERE table_type = 'BASE TABLE'", DatabaseDialect::SQLITE => "select type, name from sqlite_master where type = 'table' and name not like 'sqlite_%'", - default => throw new UnsupportedDialect(), + DatabaseDialect::POSTGRESQL, + => "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE' AND table_schema NOT IN ('pg_catalog', 'information_schema');", }; } } diff --git a/packages/database/src/QueryStatements/TextStatement.php b/packages/database/src/QueryStatements/TextStatement.php index 84edc2572..0d65cbccb 100644 --- a/packages/database/src/QueryStatements/TextStatement.php +++ b/packages/database/src/QueryStatements/TextStatement.php @@ -26,7 +26,7 @@ public function compile(DatabaseDialect $dialect): string default => sprintf( '`%s` TEXT %s %s', $this->name, - $this->default !== null ? "DEFAULT \"{$this->default}\"" : '', + $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', ), }; diff --git a/packages/database/src/QueryStatements/VarcharStatement.php b/packages/database/src/QueryStatements/VarcharStatement.php index 6381af4e8..4f89e43c5 100644 --- a/packages/database/src/QueryStatements/VarcharStatement.php +++ b/packages/database/src/QueryStatements/VarcharStatement.php @@ -22,7 +22,7 @@ public function compile(DatabaseDialect $dialect): string '`%s` VARCHAR(%s) %s %s', $this->name, $this->size, - $this->default !== null ? "DEFAULT \"{$this->default}\"" : '', + $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', ); } diff --git a/packages/database/src/Transactions/GenericTransactionManager.php b/packages/database/src/Transactions/GenericTransactionManager.php index 93f40e82a..c43ab8daa 100644 --- a/packages/database/src/Transactions/GenericTransactionManager.php +++ b/packages/database/src/Transactions/GenericTransactionManager.php @@ -9,7 +9,7 @@ use Tempest\Database\Exceptions\CouldNotCommitTransaction; use Tempest\Database\Exceptions\CouldNotRollbackTransaction; -final class GenericTransactionManager implements TransactionManager +final readonly class GenericTransactionManager implements TransactionManager { public function __construct( private Connection $connection, diff --git a/packages/database/tests/GenericDatabaseTest.php b/packages/database/tests/GenericDatabaseTest.php index 5fc8e2fa5..2d0be158a 100644 --- a/packages/database/tests/GenericDatabaseTest.php +++ b/packages/database/tests/GenericDatabaseTest.php @@ -6,6 +6,7 @@ use Exception; use PHPUnit\Framework\TestCase; +use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Connection\Connection; use Tempest\Database\GenericDatabase; use Tempest\Database\Transactions\GenericTransactionManager; @@ -32,6 +33,7 @@ public function test_it_executes_transactions(): void $database = new GenericDatabase( $connection, new GenericTransactionManager($connection), + DatabaseDialect::SQLITE, ); $result = $database->withinTransaction(function () { @@ -58,6 +60,7 @@ public function test_it_rolls_back_transactions_on_failure(): void $database = new GenericDatabase( $connection, new GenericTransactionManager($connection), + DatabaseDialect::SQLITE, ); $result = $database->withinTransaction(function (): never { diff --git a/packages/database/tests/QueryStatements/AlterTableStatementTest.php b/packages/database/tests/QueryStatements/AlterTableStatementTest.php index fe8208316..b666fd993 100644 --- a/packages/database/tests/QueryStatements/AlterTableStatementTest.php +++ b/packages/database/tests/QueryStatements/AlterTableStatementTest.php @@ -38,7 +38,7 @@ public function test_alter_for_only_indexes(DatabaseDialect $dialect): void #[TestWith([DatabaseDialect::SQLITE])] public function test_alter_add_column(DatabaseDialect $dialect): void { - $expected = 'ALTER TABLE `table` ADD `bar` VARCHAR(42) DEFAULT "xx" ;'; + $expected = 'ALTER TABLE `table` ADD `bar` VARCHAR(42) DEFAULT \'xx\' ;'; $statement = new AlterTableStatement('table') ->add(new VarcharStatement('bar', 42, true, 'xx')) ->compile($dialect); @@ -49,8 +49,7 @@ public function test_alter_add_column(DatabaseDialect $dialect): void } #[TestWith([DatabaseDialect::MYSQL])] - #[TestWith([DatabaseDialect::POSTGRESQL])] - public function test_alter_add_belongs_to(DatabaseDialect $dialect): void + public function test_alter_add_belongs_to_mysql(DatabaseDialect $dialect): void { $expected = 'ALTER TABLE `table` ADD CONSTRAINT `fk_parent_table_foo` FOREIGN KEY table(foo) REFERENCES parent(bar) ON DELETE RESTRICT ON UPDATE NO ACTION ;'; $statement = new AlterTableStatement('table') @@ -62,6 +61,19 @@ public function test_alter_add_belongs_to(DatabaseDialect $dialect): void $this->assertEqualsIgnoringCase($expected, $normalized); } + #[TestWith([DatabaseDialect::POSTGRESQL])] + public function test_alter_add_belongs_to_postgresql(DatabaseDialect $dialect): void + { + $expected = 'ALTER TABLE `table` ADD CONSTRAINT `fk_parent_table_foo` FOREIGN KEY(foo) REFERENCES parent(bar) ON DELETE RESTRICT ON UPDATE NO ACTION ;'; + $statement = new AlterTableStatement('table') + ->add(new BelongsToStatement('table.foo', 'parent.bar')) + ->compile($dialect); + + $normalized = self::removeDuplicateWhitespace($statement); + + $this->assertEqualsIgnoringCase($expected, $normalized); + } + #[TestWith([DatabaseDialect::SQLITE])] public function test_alter_add_belongs_to_unsupported(DatabaseDialect $dialect): void { @@ -109,12 +121,12 @@ public function test_alter_table_drop_constraint_unsupported_dialects(DatabaseDi ->compile($dialect); } - #[TestWith([DatabaseDialect::MYSQL, 'ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT "bar" NOT NULL ;'])] + #[TestWith([DatabaseDialect::MYSQL, 'ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT \'bar\' NOT NULL ;'])] #[TestWith([ DatabaseDialect::POSTGRESQL, - 'ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT "bar" NOT NULL ;', + 'ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT \'bar\' NOT NULL ;', ])] - #[TestWith([DatabaseDialect::SQLITE, 'ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT "bar" NOT NULL ;'])] + #[TestWith([DatabaseDialect::SQLITE, 'ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT \'bar\' NOT NULL ;'])] public function test_alter_table_add_column(DatabaseDialect $dialect, string $expected): void { $statement = new AlterTableStatement('table') @@ -141,8 +153,8 @@ public function test_alter_table_rename_column(DatabaseDialect $dialect): void $this->assertEqualsIgnoringCase($expected, $normalized); } - #[TestWith([DatabaseDialect::MYSQL, 'ALTER TABLE `table` MODIFY COLUMN `foo` VARCHAR(42) DEFAULT "bar" NOT NULL ;'])] - #[TestWith([DatabaseDialect::POSTGRESQL, 'ALTER TABLE `table` ALTER COLUMN `foo` VARCHAR(42) DEFAULT "bar" NOT NULL ;'])] + #[TestWith([DatabaseDialect::MYSQL, 'ALTER TABLE `table` MODIFY COLUMN `foo` VARCHAR(42) DEFAULT \'bar\' NOT NULL ;'])] + #[TestWith([DatabaseDialect::POSTGRESQL, 'ALTER TABLE `table` ALTER COLUMN `foo` VARCHAR(42) DEFAULT \'bar\' NOT NULL ;'])] public function test_alter_table_modify_column(DatabaseDialect $dialect, string $expected): void { $statement = new AlterTableStatement('table') diff --git a/packages/database/tests/QueryStatements/CountStatementTest.php b/packages/database/tests/QueryStatements/CountStatementTest.php index 2e6682afa..816e5966c 100644 --- a/packages/database/tests/QueryStatements/CountStatementTest.php +++ b/packages/database/tests/QueryStatements/CountStatementTest.php @@ -19,13 +19,11 @@ public function test_count_statement(): void ); $expected = <<assertSame($expected, $statement->compile(DatabaseDialect::MYSQL)); - $this->assertSame($expected, $statement->compile(DatabaseDialect::POSTGRESQL)); - $this->assertSame($expected, $statement->compile(DatabaseDialect::SQLITE)); } public function test_count_statement_with_specified_column(): void @@ -38,13 +36,11 @@ public function test_count_statement_with_specified_column(): void ); $expected = <<assertSame($expected, $statement->compile(DatabaseDialect::MYSQL)); - $this->assertSame($expected, $statement->compile(DatabaseDialect::POSTGRESQL)); - $this->assertSame($expected, $statement->compile(DatabaseDialect::SQLITE)); } public function test_count_statement_with_distinct_specified_column(): void @@ -59,12 +55,10 @@ public function test_count_statement_with_distinct_specified_column(): void $statement->distinct = true; $expected = <<assertSame($expected, $statement->compile(DatabaseDialect::MYSQL)); - $this->assertSame($expected, $statement->compile(DatabaseDialect::POSTGRESQL)); - $this->assertSame($expected, $statement->compile(DatabaseDialect::SQLITE)); } } diff --git a/packages/database/tests/QueryStatements/CreateTableStatementTest.php b/packages/database/tests/QueryStatements/CreateTableStatementTest.php index 5ebf4830a..12f767e64 100644 --- a/packages/database/tests/QueryStatements/CreateTableStatementTest.php +++ b/packages/database/tests/QueryStatements/CreateTableStatementTest.php @@ -86,7 +86,7 @@ public static function provide_fk_create_table_database_drivers(): Generator 'CREATE TABLE `books` ( `id` SERIAL PRIMARY KEY, `author_id` INTEGER NOT NULL, - CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY books(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION, `name` VARCHAR(255) NOT NULL );', ]; diff --git a/packages/database/tests/QueryStatements/FieldStatementTest.php b/packages/database/tests/QueryStatements/FieldStatementTest.php index c2295598c..6524d25ce 100644 --- a/packages/database/tests/QueryStatements/FieldStatementTest.php +++ b/packages/database/tests/QueryStatements/FieldStatementTest.php @@ -19,6 +19,11 @@ public function test_sqlite(): void 'table.field', new FieldStatement('`table`.`field`')->compile(DatabaseDialect::SQLITE), ); + + $this->assertSame( + 'COUNT(*) AS `count`', + new FieldStatement('COUNT(*) AS count')->compile(DatabaseDialect::MYSQL), + ); } public function test_mysql(): void @@ -32,6 +37,11 @@ public function test_mysql(): void '`table`.`field`', new FieldStatement('table.field')->compile(DatabaseDialect::MYSQL), ); + + $this->assertSame( + 'COUNT(*) AS `count`', + new FieldStatement('COUNT(*) AS count')->compile(DatabaseDialect::MYSQL), + ); } public function test_postgres(): void diff --git a/packages/database/tests/QueryStatements/InsertStatementTest.php b/packages/database/tests/QueryStatements/InsertStatementTest.php index 0cd8ab91c..4a7c19ccd 100644 --- a/packages/database/tests/QueryStatements/InsertStatementTest.php +++ b/packages/database/tests/QueryStatements/InsertStatementTest.php @@ -28,7 +28,13 @@ public function test_insert_statement(): void $this->assertSame($expected, $statement->compile(DatabaseDialect::MYSQL)); $this->assertSame($expected, $statement->compile(DatabaseDialect::SQLITE)); - $this->assertSame($expected, $statement->compile(DatabaseDialect::POSTGRESQL)); + + $expectedPostgres = <<assertSame($expectedPostgres, $statement->compile(DatabaseDialect::POSTGRESQL)); } public function test_exception_on_column_mismatch(): void diff --git a/packages/database/tests/QueryStatements/SelectStatementTest.php b/packages/database/tests/QueryStatements/SelectStatementTest.php index d4f2c6a1c..4ae1e26ec 100644 --- a/packages/database/tests/QueryStatements/SelectStatementTest.php +++ b/packages/database/tests/QueryStatements/SelectStatementTest.php @@ -33,7 +33,7 @@ public function test_select(): void offset: 100, ); - $expectedWithBackticks = <<assertSame($expectedWithBackticks, $statement->compile(DatabaseDialect::MYSQL)); - $this->assertSame($expectedWithBackticks, $statement->compile(DatabaseDialect::POSTGRESQL)); - - $expectedWithoutBackticks = <<assertSame($expectedWithoutBackticks, $statement->compile(DatabaseDialect::SQLITE)); + $this->assertSame($expectedMysql, $statement->compile(DatabaseDialect::MYSQL)); } } diff --git a/src/Tempest/Framework/Testing/IntegrationTest.php b/src/Tempest/Framework/Testing/IntegrationTest.php index ee46ed16f..a35e314d4 100644 --- a/src/Tempest/Framework/Testing/IntegrationTest.php +++ b/src/Tempest/Framework/Testing/IntegrationTest.php @@ -9,6 +9,7 @@ use Tempest\Clock\MockClock; use Tempest\Console\Testing\ConsoleTester; use Tempest\Container\Container; +use Tempest\Container\GenericContainer; use Tempest\Core\AppConfig; use Tempest\Core\FrameworkKernel; use Tempest\Core\Kernel; @@ -35,7 +36,7 @@ abstract class IntegrationTest extends TestCase protected Kernel $kernel; - protected Container $container; + protected GenericContainer $container; protected ConsoleTester $console; @@ -61,6 +62,7 @@ protected function setUp(): void discoveryLocations: $this->discoveryLocations, ); + // @phpstan-ignore-next-line $this->container = $this->kernel->container; $this->console = $this->container->get(ConsoleTester::class); diff --git a/tests/Fixtures/Config/database.postgres.php b/tests/Fixtures/Config/database.postgres.php new file mode 100644 index 000000000..5bc12675e --- /dev/null +++ b/tests/Fixtures/Config/database.postgres.php @@ -0,0 +1,13 @@ +build(); $expected = << ? @@ -36,7 +39,7 @@ public function test_simple_count_query(): void $sql = $query->toSql(); $bindings = $query->bindings; - $this->assertSame($expected, $sql); + $this->assertSameWithoutBackticks($expected, $sql); $this->assertSame(['Timeline Taxi', '1', '2025-01-01'], $bindings); } @@ -49,11 +52,11 @@ public function test_count_query_with_specified_asterisk(): void $sql = $query->toSql(); $expected = <<assertSame($expected, $sql); + $this->assertSameWithoutBackticks($expected, $sql); } public function test_count_query_with_specified_field(): void @@ -63,11 +66,11 @@ public function test_count_query_with_specified_field(): void $sql = $query->toSql(); $expected = <<assertSame($expected, $sql); + $this->assertSameWithoutBackticks($expected, $sql); } public function test_count_query_without_specifying_column_cannot_be_distinct(): void @@ -100,11 +103,11 @@ public function test_count_query_with_distinct_specified_field(): void $sql = $query->toSql(); $expected = <<assertSame($expected, $sql); + $this->assertSameWithoutBackticks($expected, $sql); } public function test_count_from_model(): void @@ -114,11 +117,11 @@ public function test_count_from_model(): void $sql = $query->toSql(); $expected = <<assertSame($expected, $sql); + $this->assertSameWithoutBackticks($expected, $sql); } public function test_count_query_with_conditions(): void @@ -142,7 +145,7 @@ public function test_count_query_with_conditions(): void ->build(); $expected = << ? @@ -152,7 +155,21 @@ public function test_count_query_with_conditions(): void $sql = $query->toSql(); $bindings = $query->bindings; - $this->assertSame($expected, $sql); + $this->assertSameWithoutBackticks($expected, $sql); $this->assertSame(['Timeline Taxi', '1', '2025-01-01'], $bindings); } + + public function test_count(): void + { + $this->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class); + + query('authors')->insert( + ['id' => 1, 'name' => 'Brent'], + ['id' => 2, 'name' => 'Other'], + )->execute(); + + $count = query('authors')->count()->execute(); + + $this->assertSame(2, $count); + } } diff --git a/tests/Integration/Database/Builder/DeleteQueryBuilderTest.php b/tests/Integration/Database/Builder/DeleteQueryBuilderTest.php index 3aaff3a26..1d1c1b811 100644 --- a/tests/Integration/Database/Builder/DeleteQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/DeleteQueryBuilderTest.php @@ -21,7 +21,7 @@ public function test_delete_on_plain_table(): void ->where('`bar` = ?', 'boo') ->build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<toSql(), ); - $this->assertSame( + $this->assertSameWithoutBackticks( 'boo', $query->bindings[0], ); @@ -42,7 +42,7 @@ public function test_delete_on_model_table(): void ->allowAll() ->build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<delete() ->build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<build(); - $this->assertSame( - <<buildExpectedInsert(<<assertSameWithoutBackticks( + $expected, $query->toSql(), ); @@ -56,11 +59,13 @@ public function test_insert_with_batch(): void ->insert(...$arrayOfStuff) ->build(); - $this->assertSame( - <<buildExpectedInsert(<<assertSameWithoutBackticks( + $expected, $query->toSql(), ); @@ -84,12 +89,12 @@ public function test_insert_on_model_table(): void ) ->build(); - $expected = <<buildExpectedInsert(<<assertSame($expected, $query->toSql()); + $this->assertSameWithoutBackticks($expected, $query->toSql()); $this->assertSame(['brent', 'a', null, 'other name', 'b', null], $query->bindings); } @@ -108,23 +113,23 @@ public function test_insert_on_model_table_with_new_relation(): void ) ->build(); - $expectedBookQuery = <<buildExpectedInsert(<<assertSame($expectedBookQuery, $bookQuery->toSql()); + $this->assertSameWithoutBackticks($expectedBookQuery, $bookQuery->toSql()); $this->assertSame('Timeline Taxi', $bookQuery->bindings[0]); $this->assertInstanceOf(Query::class, $bookQuery->bindings[1]); $authorQuery = $bookQuery->bindings[1]; - $expectedAuthorQuery = <<buildExpectedInsert(<<assertSame($expectedAuthorQuery, $authorQuery->toSql()); + $this->assertSameWithoutBackticks($expectedAuthorQuery, $authorQuery->toSql()); $this->assertSame('Brent', $authorQuery->bindings[0]); } @@ -144,12 +149,12 @@ public function test_insert_on_model_table_with_existing_relation(): void ) ->build(); - $expectedBookQuery = <<buildExpectedInsert(<<assertSame($expectedBookQuery, $bookQuery->toSql()); + $this->assertSameWithoutBackticks($expectedBookQuery, $bookQuery->toSql()); $this->assertSame('Timeline Taxi', $bookQuery->bindings[0]); $this->assertSame(10, $bookQuery->bindings[1]); } @@ -220,4 +225,13 @@ public function test_insert_with_non_object_model(): void $this->assertSame(2, $count); } + + private function buildExpectedInsert(string $query): string + { + if ($this->container->get(DatabaseDialect::class) === DatabaseDialect::POSTGRESQL) { + $query .= ' RETURNING *'; + } + + return $query; + } } diff --git a/tests/Integration/Database/Builder/SelectQueryBuilderTest.php b/tests/Integration/Database/Builder/SelectQueryBuilderTest.php index 28cc1f892..aaae8840f 100644 --- a/tests/Integration/Database/Builder/SelectQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/SelectQueryBuilderTest.php @@ -205,7 +205,7 @@ public function test_chunk(): void $this->assertCount(4, $results); $results = []; - Book::select()->where('title <> "A"')->chunk(function (array $chunk) use (&$results): void { + Book::select()->where("title <> 'A'")->chunk(function (array $chunk) use (&$results): void { $results = [...$results, ...$chunk]; }, 2); $this->assertCount(3, $results); @@ -413,12 +413,4 @@ private function seed(): void ['title' => 'Timeline Taxi Chapter 4', 'book_id' => 4], )->execute(); } - - private function assertSameWithoutBackticks(string $expected, string $actual): void - { - $this->assertSame( - str_replace('`', '', $expected), - str_replace('`', '', $actual), - ); - } } diff --git a/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php b/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php index 40c960a85..ca8313ca4 100644 --- a/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php @@ -3,7 +3,7 @@ namespace Tests\Tempest\Integration\Database\Builder; use Tempest\Database\Builder\QueryBuilders\UpdateQueryBuilder; -use Tempest\Database\Exceptions\CannotInsertHasManyRelation; +use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Exceptions\CannotUpdateHasManyRelation; use Tempest\Database\Exceptions\CannotUpdateHasOneRelation; use Tempest\Database\Exceptions\InvalidUpdateStatement; @@ -15,7 +15,6 @@ use Tests\Tempest\Fixtures\Modules\Books\Models\Author; use Tests\Tempest\Fixtures\Modules\Books\Models\AuthorType; use Tests\Tempest\Fixtures\Modules\Books\Models\Book; -use Tests\Tempest\Fixtures\Modules\Books\Models\Chapter; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; use function Tempest\Database\query; @@ -32,7 +31,7 @@ public function test_update_on_plain_table(): void ->where('`id` = ?', 10) ->build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<allowAll() ->build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<where('`id` = ?', 10) ->build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<update(author: Author::new(name: 'Brent')) ->build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<bindings[0]; - $this->assertSame( - <<container->get(DatabaseDialect::class) === DatabaseDialect::POSTGRESQL) { + $expected .= ' RETURNING *'; + } + + $this->assertSameWithoutBackticks( + $expected, $authorQuery->toSql(), ); @@ -192,7 +197,7 @@ public function test_attach_existing_relation_on_update(): void ->update(author: Author::new(id: new Id(5), name: 'Brent')) ->build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<build(); - $this->assertSame( + $this->assertSameWithoutBackticks( <<container->get(Database::class); + $this->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class); + + $db = $this->container->get(Database::class); - $manager->withinTransaction(function (): void { - $this->console - ->call('migrate:up'); + $db->withinTransaction(function (): void { + query(Author::class)->insert( + name: 'Brent', + )->execute(); }); - $this->assertNotEmpty(Migration::all()); + $this->assertSame(1, query(Author::class)->count()->execute()); } - public function test_execute_with_fail_works_correctly(): void + public function test_transaction_manager_fails(): void { - $database = $this->container->get(Database::class); - $this->migrate(CreateMigrationsTable::class, CreatePublishersTable::class, CreateAuthorTable::class); - $database->withinTransaction(function (): never { - new Author(name: 'test')->save(); + $db = $this->container->get(Database::class); + + $db->withinTransaction(function (): never { + query(Author::class)->insert( + name: 'Brent', + )->execute(); - throw new Exception('Dummy exception to force rollback'); + throw new Exception('Test'); }); - $this->assertCount(0, Author::all()); + $this->assertSame(0, query(Author::class)->count()->execute()); } } diff --git a/tests/Integration/Database/GenericTransactionManagerTest.php b/tests/Integration/Database/GenericTransactionManagerTest.php index d2d6ad71a..55f06a9a5 100644 --- a/tests/Integration/Database/GenericTransactionManagerTest.php +++ b/tests/Integration/Database/GenericTransactionManagerTest.php @@ -12,6 +12,8 @@ use Tests\Tempest\Fixtures\Modules\Books\Models\Author; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; +use function Tempest\Database\query; + /** * @internal */ diff --git a/tests/Integration/Database/QueryStatements/AlterTableStatementTest.php b/tests/Integration/Database/QueryStatements/AlterTableStatementTest.php index 25239b391..71b79255d 100644 --- a/tests/Integration/Database/QueryStatements/AlterTableStatementTest.php +++ b/tests/Integration/Database/QueryStatements/AlterTableStatementTest.php @@ -48,7 +48,7 @@ public function test_it_can_alter_a_table_definition(): void $message = match ($this->container->get(DatabaseConfig::class)?->dialect) { DatabaseDialect::MYSQL => "Unknown column 'email'", DatabaseDialect::SQLITE => 'table users has no column named email', - DatabaseDialect::POSTGRESQL => 'table users has no column named email', + DatabaseDialect::POSTGRESQL => 'column "email" of relation "users" does not exist', null => throw new RuntimeException('No database dialect available'), }; diff --git a/tests/Integration/Database/QueryStatements/CompoundStatementTest.php b/tests/Integration/Database/QueryStatements/CompoundStatementTest.php new file mode 100644 index 000000000..9e22e83a4 --- /dev/null +++ b/tests/Integration/Database/QueryStatements/CompoundStatementTest.php @@ -0,0 +1,27 @@ +assertSame( + <<compile(DatabaseDialect::SQLITE), + ); + } +} diff --git a/tests/Integration/Database/QueryStatements/CreateEnumTypeStatementTest.php b/tests/Integration/Database/QueryStatements/CreateEnumTypeStatementTest.php index aa2cbce57..b7f135501 100644 --- a/tests/Integration/Database/QueryStatements/CreateEnumTypeStatementTest.php +++ b/tests/Integration/Database/QueryStatements/CreateEnumTypeStatementTest.php @@ -16,13 +16,9 @@ enumClass: EnumForCreateTable::class, ); $this->assertSame( - <<compile(DatabaseDialect::POSTGRESQL), ); } diff --git a/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php b/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php index ca1df52b6..09cb96656 100644 --- a/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php +++ b/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php @@ -12,7 +12,10 @@ use Tempest\Database\Exceptions\InvalidValue; use Tempest\Database\Migrations\CreateMigrationsTable; use Tempest\Database\QueryStatement; +use Tempest\Database\QueryStatements\CompoundStatement; +use Tempest\Database\QueryStatements\CreateEnumTypeStatement; use Tempest\Database\QueryStatements\CreateTableStatement; +use Tempest\Database\QueryStatements\DropEnumTypeStatement; use Tempest\Database\UnsupportedDialect; use Tests\Tempest\Integration\Database\Fixtures\EnumForCreateTable; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -29,7 +32,7 @@ public function test_defaults(): void public function up(): QueryStatement { - return new CreateTableStatement('table') + return new CreateTableStatement('test_table') ->text('text', default: 'default') ->char('char', default: 'd') ->varchar('varchar', default: 'default') @@ -64,7 +67,7 @@ public function test_set_statement(): void public function up(): QueryStatement { - return new CreateTableStatement('table') + return new CreateTableStatement('test_table') ->set('set', values: ['foo', 'bar'], default: 'foo'); } @@ -95,8 +98,8 @@ public function test_array_statement(): void public function up(): QueryStatement { - return new CreateTableStatement('table') - ->array('array', default: ['foo', 'bar']); + return new CreateTableStatement('test_table') + ->array('test_array', default: ['foo', 'bar']); } public function down(): ?QueryStatement @@ -115,12 +118,35 @@ public function down(): ?QueryStatement public function test_enum_statement(): void { - $migration = new class() implements DatabaseMigration { + $this->migrate(CreateMigrationsTable::class); + + if ($this->container->get(DatabaseDialect::class) === DatabaseDialect::POSTGRESQL) { + $enumTypeMigration = new class() implements DatabaseMigration { + public string $name = '0'; + + public function up(): QueryStatement + { + return new CompoundStatement( + new DropEnumTypeStatement(EnumForCreateTable::class), + new CreateEnumTypeStatement(EnumForCreateTable::class), + ); + } + + public function down(): ?QueryStatement + { + return null; + } + }; + + $this->migrate($enumTypeMigration); + } + + $tableMigration = new class() implements DatabaseMigration { public string $name = '0'; public function up(): QueryStatement { - return new CreateTableStatement('table') + return new CreateTableStatement('test_table') ->enum( name: 'enum', enumClass: EnumForCreateTable::class, @@ -134,10 +160,7 @@ public function down(): ?QueryStatement } }; - $this->migrate( - CreateMigrationsTable::class, - $migration, - ); + $this->migrate($tableMigration); $this->expectNotToPerformAssertions(); } @@ -149,7 +172,7 @@ public function test_invalid_json_default(): void public function up(): QueryStatement { - return new CreateTableStatement('table') + return new CreateTableStatement('test_table') ->json('json', default: '{default: "invalid json"}'); } @@ -175,7 +198,7 @@ public function test_invalid_set_values(): void public function up(): QueryStatement { - return new CreateTableStatement('table') + return new CreateTableStatement('test_table') ->set('set', values: []); } diff --git a/tests/Integration/Database/QueryStatements/DropEnumTypeStatementTest.php b/tests/Integration/Database/QueryStatements/DropEnumTypeStatementTest.php new file mode 100644 index 000000000..680ead1fc --- /dev/null +++ b/tests/Integration/Database/QueryStatements/DropEnumTypeStatementTest.php @@ -0,0 +1,25 @@ +assertSame( + <<compile(DatabaseDialect::POSTGRESQL), + ); + } +} diff --git a/tests/Integration/Database/QueryStatements/EnumStatementTest.php b/tests/Integration/Database/QueryStatements/EnumStatementTest.php index 28cb7eb6e..6cde0d6ef 100644 --- a/tests/Integration/Database/QueryStatements/EnumStatementTest.php +++ b/tests/Integration/Database/QueryStatements/EnumStatementTest.php @@ -17,7 +17,7 @@ enumClass: EnumForCreateTable::class, ); $this->assertSame( - "`enum` ENUM('foo', 'bar', 'Tests\\\\Tempest\\\\Integration\\\\Database\\\\Fixtures\\\\EnumForCreateTable') NOT NULL", + "`enum` ENUM('foo', 'bar') NOT NULL", $enumStatement->compile(DatabaseDialect::MYSQL), ); } @@ -43,7 +43,7 @@ enumClass: EnumForCreateTable::class, ); $this->assertSame( - '`enum` Tests\Tempest\Integration\Database\Fixtures\EnumForCreateTable NOT NULL', + '"enum" "Tests\Tempest\Integration\Database\Fixtures\EnumForCreateTable" NOT NULL', $enumStatement->compile(DatabaseDialect::POSTGRESQL), ); } diff --git a/tests/Integration/FrameworkIntegrationTestCase.php b/tests/Integration/FrameworkIntegrationTestCase.php index 643a4bf34..5fa05f53e 100644 --- a/tests/Integration/FrameworkIntegrationTestCase.php +++ b/tests/Integration/FrameworkIntegrationTestCase.php @@ -11,12 +11,11 @@ use Tempest\Console\Output\StdoutOutputBuffer; use Tempest\Console\OutputBuffer; use Tempest\Console\Testing\ConsoleTester; -use Tempest\Core\AppConfig; use Tempest\Core\Application; use Tempest\Core\ShellExecutor; use Tempest\Core\ShellExecutors\NullShellExecutor; -use Tempest\Database\Connection\CachedConnectionInitializer; -use Tempest\Database\Connection\Connection; +use Tempest\Database\Connection\ConnectionInitializer; +use Tempest\Database\DatabaseInitializer; use Tempest\Database\Migrations\MigrationManager; use Tempest\Discovery\DiscoveryLocation; use Tempest\Framework\Testing\IntegrationTest; @@ -57,7 +56,10 @@ protected function setUp(): void $this->console = new ConsoleTester($this->container); // Database - $this->container->addInitializer(CachedConnectionInitializer::class); + $this->container + ->removeInitializer(DatabaseInitializer::class) + ->addInitializer(TestingDatabaseInitializer::class); + $databaseConfigPath = __DIR__ . '/../Fixtures/Config/database.config.php'; if (! file_exists($databaseConfigPath)) { @@ -71,7 +73,7 @@ protected function setUp(): void protected function tearDown(): void { - $this->container->get(Connection::class)->close(); + // $this->container->get(Connection::class)->close(); } protected function actAsConsoleApplication(string $command = ''): Application @@ -176,4 +178,19 @@ protected function assertSnippetsMatch(string $expected, string $actual): void $this->assertSame($expected, $actual); } + + protected function assertSameWithoutBackticks(string $expected, string $actual): void + { + $clean = function (string $string): string { + return \Tempest\Support\str($string) + ->replace('`', '') + ->replaceRegex('/AS \"(?.*?)\"/', fn (array $matches) => "AS {$matches['alias']}") + ->toString(); + }; + + $this->assertSame( + $clean($expected), + $clean($actual), + ); + } } diff --git a/tests/Integration/ORM/IsDatabaseModelTest.php b/tests/Integration/ORM/IsDatabaseModelTest.php index 05de1d892..250e2b033 100644 --- a/tests/Integration/ORM/IsDatabaseModelTest.php +++ b/tests/Integration/ORM/IsDatabaseModelTest.php @@ -6,6 +6,7 @@ use Carbon\Carbon; use DateTimeImmutable; +use Integration\ORM\Migrations\CreateCasterEnumType; use Tempest\Database\Builder\ModelDefinition; use Tempest\Database\Exceptions\MissingRelation; use Tempest\Database\Exceptions\MissingValue; @@ -506,15 +507,15 @@ public function test_two_way_casters_on_models(): void new CasterModel( date: new DateTimeImmutable('2025-01-01 00:00:00'), - array: ['a', 'b', 'c'], - enum: CasterEnum::BAR, + array_prop: ['a', 'b', 'c'], + enum_prop: CasterEnum::BAR, )->save(); $model = CasterModel::select()->first(); $this->assertSame(new DateTimeImmutable('2025-01-01 00:00:00')->format('c'), $model->date->format('c')); - $this->assertSame(['a', 'b', 'c'], $model->array); - $this->assertSame(CasterEnum::BAR, $model->enum); + $this->assertSame(['a', 'b', 'c'], $model->array_prop); + $this->assertSame(CasterEnum::BAR, $model->enum_prop); } public function test_find(): void diff --git a/tests/Integration/ORM/Mappers/QueryMapperTest.php b/tests/Integration/ORM/Mappers/QueryMapperTest.php index 915235e15..0d5c9bd88 100644 --- a/tests/Integration/ORM/Mappers/QueryMapperTest.php +++ b/tests/Integration/ORM/Mappers/QueryMapperTest.php @@ -5,6 +5,7 @@ namespace Tests\Tempest\Integration\ORM\Mappers; use Tempest\Database\Builder\QueryBuilders\UpdateQueryBuilder; +use Tempest\Database\Config\DatabaseDialect; use Tempest\Database\Id; use Tempest\Database\Query; use Tests\Tempest\Fixtures\Modules\Books\Models\Author; @@ -24,10 +25,20 @@ public function test_insert_query(): void $query = query(Author::class)->insert($author)->build(); - $this->assertSame(<<<'SQL' - INSERT INTO `authors` (`name`) - VALUES (?) - SQL, $query->toSql()); + $dialect = $this->container->get(DatabaseDialect::class); + + $expected = match ($dialect) { + DatabaseDialect::POSTGRESQL => <<<'SQL' + INSERT INTO authors (name) + VALUES (?) RETURNING * + SQL, + default => <<<'SQL' + INSERT INTO `authors` (`name`) + VALUES (?) + SQL, + }; + + $this->assertSame($expected, $query->toSql()); $this->assertSame(['test'], $query->bindings); } @@ -37,11 +48,22 @@ public function test_update_query(): void $query = query($author)->update(name: 'other')->build(); - $this->assertSame(<<<'SQL' - UPDATE `authors` - SET `name` = ? - WHERE `id` = ? - SQL, $query->toSql()); + $dialect = $this->container->get(DatabaseDialect::class); + + $expected = match ($dialect) { + DatabaseDialect::POSTGRESQL => <<<'SQL' + UPDATE authors + SET name = ? + WHERE id = ? + SQL, + default => <<<'SQL' + UPDATE `authors` + SET `name` = ? + WHERE `id` = ? + SQL, + }; + + $this->assertSame($expected, $query->toSql()); $this->assertSame(['other', 1], $query->bindings); } diff --git a/tests/Integration/ORM/Migrations/CreateCasterModelTable.php b/tests/Integration/ORM/Migrations/CreateCasterModelTable.php index 02c155217..b75ed8fe5 100644 --- a/tests/Integration/ORM/Migrations/CreateCasterModelTable.php +++ b/tests/Integration/ORM/Migrations/CreateCasterModelTable.php @@ -4,7 +4,10 @@ use Tempest\Database\DatabaseMigration; use Tempest\Database\QueryStatement; +use Tempest\Database\QueryStatements\CompoundStatement; +use Tempest\Database\QueryStatements\CreateEnumTypeStatement; use Tempest\Database\QueryStatements\CreateTableStatement; +use Tempest\Database\QueryStatements\DropEnumTypeStatement; use Tempest\Database\QueryStatements\DropTableStatement; use Tests\Tempest\Integration\ORM\Models\CasterEnum; use Tests\Tempest\Integration\ORM\Models\CasterModel; @@ -15,11 +18,15 @@ final class CreateCasterModelTable implements DatabaseMigration public function up(): QueryStatement { - return CreateTableStatement::forModel(CasterModel::class) - ->primary() - ->datetime('date') - ->array('array') - ->enum('enum', CasterEnum::class); + return new CompoundStatement( + new DropEnumTypeStatement(CasterEnum::class), + new CreateEnumTypeStatement(CasterEnum::class), + CreateTableStatement::forModel(CasterModel::class) + ->primary() + ->datetime('date') + ->array('array_prop') + ->enum('enum_prop', CasterEnum::class), + ); } public function down(): QueryStatement diff --git a/tests/Integration/ORM/Models/CasterModel.php b/tests/Integration/ORM/Models/CasterModel.php index 0470fa089..9a3a8a375 100644 --- a/tests/Integration/ORM/Models/CasterModel.php +++ b/tests/Integration/ORM/Models/CasterModel.php @@ -11,7 +11,7 @@ final class CasterModel public function __construct( public DateTimeImmutable $date, - public array $array, - public CasterEnum $enum, + public array $array_prop, + public CasterEnum $enum_prop, ) {} } diff --git a/tests/Integration/TestingDatabaseInitializer.php b/tests/Integration/TestingDatabaseInitializer.php new file mode 100644 index 000000000..41527801f --- /dev/null +++ b/tests/Integration/TestingDatabaseInitializer.php @@ -0,0 +1,51 @@ +getType()->matches(Database::class); + } + + #[Singleton] + public function initialize(ClassReflector $class, ?string $tag, Container $container): Database + { + if (self::$connection === null) { + $config = $container->get(DatabaseConfig::class, $tag); + $connection = new PDOConnection($config); + $connection->connect(); + + self::$connection = $connection; + } + + if (self::$connection->ping() === false) { + self::$connection->reconnect(); + } + + $container->singleton(Connection::class, self::$connection); + + return new GenericDatabase( + self::$connection, + new GenericTransactionManager(self::$connection), + $container->get(DatabaseDialect::class), + ); + } +}