From a12ac6b6b84ba52d12db07d14ee0976bffd25160 Mon Sep 17 00:00:00 2001 From: brendt Date: Tue, 20 May 2025 08:39:20 +0200 Subject: [PATCH 01/29] wip --- .github/workflows/integration-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 1c5326f7a..4e2fcb94d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -96,6 +96,9 @@ jobs: - 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');" + - name: List discovered locations + run: php ./tempest about + - name: List discovered locations run: php ./tempest discovery:status From f7aa348448aa174bd562eca4a425573b267a2b86 Mon Sep 17 00:00:00 2001 From: brendt Date: Tue, 20 May 2025 08:40:04 +0200 Subject: [PATCH 02/29] wip --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 4e2fcb94d..71bd8589b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -96,7 +96,7 @@ jobs: - 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');" - - name: List discovered locations + - name: Tempest about run: php ./tempest about - name: List discovered locations From d9d1ff2bfdda179e5c3f258a229687232fe4a284 Mon Sep 17 00:00:00 2001 From: brendt Date: Tue, 20 May 2025 08:49:38 +0200 Subject: [PATCH 03/29] wip --- .github/workflows/integration-tests.yml | 2 +- packages/database/src/GenericDatabase.php | 2 +- .../Config/{database.postgresql.php => database.postgres.php} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename tests/Fixtures/Config/{database.postgresql.php => database.postgres.php} (100%) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 71bd8589b..b235bec1a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -94,7 +94,7 @@ 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 diff --git a/packages/database/src/GenericDatabase.php b/packages/database/src/GenericDatabase.php index 0f7b7e1aa..b9d13cf76 100644 --- a/packages/database/src/GenericDatabase.php +++ b/packages/database/src/GenericDatabase.php @@ -13,7 +13,7 @@ use Tempest\Database\Transactions\TransactionManager; use Throwable; -final readonly class GenericDatabase implements Database +final class GenericDatabase implements Database { public function __construct( private(set) Connection $connection, diff --git a/tests/Fixtures/Config/database.postgresql.php b/tests/Fixtures/Config/database.postgres.php similarity index 100% rename from tests/Fixtures/Config/database.postgresql.php rename to tests/Fixtures/Config/database.postgres.php From 8b02466afd8c1272f13a8dba9f4a1d95259544f5 Mon Sep 17 00:00:00 2001 From: brendt Date: Tue, 20 May 2025 08:57:08 +0200 Subject: [PATCH 04/29] wip --- packages/database/src/GenericDatabase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/database/src/GenericDatabase.php b/packages/database/src/GenericDatabase.php index b9d13cf76..0f7b7e1aa 100644 --- a/packages/database/src/GenericDatabase.php +++ b/packages/database/src/GenericDatabase.php @@ -13,7 +13,7 @@ use Tempest\Database\Transactions\TransactionManager; use Throwable; -final class GenericDatabase implements Database +final readonly class GenericDatabase implements Database { public function __construct( private(set) Connection $connection, From b0d6ceef74ba2ef3947d215174b969efed8480cb Mon Sep 17 00:00:00 2001 From: brendt Date: Tue, 20 May 2025 11:02:11 +0200 Subject: [PATCH 05/29] wip --- .../QueryBuilders/UpdateQueryBuilder.php | 2 +- .../database/src/Connection/Connection.php | 2 +- .../database/src/Connection/PDOConnection.php | 12 +++- packages/database/src/Database.php | 2 +- .../src/DatabaseDialectInitializer.php | 16 ++++++ packages/database/src/DatabaseInitializer.php | 1 + .../Exceptions/NoLastInsertIdAvailable.php | 13 +++++ packages/database/src/GenericDatabase.php | 38 ++++++++++--- .../src/Migrations/MigrationManager.php | 3 +- packages/database/src/Query.php | 11 +++- .../QueryStatements/BelongsToStatement.php | 57 ++++++++++++------- .../QueryStatements/CanExecuteStatement.php | 10 +++- .../QueryStatements/ConstraintStatement.php | 1 - .../src/QueryStatements/DatetimeStatement.php | 20 +++++-- .../QueryStatements/DropTableStatement.php | 5 +- .../src/QueryStatements/FieldStatement.php | 5 +- .../src/QueryStatements/InsertStatement.php | 8 ++- .../SetForeignKeyChecksStatement.php | 2 +- .../QueryStatements/ShowTablesStatement.php | 2 +- .../src/QueryStatements/TextStatement.php | 6 ++ .../database/tests/GenericDatabaseTest.php | 3 + .../ORM/Mappers/QueryMapperTest.php | 30 ++++++++-- 22 files changed, 193 insertions(+), 56 deletions(-) create mode 100644 packages/database/src/DatabaseDialectInitializer.php create mode 100644 packages/database/src/Exceptions/NoLastInsertIdAvailable.php diff --git a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php index 4455aa707..5aef67b92 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|null { return $this->build()->execute(...$bindings); } 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..f018a8ee1 100644 --- a/packages/database/src/Connection/PDOConnection.php +++ b/packages/database/src/Connection/PDOConnection.php @@ -13,6 +13,8 @@ final class PDOConnection implements Connection { private ?PDO $pdo = null; + private int|string|null $lastInsertId = null; + public function __construct( private(set) readonly DatabaseConfig $config, ) {} @@ -53,13 +55,19 @@ 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 close(): void diff --git a/packages/database/src/Database.php b/packages/database/src/Database.php index 27e7053e8..8ac677fb3 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|null; 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..0138112e3 --- /dev/null +++ b/packages/database/src/DatabaseDialectInitializer.php @@ -0,0 +1,16 @@ +get(DatabaseConfig::class)->dialect; + } +} \ No newline at end of file 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..d46aa394c --- /dev/null +++ b/packages/database/src/Exceptions/NoLastInsertIdAvailable.php @@ -0,0 +1,13 @@ +resolveBindings($query); try { - $this->connection - ->prepare($query->toSql()) - ->execute($bindings); + $statement = $this->connection->prepare($query->toSql()); + + $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|null { - return new Id($this->connection->lastInsertId()); + $sql = $this->lastQuery->toSql(); + + 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 new Id($lastInsertId); } public function fetch(Query $query): array diff --git a/packages/database/src/Migrations/MigrationManager.php b/packages/database/src/Migrations/MigrationManager.php index fb14929fb..4aa58bc33 100644 --- a/packages/database/src/Migrations/MigrationManager.php +++ b/packages/database/src/Migrations/MigrationManager.php @@ -258,7 +258,8 @@ private function getTableDefinitions(DatabaseDialect $dialect): array return array_map( fn (array $item) => match ($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), ); diff --git a/packages/database/src/Query.php b/packages/database/src/Query.php index 1457be5d3..6dd47f711 100644 --- a/packages/database/src/Query.php +++ b/packages/database/src/Query.php @@ -6,6 +6,7 @@ use Tempest\Database\Config\DatabaseConfig; +use Tempest\Database\Config\DatabaseDialect; use function Tempest\get; final class Query @@ -17,7 +18,7 @@ public function __construct( public array $executeAfter = [], ) {} - public function execute(mixed ...$bindings): Id + public function execute(mixed ...$bindings): Id|null { $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/CanExecuteStatement.php b/packages/database/src/QueryStatements/CanExecuteStatement.php index 6340ea679..629798225 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|null { - 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/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/DatetimeStatement.php b/packages/database/src/QueryStatements/DatetimeStatement.php index ad9626ca2..21c415c01 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/DropTableStatement.php b/packages/database/src/QueryStatements/DropTableStatement.php index 90aa89df4..9ac7943a7 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/FieldStatement.php b/packages/database/src/QueryStatements/FieldStatement.php index 8ee8d6c29..949103af2 100644 --- a/packages/database/src/QueryStatements/FieldStatement.php +++ b/packages/database/src/QueryStatements/FieldStatement.php @@ -58,7 +58,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/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..b7b28a83f 100644 --- a/packages/database/src/QueryStatements/ShowTablesStatement.php +++ b/packages/database/src/QueryStatements/ShowTablesStatement.php @@ -23,7 +23,7 @@ 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..9c43163ff 100644 --- a/packages/database/src/QueryStatements/TextStatement.php +++ b/packages/database/src/QueryStatements/TextStatement.php @@ -23,6 +23,12 @@ public function compile(DatabaseDialect $dialect): string $this->name, $this->nullable ? '' : 'NOT NULL', ), + DatabaseDialect::POSTGRESQL => sprintf( + '`%s` TEXT %s %s', + $this->name, + $this->default !== null ? "DEFAULT '{$this->default}'" : '', + $this->nullable ? '' : 'NOT NULL', + ), default => sprintf( '`%s` TEXT %s %s', $this->name, 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/tests/Integration/ORM/Mappers/QueryMapperTest.php b/tests/Integration/ORM/Mappers/QueryMapperTest.php index 915235e15..7cf91c15f 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' + $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, $query->toSql()); + 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' + $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, $query->toSql()); + SQL, + }; + + $this->assertSame($expected, $query->toSql()); $this->assertSame(['other', 1], $query->bindings); } From 76f204e004a227238858cac8eae17a96e2557e9a Mon Sep 17 00:00:00 2001 From: brendt Date: Tue, 20 May 2025 14:15:06 +0200 Subject: [PATCH 06/29] wip --- .../database/src/Connection/PDOConnection.php | 2 - packages/database/src/GenericDatabase.php | 17 +++++-- packages/database/src/Id.php | 9 ++++ packages/database/src/IsDatabaseModel.php | 6 ++- .../src/QueryStatements/BooleanStatement.php | 11 ++++- .../src/QueryStatements/CharStatement.php | 2 +- .../src/QueryStatements/CompoundStatement.php | 29 +++++++++++ .../src/QueryStatements/CountStatement.php | 17 ++++--- .../CreateEnumTypeStatement.php | 8 +-- .../QueryStatements/CreateTableStatement.php | 48 +++++++++++------- .../src/QueryStatements/DateStatement.php | 2 +- .../src/QueryStatements/DatetimeStatement.php | 2 +- .../QueryStatements/DropEnumTypeStatement.php | 34 +++++++++++++ .../src/QueryStatements/EnumStatement.php | 4 +- .../src/QueryStatements/JsonStatement.php | 2 +- .../src/QueryStatements/TextStatement.php | 8 +-- .../src/QueryStatements/VarcharStatement.php | 2 +- .../Builder/CountQueryBuilderTest.php | 41 +++++++++++----- .../Builder/DeleteQueryBuilderTest.php | 10 ++-- .../Builder/InsertQueryBuilderTest.php | 49 +++++++++++++------ .../Builder/SelectQueryBuilderTest.php | 10 +--- .../Builder/UpdateQueryBuilderTest.php | 29 ++++++----- .../Database/Fixtures/EnumForCreateTable.php | 1 - .../AlterTableStatementTest.php | 2 +- .../QueryStatements/CompoundStatementTest.php | 27 ++++++++++ .../CreateEnumTypeStatementTest.php | 10 ++-- .../CreateTableStatementTest.php | 47 +++++++++++++----- .../DropEnumTypeStatementTest.php | 25 ++++++++++ .../QueryStatements/EnumStatementTest.php | 4 +- .../FrameworkIntegrationTestCase.php | 15 ++++++ 30 files changed, 341 insertions(+), 132 deletions(-) create mode 100644 packages/database/src/QueryStatements/CompoundStatement.php create mode 100644 packages/database/src/QueryStatements/DropEnumTypeStatement.php create mode 100644 tests/Integration/Database/QueryStatements/CompoundStatementTest.php create mode 100644 tests/Integration/Database/QueryStatements/DropEnumTypeStatementTest.php diff --git a/packages/database/src/Connection/PDOConnection.php b/packages/database/src/Connection/PDOConnection.php index f018a8ee1..a13882d7b 100644 --- a/packages/database/src/Connection/PDOConnection.php +++ b/packages/database/src/Connection/PDOConnection.php @@ -13,8 +13,6 @@ final class PDOConnection implements Connection { private ?PDO $pdo = null; - private int|string|null $lastInsertId = null; - public function __construct( private(set) readonly DatabaseConfig $config, ) {} diff --git a/packages/database/src/GenericDatabase.php b/packages/database/src/GenericDatabase.php index 8f3a120ed..d7cbffa44 100644 --- a/packages/database/src/GenericDatabase.php +++ b/packages/database/src/GenericDatabase.php @@ -31,12 +31,18 @@ public function execute(Query $query): void $bindings = $this->resolveBindings($query); try { - $statement = $this->connection->prepare($query->toSql()); + foreach (explode(';', $query->toSql()) as $sql) { + if (! $sql) { + continue; + } - $statement->execute($bindings); + $statement = $this->connection->prepare($sql . ';'); - $this->lastStatement = $statement; - $this->lastQuery = $query; + $statement->execute($bindings); + + $this->lastStatement = $statement; + $this->lastQuery = $query; + } } catch (PDOException $pdoException) { throw new QueryException($query, $bindings, $pdoException); } @@ -46,6 +52,7 @@ public function getLastInsertId(): Id|null { $sql = $this->lastQuery->toSql(); + // TODO: properly determine whether a query is an insert or not if (! str_starts_with($sql, 'INSERT')) { return null; } @@ -57,7 +64,7 @@ public function getLastInsertId(): Id|null $lastInsertId = $this->connection->lastInsertId(); } - return new Id($lastInsertId); + return Id::tryFrom($lastInsertId); } public function fetch(Query $query): array diff --git a/packages/database/src/Id.php b/packages/database/src/Id.php index 633fcb59c..156e58ca2 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|null + { + 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/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/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/CountStatement.php b/packages/database/src/QueryStatements/CountStatement.php index 182cecb10..4a7d068b9 100644 --- a/packages/database/src/QueryStatements/CountStatement.php +++ b/packages/database/src/QueryStatements/CountStatement.php @@ -21,18 +21,21 @@ 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), ]); if ($this->where->isNotEmpty()) { $query[] = 'WHERE ' . $this->where - ->map(fn (WhereStatement $where) => $where->compile($dialect)) - ->implode(PHP_EOL); + ->map(fn (WhereStatement $where) => $where->compile($dialect)) + ->implode(PHP_EOL); } return $query->implode(PHP_EOL); @@ -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..8223ad084 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; @@ -43,7 +42,8 @@ public function belongsTo( OnDelete $onDelete = OnDelete::RESTRICT, OnUpdate $onUpdate = OnUpdate::NO_ACTION, bool $nullable = false, - ): self { + ): self + { [, $localKey] = explode('.', $local); $this->integer($localKey, nullable: $nullable); @@ -62,7 +62,8 @@ public function text( string $name, bool $nullable = false, ?string $default = null, - ): self { + ): self + { $this->statements[] = new TextStatement( name: $name, nullable: $nullable, @@ -77,7 +78,8 @@ public function varchar( int $length = 255, bool $nullable = false, ?string $default = null, - ): self { + ): self + { $this->statements[] = new VarcharStatement( name: $name, size: $length, @@ -92,7 +94,8 @@ public function char( string $name, bool $nullable = false, ?string $default = null, - ): self { + ): self + { $this->statements[] = new CharStatement( name: $name, nullable: $nullable, @@ -107,7 +110,8 @@ public function integer( bool $unsigned = false, bool $nullable = false, ?int $default = null, - ): self { + ): self + { $this->statements[] = new IntegerStatement( name: $name, unsigned: $unsigned, @@ -122,7 +126,8 @@ public function float( string $name, bool $nullable = false, ?float $default = null, - ): self { + ): self + { $this->statements[] = new FloatStatement( name: $name, nullable: $nullable, @@ -136,7 +141,8 @@ public function datetime( string $name, bool $nullable = false, ?string $default = null, - ): self { + ): self + { $this->statements[] = new DatetimeStatement( name: $name, nullable: $nullable, @@ -150,7 +156,8 @@ public function date( string $name, bool $nullable = false, ?string $default = null, - ): self { + ): self + { $this->statements[] = new DateStatement( name: $name, nullable: $nullable, @@ -164,7 +171,8 @@ public function boolean( string $name, bool $nullable = false, ?bool $default = null, - ): self { + ): self + { $this->statements[] = new BooleanStatement( name: $name, nullable: $nullable, @@ -178,7 +186,8 @@ public function json( string $name, bool $nullable = false, ?string $default = null, - ): self { + ): self + { $this->statements[] = new JsonStatement( name: $name, nullable: $nullable, @@ -192,7 +201,8 @@ public function array( string $name, bool $nullable = false, array $default = [], - ): self { + ): self + { $this->statements[] = new JsonStatement( name: $name, nullable: $nullable, @@ -207,9 +217,8 @@ public function enum( string $enumClass, bool $nullable = false, null|UnitEnum|BackedEnum $default = null, - ): self { - $this->statements[] = new CreateEnumTypeStatement($enumClass); - + ): self + { $this->statements[] = new EnumStatement( name: $name, enumClass: $enumClass, @@ -225,7 +234,8 @@ public function set( array $values, bool $nullable = false, ?string $default = null, - ): self { + ): self + { $this->statements[] = new SetStatement( name: $name, values: $values, @@ -280,9 +290,9 @@ public function compile(DatabaseDialect $dialect): string if ($this->indexStatements !== []) { $createIndices = PHP_EOL . arr($this->indexStatements) - ->map(fn (QueryStatement $queryStatement) => str($queryStatement->compile($dialect))->trim()->replace(' ', ' ')) - ->implode(';' . PHP_EOL) - ->append(';'); + ->map(fn (QueryStatement $queryStatement) => str($queryStatement->compile($dialect))->trim()->replace(' ', ' ')) + ->implode(';' . PHP_EOL) + ->append(';'); } else { $createIndices = ''; } 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 21c415c01..88353e1c9 100644 --- a/packages/database/src/QueryStatements/DatetimeStatement.php +++ b/packages/database/src/QueryStatements/DatetimeStatement.php @@ -27,7 +27,7 @@ public function compile(DatabaseDialect $dialect): string default => sprintf( '`%s` DATETIME %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/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/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/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/TextStatement.php b/packages/database/src/QueryStatements/TextStatement.php index 9c43163ff..0d65cbccb 100644 --- a/packages/database/src/QueryStatements/TextStatement.php +++ b/packages/database/src/QueryStatements/TextStatement.php @@ -23,16 +23,10 @@ public function compile(DatabaseDialect $dialect): string $this->name, $this->nullable ? '' : 'NOT NULL', ), - DatabaseDialect::POSTGRESQL => sprintf( - '`%s` TEXT %s %s', - $this->name, - $this->default !== null ? "DEFAULT '{$this->default}'" : '', - $this->nullable ? '' : 'NOT NULL', - ), 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/tests/Integration/Database/Builder/CountQueryBuilderTest.php b/tests/Integration/Database/Builder/CountQueryBuilderTest.php index 891869c04..a83785424 100644 --- a/tests/Integration/Database/Builder/CountQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/CountQueryBuilderTest.php @@ -6,6 +6,9 @@ use Tempest\Database\Builder\QueryBuilders\CountQueryBuilder; use Tempest\Database\Exceptions\CannotCountDistinctWithoutSpecifyingAColumn; +use Tempest\Database\Migrations\CreateMigrationsTable; +use Tests\Tempest\Fixtures\Migrations\CreateAuthorTable; +use Tests\Tempest\Fixtures\Migrations\CreatePublishersTable; use Tests\Tempest\Fixtures\Modules\Books\Models\Author; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -26,7 +29,7 @@ public function test_simple_count_query(): void ->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 +60,14 @@ public function test_insert_with_batch(): void ->insert(...$arrayOfStuff) ->build(); - $this->assertSame( - <<buildExpectedInsert(<<assertSameWithoutBackticks( + $expected, $query->toSql(), ); @@ -84,12 +91,13 @@ 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 +116,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 +152,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 +228,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..a036418f3 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..8098d8f6b 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(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..f80bd4ffd 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..3ae5878f5 100644 --- a/tests/Integration/FrameworkIntegrationTestCase.php +++ b/tests/Integration/FrameworkIntegrationTestCase.php @@ -176,4 +176,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), + ); + } } From e4a97bd4c2b4e5cbc048872267d4689632c2d73d Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 08:39:54 +0200 Subject: [PATCH 07/29] wip --- .../database/src/Config/DatabaseDialect.php | 11 ++++ packages/database/src/GenericDatabase.php | 6 +-- .../src/Migrations/MigrationManager.php | 53 ++++++++----------- .../Database/GenericDatabaseTest.php | 31 ++++++----- 4 files changed, 55 insertions(+), 46 deletions(-) diff --git a/packages/database/src/Config/DatabaseDialect.php b/packages/database/src/Config/DatabaseDialect.php index 9f7f8b183..e703c5c48 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/GenericDatabase.php b/packages/database/src/GenericDatabase.php index d7cbffa44..1b1c6a945 100644 --- a/packages/database/src/GenericDatabase.php +++ b/packages/database/src/GenericDatabase.php @@ -69,9 +69,11 @@ public function getLastInsertId(): Id|null 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); } @@ -89,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/Migrations/MigrationManager.php b/packages/database/src/Migrations/MigrationManager.php index 4aa58bc33..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,15 +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']), DatabaseDialect::POSTGRESQL => new TableMigrationDefinition($item['table_name']), DatabaseDialect::MYSQL => new TableMigrationDefinition(array_values($item)[0]), }, - new ShowTablesStatement()->fetch($dialect), + new ShowTablesStatement()->fetch($this->dialect), ); } @@ -279,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/tests/Integration/Database/GenericDatabaseTest.php b/tests/Integration/Database/GenericDatabaseTest.php index 9b2d5e217..2085339e1 100644 --- a/tests/Integration/Database/GenericDatabaseTest.php +++ b/tests/Integration/Database/GenericDatabaseTest.php @@ -7,11 +7,11 @@ use Exception; use Tempest\Database\Database; use Tempest\Database\Migrations\CreateMigrationsTable; -use Tempest\Database\Migrations\Migration; use Tests\Tempest\Fixtures\Migrations\CreateAuthorTable; use Tests\Tempest\Fixtures\Migrations\CreatePublishersTable; use Tests\Tempest\Fixtures\Modules\Books\Models\Author; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; +use function Tempest\Database\query; /** * @internal @@ -20,28 +20,33 @@ final class GenericDatabaseTest extends FrameworkIntegrationTestCase { public function test_transaction_manager_execute(): void { - $manager = $this->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 (): void { + 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()); } } From ecb1086c1c2ab5bdea714d00ce77be0c37a44c8c Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 09:14:31 +0200 Subject: [PATCH 08/29] wip --- packages/container/src/GenericContainer.php | 2 +- .../CachedConnectionInitializer.php | 35 ------------- .../database/src/Connection/PDOConnection.php | 19 +++++++ .../Framework/Testing/IntegrationTest.php | 3 +- .../FrameworkIntegrationTestCase.php | 12 +++-- .../TestingDatabaseInitializer.php | 50 +++++++++++++++++++ 6 files changed, 79 insertions(+), 42 deletions(-) delete mode 100644 packages/database/src/Connection/CachedConnectionInitializer.php create mode 100644 tests/Integration/TestingDatabaseInitializer.php 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/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/PDOConnection.php b/packages/database/src/Connection/PDOConnection.php index a13882d7b..99d7b93fe 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 { @@ -68,6 +69,24 @@ public function prepare(string $sql): PDOStatement 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 { $this->pdo = null; diff --git a/src/Tempest/Framework/Testing/IntegrationTest.php b/src/Tempest/Framework/Testing/IntegrationTest.php index ee46ed16f..7ae73a0ef 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 Container|GenericContainer $container; protected ConsoleTester $console; diff --git a/tests/Integration/FrameworkIntegrationTestCase.php b/tests/Integration/FrameworkIntegrationTestCase.php index 3ae5878f5..a6cd24e8c 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 diff --git a/tests/Integration/TestingDatabaseInitializer.php b/tests/Integration/TestingDatabaseInitializer.php new file mode 100644 index 000000000..f2e723c88 --- /dev/null +++ b/tests/Integration/TestingDatabaseInitializer.php @@ -0,0 +1,50 @@ +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(); + } + + return new GenericDatabase( + self::$connection, + new GenericTransactionManager(self::$connection), + $container->get(DatabaseDialect::class), + ); + } +} From 1c1d539bb237495dd5c113da0aae91887f1b3db0 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 09:35:20 +0200 Subject: [PATCH 09/29] wip --- packages/database/src/Config/PostgresConfig.php | 2 +- .../database/src/Transactions/GenericTransactionManager.php | 2 +- tests/Integration/Database/GenericTransactionManagerTest.php | 1 + tests/Integration/TestingDatabaseInitializer.php | 5 +++-- 4 files changed, 6 insertions(+), 4 deletions(-) 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/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/tests/Integration/Database/GenericTransactionManagerTest.php b/tests/Integration/Database/GenericTransactionManagerTest.php index d2d6ad71a..09d51bfe1 100644 --- a/tests/Integration/Database/GenericTransactionManagerTest.php +++ b/tests/Integration/Database/GenericTransactionManagerTest.php @@ -11,6 +11,7 @@ use Tests\Tempest\Fixtures\Migrations\CreatePublishersTable; use Tests\Tempest\Fixtures\Modules\Books\Models\Author; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; +use function Tempest\Database\query; /** * @internal diff --git a/tests/Integration/TestingDatabaseInitializer.php b/tests/Integration/TestingDatabaseInitializer.php index f2e723c88..41527801f 100644 --- a/tests/Integration/TestingDatabaseInitializer.php +++ b/tests/Integration/TestingDatabaseInitializer.php @@ -36,11 +36,12 @@ public function initialize(ClassReflector $class, ?string $tag, Container $conta self::$connection = $connection; } - if (self::$connection->ping() === false) - { + if (self::$connection->ping() === false) { self::$connection->reconnect(); } + $container->singleton(Connection::class, self::$connection); + return new GenericDatabase( self::$connection, new GenericTransactionManager(self::$connection), From abeef6fc72ac1fdff18d0ebb12174ec5f23839ff Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 09:40:44 +0200 Subject: [PATCH 10/29] wip --- tests/Integration/ORM/IsDatabaseModelTest.php | 9 +++++---- .../ORM/Migrations/CreateCasterModelTable.php | 17 ++++++++++++----- tests/Integration/ORM/Models/CasterModel.php | 4 ++-- 3 files changed, 19 insertions(+), 11 deletions(-) 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/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, ) {} } From 7a0de89be8b32bee7e1db1652d67566cf4eb2fa0 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 09:41:41 +0200 Subject: [PATCH 11/29] wip --- .../QueryBuilders/UpdateQueryBuilder.php | 2 +- .../database/src/Config/DatabaseDialect.php | 2 +- packages/database/src/Database.php | 2 +- .../src/DatabaseDialectInitializer.php | 2 +- .../Exceptions/NoLastInsertIdAvailable.php | 4 +- packages/database/src/GenericDatabase.php | 6 +-- packages/database/src/Id.php | 2 +- packages/database/src/Query.php | 4 +- .../QueryStatements/CanExecuteStatement.php | 2 +- .../src/QueryStatements/CountStatement.php | 4 +- .../QueryStatements/CreateTableStatement.php | 45 +++++++------------ .../src/QueryStatements/DatetimeStatement.php | 4 +- .../QueryStatements/DropTableStatement.php | 2 +- .../src/QueryStatements/FieldStatement.php | 2 +- .../QueryStatements/ShowTablesStatement.php | 3 +- .../Builder/InsertQueryBuilderTest.php | 17 +++---- .../Builder/SelectQueryBuilderTest.php | 2 +- .../Builder/UpdateQueryBuilderTest.php | 6 +-- .../Database/GenericDatabaseTest.php | 3 +- .../GenericTransactionManagerTest.php | 1 + .../CreateTableStatementTest.php | 2 +- .../FrameworkIntegrationTestCase.php | 2 +- .../ORM/Mappers/QueryMapperTest.php | 28 ++++++------ 23 files changed, 67 insertions(+), 80 deletions(-) diff --git a/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php b/packages/database/src/Builder/QueryBuilders/UpdateQueryBuilder.php index 5aef67b92..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|null + 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 e703c5c48..c2d3209be 100644 --- a/packages/database/src/Config/DatabaseDialect.php +++ b/packages/database/src/Config/DatabaseDialect.php @@ -23,7 +23,7 @@ public function tableNotFoundCode(): string public function isTableNotFoundError(PDOException $exception): bool { - return match($this) { + 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/Database.php b/packages/database/src/Database.php index 8ac677fb3..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|null; + public function getLastInsertId(): ?Id; public function fetch(Query $query): array; diff --git a/packages/database/src/DatabaseDialectInitializer.php b/packages/database/src/DatabaseDialectInitializer.php index 0138112e3..45ccda28b 100644 --- a/packages/database/src/DatabaseDialectInitializer.php +++ b/packages/database/src/DatabaseDialectInitializer.php @@ -13,4 +13,4 @@ public function initialize(Container $container): DatabaseDialect { return $container->get(DatabaseConfig::class)->dialect; } -} \ No newline at end of file +} diff --git a/packages/database/src/Exceptions/NoLastInsertIdAvailable.php b/packages/database/src/Exceptions/NoLastInsertIdAvailable.php index d46aa394c..4e641d315 100644 --- a/packages/database/src/Exceptions/NoLastInsertIdAvailable.php +++ b/packages/database/src/Exceptions/NoLastInsertIdAvailable.php @@ -8,6 +8,6 @@ final class NoLastInsertIdAvailable extends Exception { public function __construct() { - parent::__construct("No last insert id available."); + parent::__construct('No last insert id available.'); } -} \ No newline at end of file +} diff --git a/packages/database/src/GenericDatabase.php b/packages/database/src/GenericDatabase.php index 1b1c6a945..874b05bb8 100644 --- a/packages/database/src/GenericDatabase.php +++ b/packages/database/src/GenericDatabase.php @@ -17,8 +17,8 @@ final class GenericDatabase implements Database { - private PDOStatement|null $lastStatement = null; - private Query|null $lastQuery = null; + private ?PDOStatement $lastStatement = null; + private ?Query $lastQuery = null; public function __construct( private(set) readonly Connection $connection, @@ -48,7 +48,7 @@ public function execute(Query $query): void } } - public function getLastInsertId(): Id|null + public function getLastInsertId(): ?Id { $sql = $this->lastQuery->toSql(); diff --git a/packages/database/src/Id.php b/packages/database/src/Id.php index 156e58ca2..7d74aadc7 100644 --- a/packages/database/src/Id.php +++ b/packages/database/src/Id.php @@ -13,7 +13,7 @@ { public string|int $id; - public static function tryFrom(string|int|self|null $id): self|null + public static function tryFrom(string|int|self|null $id): ?self { if ($id === null) { return null; diff --git a/packages/database/src/Query.php b/packages/database/src/Query.php index 6dd47f711..1b1df535e 100644 --- a/packages/database/src/Query.php +++ b/packages/database/src/Query.php @@ -5,8 +5,8 @@ namespace Tempest\Database; use Tempest\Database\Config\DatabaseConfig; - use Tempest\Database\Config\DatabaseDialect; + use function Tempest\get; final class Query @@ -18,7 +18,7 @@ public function __construct( public array $executeAfter = [], ) {} - public function execute(mixed ...$bindings): Id|null + public function execute(mixed ...$bindings): ?Id { $this->bindings = [...$this->bindings, ...$bindings]; diff --git a/packages/database/src/QueryStatements/CanExecuteStatement.php b/packages/database/src/QueryStatements/CanExecuteStatement.php index 629798225..a06a4e235 100644 --- a/packages/database/src/QueryStatements/CanExecuteStatement.php +++ b/packages/database/src/QueryStatements/CanExecuteStatement.php @@ -10,7 +10,7 @@ trait CanExecuteStatement { - public function execute(DatabaseDialect $dialect): Id|null + public function execute(DatabaseDialect $dialect): ?Id { $sql = $this->compile($dialect); diff --git a/packages/database/src/QueryStatements/CountStatement.php b/packages/database/src/QueryStatements/CountStatement.php index 4a7d068b9..d94a2e90c 100644 --- a/packages/database/src/QueryStatements/CountStatement.php +++ b/packages/database/src/QueryStatements/CountStatement.php @@ -34,8 +34,8 @@ public function compile(DatabaseDialect $dialect): string if ($this->where->isNotEmpty()) { $query[] = 'WHERE ' . $this->where - ->map(fn (WhereStatement $where) => $where->compile($dialect)) - ->implode(PHP_EOL); + ->map(fn (WhereStatement $where) => $where->compile($dialect)) + ->implode(PHP_EOL); } return $query->implode(PHP_EOL); diff --git a/packages/database/src/QueryStatements/CreateTableStatement.php b/packages/database/src/QueryStatements/CreateTableStatement.php index 8223ad084..26c840f77 100644 --- a/packages/database/src/QueryStatements/CreateTableStatement.php +++ b/packages/database/src/QueryStatements/CreateTableStatement.php @@ -42,8 +42,7 @@ public function belongsTo( OnDelete $onDelete = OnDelete::RESTRICT, OnUpdate $onUpdate = OnUpdate::NO_ACTION, bool $nullable = false, - ): self - { + ): self { [, $localKey] = explode('.', $local); $this->integer($localKey, nullable: $nullable); @@ -62,8 +61,7 @@ public function text( string $name, bool $nullable = false, ?string $default = null, - ): self - { + ): self { $this->statements[] = new TextStatement( name: $name, nullable: $nullable, @@ -78,8 +76,7 @@ public function varchar( int $length = 255, bool $nullable = false, ?string $default = null, - ): self - { + ): self { $this->statements[] = new VarcharStatement( name: $name, size: $length, @@ -94,8 +91,7 @@ public function char( string $name, bool $nullable = false, ?string $default = null, - ): self - { + ): self { $this->statements[] = new CharStatement( name: $name, nullable: $nullable, @@ -110,8 +106,7 @@ public function integer( bool $unsigned = false, bool $nullable = false, ?int $default = null, - ): self - { + ): self { $this->statements[] = new IntegerStatement( name: $name, unsigned: $unsigned, @@ -126,8 +121,7 @@ public function float( string $name, bool $nullable = false, ?float $default = null, - ): self - { + ): self { $this->statements[] = new FloatStatement( name: $name, nullable: $nullable, @@ -141,8 +135,7 @@ public function datetime( string $name, bool $nullable = false, ?string $default = null, - ): self - { + ): self { $this->statements[] = new DatetimeStatement( name: $name, nullable: $nullable, @@ -156,8 +149,7 @@ public function date( string $name, bool $nullable = false, ?string $default = null, - ): self - { + ): self { $this->statements[] = new DateStatement( name: $name, nullable: $nullable, @@ -171,8 +163,7 @@ public function boolean( string $name, bool $nullable = false, ?bool $default = null, - ): self - { + ): self { $this->statements[] = new BooleanStatement( name: $name, nullable: $nullable, @@ -186,8 +177,7 @@ public function json( string $name, bool $nullable = false, ?string $default = null, - ): self - { + ): self { $this->statements[] = new JsonStatement( name: $name, nullable: $nullable, @@ -201,8 +191,7 @@ public function array( string $name, bool $nullable = false, array $default = [], - ): self - { + ): self { $this->statements[] = new JsonStatement( name: $name, nullable: $nullable, @@ -217,8 +206,7 @@ public function enum( string $enumClass, bool $nullable = false, null|UnitEnum|BackedEnum $default = null, - ): self - { + ): self { $this->statements[] = new EnumStatement( name: $name, enumClass: $enumClass, @@ -234,8 +222,7 @@ public function set( array $values, bool $nullable = false, ?string $default = null, - ): self - { + ): self { $this->statements[] = new SetStatement( name: $name, values: $values, @@ -290,9 +277,9 @@ public function compile(DatabaseDialect $dialect): string if ($this->indexStatements !== []) { $createIndices = PHP_EOL . arr($this->indexStatements) - ->map(fn (QueryStatement $queryStatement) => str($queryStatement->compile($dialect))->trim()->replace(' ', ' ')) - ->implode(';' . PHP_EOL) - ->append(';'); + ->map(fn (QueryStatement $queryStatement) => str($queryStatement->compile($dialect))->trim()->replace(' ', ' ')) + ->implode(';' . PHP_EOL) + ->append(';'); } else { $createIndices = ''; } diff --git a/packages/database/src/QueryStatements/DatetimeStatement.php b/packages/database/src/QueryStatements/DatetimeStatement.php index 88353e1c9..99ed570dd 100644 --- a/packages/database/src/QueryStatements/DatetimeStatement.php +++ b/packages/database/src/QueryStatements/DatetimeStatement.php @@ -17,7 +17,7 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { - return match($dialect){ + return match ($dialect) { DatabaseDialect::POSTGRESQL => sprintf( '`%s` TIMESTAMP %s %s', $this->name, @@ -29,7 +29,7 @@ public function compile(DatabaseDialect $dialect): string $this->name, $this->default !== null ? "DEFAULT '{$this->default}'" : '', $this->nullable ? '' : 'NOT NULL', - ) + ), }; } } diff --git a/packages/database/src/QueryStatements/DropTableStatement.php b/packages/database/src/QueryStatements/DropTableStatement.php index 9ac7943a7..c52959221 100644 --- a/packages/database/src/QueryStatements/DropTableStatement.php +++ b/packages/database/src/QueryStatements/DropTableStatement.php @@ -39,7 +39,7 @@ public function compile(DatabaseDialect $dialect): string $statements[] = $dropReference->compile($dialect); } - $statements[] = match($dialect) { + $statements[] = match ($dialect) { DatabaseDialect::POSTGRESQL => sprintf('DROP TABLE IF EXISTS `%s` CASCADE', $this->tableName), default => sprintf('DROP TABLE IF EXISTS `%s`', $this->tableName), }; diff --git a/packages/database/src/QueryStatements/FieldStatement.php b/packages/database/src/QueryStatements/FieldStatement.php index 949103af2..70255aaa6 100644 --- a/packages/database/src/QueryStatements/FieldStatement.php +++ b/packages/database/src/QueryStatements/FieldStatement.php @@ -58,7 +58,7 @@ public function compile(DatabaseDialect $dialect): string return $field; } - return match($dialect) { + return match ($dialect) { DatabaseDialect::POSTGRESQL => sprintf('%s AS "%s"', $field, trim($alias, '`')), default => sprintf('%s AS `%s`', $field, trim($alias, '`')), }; diff --git a/packages/database/src/QueryStatements/ShowTablesStatement.php b/packages/database/src/QueryStatements/ShowTablesStatement.php index b7b28a83f..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_%'", - DatabaseDialect::POSTGRESQL => "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE' AND table_schema NOT IN ('pg_catalog', 'information_schema');", + 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/tests/Integration/Database/Builder/InsertQueryBuilderTest.php b/tests/Integration/Database/Builder/InsertQueryBuilderTest.php index 761915e55..f5b5440b0 100644 --- a/tests/Integration/Database/Builder/InsertQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/InsertQueryBuilderTest.php @@ -32,10 +32,9 @@ public function test_insert_on_plain_table(): void ->build(); $expected = $this->buildExpectedInsert(<<assertSameWithoutBackticks( $expected, @@ -61,10 +60,9 @@ public function test_insert_with_batch(): void ->build(); $expected = $this->buildExpectedInsert(<<assertSameWithoutBackticks( $expected, @@ -94,8 +92,7 @@ public function test_insert_on_model_table(): void $expected = $this->buildExpectedInsert(<<assertSameWithoutBackticks($expected, $query->toSql()); $this->assertSame(['brent', 'a', null, 'other name', 'b', null], $query->bindings); diff --git a/tests/Integration/Database/Builder/SelectQueryBuilderTest.php b/tests/Integration/Database/Builder/SelectQueryBuilderTest.php index a036418f3..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); diff --git a/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php b/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php index 8098d8f6b..ca8313ca4 100644 --- a/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php +++ b/tests/Integration/Database/Builder/UpdateQueryBuilderTest.php @@ -171,9 +171,9 @@ public function test_insert_new_relation_on_update(): void $authorQuery = $bookQuery->bindings[0]; $expected = <<container->get(DatabaseDialect::class) === DatabaseDialect::POSTGRESQL) { $expected .= ' RETURNING *'; diff --git a/tests/Integration/Database/GenericDatabaseTest.php b/tests/Integration/Database/GenericDatabaseTest.php index 2085339e1..912a0976e 100644 --- a/tests/Integration/Database/GenericDatabaseTest.php +++ b/tests/Integration/Database/GenericDatabaseTest.php @@ -11,6 +11,7 @@ use Tests\Tempest\Fixtures\Migrations\CreatePublishersTable; use Tests\Tempest\Fixtures\Modules\Books\Models\Author; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; + use function Tempest\Database\query; /** @@ -39,7 +40,7 @@ public function test_transaction_manager_fails(): void $db = $this->container->get(Database::class); - $db->withinTransaction(function (): void { + $db->withinTransaction(function (): never { query(Author::class)->insert( name: 'Brent', )->execute(); diff --git a/tests/Integration/Database/GenericTransactionManagerTest.php b/tests/Integration/Database/GenericTransactionManagerTest.php index 09d51bfe1..55f06a9a5 100644 --- a/tests/Integration/Database/GenericTransactionManagerTest.php +++ b/tests/Integration/Database/GenericTransactionManagerTest.php @@ -11,6 +11,7 @@ use Tests\Tempest\Fixtures\Migrations\CreatePublishersTable; use Tests\Tempest\Fixtures\Modules\Books\Models\Author; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; + use function Tempest\Database\query; /** diff --git a/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php b/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php index f80bd4ffd..09cb96656 100644 --- a/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php +++ b/tests/Integration/Database/QueryStatements/CreateTableStatementTest.php @@ -128,7 +128,7 @@ public function up(): QueryStatement { return new CompoundStatement( new DropEnumTypeStatement(EnumForCreateTable::class), - new CreateEnumTypeStatement(EnumForCreateTable::class) + new CreateEnumTypeStatement(EnumForCreateTable::class), ); } diff --git a/tests/Integration/FrameworkIntegrationTestCase.php b/tests/Integration/FrameworkIntegrationTestCase.php index a6cd24e8c..5fa05f53e 100644 --- a/tests/Integration/FrameworkIntegrationTestCase.php +++ b/tests/Integration/FrameworkIntegrationTestCase.php @@ -73,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 diff --git a/tests/Integration/ORM/Mappers/QueryMapperTest.php b/tests/Integration/ORM/Mappers/QueryMapperTest.php index 7cf91c15f..0d5c9bd88 100644 --- a/tests/Integration/ORM/Mappers/QueryMapperTest.php +++ b/tests/Integration/ORM/Mappers/QueryMapperTest.php @@ -29,13 +29,13 @@ public function test_insert_query(): void $expected = match ($dialect) { DatabaseDialect::POSTGRESQL => <<<'SQL' - INSERT INTO authors (name) - VALUES (?) RETURNING * - SQL, + INSERT INTO authors (name) + VALUES (?) RETURNING * + SQL, default => <<<'SQL' - INSERT INTO `authors` (`name`) - VALUES (?) - SQL, + INSERT INTO `authors` (`name`) + VALUES (?) + SQL, }; $this->assertSame($expected, $query->toSql()); @@ -52,15 +52,15 @@ public function test_update_query(): void $expected = match ($dialect) { DatabaseDialect::POSTGRESQL => <<<'SQL' - UPDATE authors - SET name = ? - WHERE id = ? - SQL, + UPDATE authors + SET name = ? + WHERE id = ? + SQL, default => <<<'SQL' - UPDATE `authors` - SET `name` = ? - WHERE `id` = ? - SQL, + UPDATE `authors` + SET `name` = ? + WHERE `id` = ? + SQL, }; $this->assertSame($expected, $query->toSql()); From c4d02a4daf935819a9ff0e475a39ca9b84433951 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 09:53:38 +0200 Subject: [PATCH 12/29] wip --- .../src/QueryStatements/FieldStatement.php | 14 ++++++++-- .../AlterTableStatementTest.php | 28 +++++++++++++------ .../QueryStatements/CountStatementTest.php | 12 ++------ .../CreateTableStatementTest.php | 2 +- .../QueryStatements/FieldStatementTest.php | 10 +++++++ .../QueryStatements/InsertStatementTest.php | 8 +++++- .../QueryStatements/SelectStatementTest.php | 19 ++----------- .../Framework/Testing/IntegrationTest.php | 3 +- 8 files changed, 56 insertions(+), 40 deletions(-) diff --git a/packages/database/src/QueryStatements/FieldStatement.php b/packages/database/src/QueryStatements/FieldStatement.php index 70255aaa6..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('.'); 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 7ae73a0ef..a35e314d4 100644 --- a/src/Tempest/Framework/Testing/IntegrationTest.php +++ b/src/Tempest/Framework/Testing/IntegrationTest.php @@ -36,7 +36,7 @@ abstract class IntegrationTest extends TestCase protected Kernel $kernel; - protected Container|GenericContainer $container; + protected GenericContainer $container; protected ConsoleTester $console; @@ -62,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); From b689bc42bd52e88d3a21e4193d5cd94576b965a9 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 09:55:10 +0200 Subject: [PATCH 13/29] wip --- packages/database/src/GenericDatabase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/database/src/GenericDatabase.php b/packages/database/src/GenericDatabase.php index 874b05bb8..822904931 100644 --- a/packages/database/src/GenericDatabase.php +++ b/packages/database/src/GenericDatabase.php @@ -32,7 +32,7 @@ public function execute(Query $query): void try { foreach (explode(';', $query->toSql()) as $sql) { - if (! $sql) { + if (! trim($sql)) { continue; } From 485d473220114b2d769d5cea7e6eed8159434403 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 09:59:06 +0200 Subject: [PATCH 14/29] wip --- .../database/src/Config/PostgresConfig.php | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/database/src/Config/PostgresConfig.php b/packages/database/src/Config/PostgresConfig.php index 13b447f4c..239b90de3 100644 --- a/packages/database/src/Config/PostgresConfig.php +++ b/packages/database/src/Config/PostgresConfig.php @@ -12,14 +12,7 @@ final class PostgresConfig implements DatabaseConfig { public string $dsn { - get => sprintf( - 'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s', - $this->host, - $this->port, - $this->database, - $this->username, - $this->password, - ); + get => $this->buildDsn(); } public DatabaseDialect $dialect { @@ -34,10 +27,28 @@ public function __construct( #[SensitiveParameter] public string $username = 'postgres', #[SensitiveParameter] - public string $password = '', + public ?string $password = null, #[SensitiveParameter] public string $database = 'app', public NamingStrategy $namingStrategy = new PluralizedSnakeCaseStrategy(), public null|string|UnitEnum $tag = null, ) {} + + private function buildDsn(): string + { + $dsn = sprintf( + 'pgsql:host=%s;port=%s;dbname=%s;user=%s', + $this->host, + $this->port, + $this->database, + $this->username, + $this->password, + ); + + if ($this->password !== null) { + $dsn .= ';password=%s' . $this->password; + } + + return $dsn; + } } From a6c21c0a84b6c1655f23ff073f95f15d6ce78217 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 10:38:22 +0200 Subject: [PATCH 15/29] Revert "wip" This reverts commit 485d473220114b2d769d5cea7e6eed8159434403. --- .../database/src/Config/PostgresConfig.php | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/packages/database/src/Config/PostgresConfig.php b/packages/database/src/Config/PostgresConfig.php index 239b90de3..13b447f4c 100644 --- a/packages/database/src/Config/PostgresConfig.php +++ b/packages/database/src/Config/PostgresConfig.php @@ -12,7 +12,14 @@ final class PostgresConfig implements DatabaseConfig { public string $dsn { - get => $this->buildDsn(); + get => sprintf( + 'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s', + $this->host, + $this->port, + $this->database, + $this->username, + $this->password, + ); } public DatabaseDialect $dialect { @@ -27,28 +34,10 @@ public function __construct( #[SensitiveParameter] public string $username = 'postgres', #[SensitiveParameter] - public ?string $password = null, + public string $password = '', #[SensitiveParameter] public string $database = 'app', public NamingStrategy $namingStrategy = new PluralizedSnakeCaseStrategy(), public null|string|UnitEnum $tag = null, ) {} - - private function buildDsn(): string - { - $dsn = sprintf( - 'pgsql:host=%s;port=%s;dbname=%s;user=%s', - $this->host, - $this->port, - $this->database, - $this->username, - $this->password, - ); - - if ($this->password !== null) { - $dsn .= ';password=%s' . $this->password; - } - - return $dsn; - } } From c511dd75d4899bba58553878200f21dec3a4aa3c Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 10:40:42 +0200 Subject: [PATCH 16/29] wip --- packages/database/src/Connection/PDOConnection.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/database/src/Connection/PDOConnection.php b/packages/database/src/Connection/PDOConnection.php index 99d7b93fe..641278efe 100644 --- a/packages/database/src/Connection/PDOConnection.php +++ b/packages/database/src/Connection/PDOConnection.php @@ -98,10 +98,6 @@ public function connect(): void return; } - $this->pdo = new PDO( - $this->config->dsn, - $this->config->username, - $this->config->password, - ); + $this->pdo = new PDO($this->config->dsn); } } From 20feeaf15d268a98257c14214406950d9014c60a Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 10:45:48 +0200 Subject: [PATCH 17/29] wip --- packages/database/src/Config/PostgresConfig.php | 6 ++---- packages/database/src/Connection/PDOConnection.php | 6 +++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/database/src/Config/PostgresConfig.php b/packages/database/src/Config/PostgresConfig.php index 13b447f4c..c35e47ea2 100644 --- a/packages/database/src/Config/PostgresConfig.php +++ b/packages/database/src/Config/PostgresConfig.php @@ -13,12 +13,10 @@ final class PostgresConfig implements DatabaseConfig { public string $dsn { get => sprintf( - 'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s', + 'pgsql:host=%s;port=%s;dbname=%s', $this->host, $this->port, $this->database, - $this->username, - $this->password, ); } @@ -34,7 +32,7 @@ public function __construct( #[SensitiveParameter] public string $username = 'postgres', #[SensitiveParameter] - public string $password = '', + public ?string $password = null, #[SensitiveParameter] public string $database = 'app', public NamingStrategy $namingStrategy = new PluralizedSnakeCaseStrategy(), diff --git a/packages/database/src/Connection/PDOConnection.php b/packages/database/src/Connection/PDOConnection.php index 641278efe..a4b9bf696 100644 --- a/packages/database/src/Connection/PDOConnection.php +++ b/packages/database/src/Connection/PDOConnection.php @@ -98,6 +98,10 @@ public function connect(): void return; } - $this->pdo = new PDO($this->config->dsn); + $this->pdo = new PDO( + dsn: $this->config->dsn, + username: $this->config->username, + password: $this->config->password, + ); } } From b83dbea28bcab312cbed4702a21d216ef9c75dd2 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 10:52:26 +0200 Subject: [PATCH 18/29] wip --- packages/database/src/Config/PostgresConfig.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/database/src/Config/PostgresConfig.php b/packages/database/src/Config/PostgresConfig.php index c35e47ea2..c65dc8598 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 $username = 'postgres', #[SensitiveParameter] - public ?string $password = null, + public string $password = '', #[SensitiveParameter] public string $database = 'app', public NamingStrategy $namingStrategy = new PluralizedSnakeCaseStrategy(), From 6328033afc8926e1077afbec872098a1c26214f1 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 10:58:08 +0200 Subject: [PATCH 19/29] wip --- packages/database/src/Config/PostgresConfig.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/database/src/Config/PostgresConfig.php b/packages/database/src/Config/PostgresConfig.php index c65dc8598..13b447f4c 100644 --- a/packages/database/src/Config/PostgresConfig.php +++ b/packages/database/src/Config/PostgresConfig.php @@ -13,10 +13,12 @@ final class PostgresConfig implements DatabaseConfig { public string $dsn { get => sprintf( - 'pgsql:host=%s;port=%s;dbname=%s', + 'pgsql:host=%s;port=%s;dbname=%s;user=%s;password=%s', $this->host, $this->port, $this->database, + $this->username, + $this->password, ); } From 16a504d73c5c84b50487ce1b3fa634aacfce5f5d Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 11:01:19 +0200 Subject: [PATCH 20/29] wip --- .github/workflows/integration-tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index b235bec1a..6bd09c9b0 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: postgres + POSTGRES_PASSWORD: '' + POSTGRES_DB: app jobs: vitest: From 2327e67099361204d6a9ef42fd513615b77d09b8 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 11:33:37 +0200 Subject: [PATCH 21/29] wip --- .github/workflows/integration-tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 6bd09c9b0..b235bec1a 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -5,10 +5,6 @@ on: workflow_dispatch: schedule: - cron: '0 0 * * *' -env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: '' - POSTGRES_DB: app jobs: vitest: From a39335e3a3b4c23b86c7b466b6928039974903b8 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 11:34:28 +0200 Subject: [PATCH 22/29] wip --- .github/workflows/integration-tests.yml | 4 ++++ tests/Fixtures/Config/database.postgres.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index b235bec1a..109a4553e 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: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: app jobs: vitest: diff --git a/tests/Fixtures/Config/database.postgres.php b/tests/Fixtures/Config/database.postgres.php index e4d944602..6c04e817a 100644 --- a/tests/Fixtures/Config/database.postgres.php +++ b/tests/Fixtures/Config/database.postgres.php @@ -4,4 +4,4 @@ use Tempest\Database\Config\PostgresConfig; -return new PostgresConfig(); +return new PostgresConfig(password: 'password'); From 36b2dcca58ddb92cadebbfa536ff15af5722a658 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 13:12:07 +0200 Subject: [PATCH 23/29] wip --- .github/workflows/integration-tests.yml | 2 +- tests/Fixtures/Config/database.postgres.php | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 109a4553e..af7c0ec6d 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -6,7 +6,7 @@ on: schedule: - cron: '0 0 * * *' env: - POSTGRES_USER: postgres + POSTGRES_USER: runner POSTGRES_PASSWORD: password POSTGRES_DB: app diff --git a/tests/Fixtures/Config/database.postgres.php b/tests/Fixtures/Config/database.postgres.php index 6c04e817a..bea40f2ca 100644 --- a/tests/Fixtures/Config/database.postgres.php +++ b/tests/Fixtures/Config/database.postgres.php @@ -3,5 +3,10 @@ declare(strict_types=1); use Tempest\Database\Config\PostgresConfig; +use function Tempest\env; -return new PostgresConfig(password: 'password'); + +return new PostgresConfig( + username: env('POSTGRES_USER', 'postgres'), + password: env('POSTGRES_PASSWORD', ''), +); From b1f53099c05bfc45e643486ba996fae1b9bf58bd Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 13:12:23 +0200 Subject: [PATCH 24/29] wip --- .github/workflows/integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index af7c0ec6d..dfb033409 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -7,7 +7,7 @@ on: - cron: '0 0 * * *' env: POSTGRES_USER: runner - POSTGRES_PASSWORD: password + POSTGRES_PASSWORD: '' POSTGRES_DB: app jobs: From 349ec4832909a7cd5607b03f539df3a2142352b5 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 13:15:51 +0200 Subject: [PATCH 25/29] wip --- tests/Fixtures/Config/database.postgres.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Fixtures/Config/database.postgres.php b/tests/Fixtures/Config/database.postgres.php index bea40f2ca..0bdf2908f 100644 --- a/tests/Fixtures/Config/database.postgres.php +++ b/tests/Fixtures/Config/database.postgres.php @@ -8,5 +8,5 @@ return new PostgresConfig( username: env('POSTGRES_USER', 'postgres'), - password: env('POSTGRES_PASSWORD', ''), + password: env('POSTGRES_PASSWORD', '') ?? '', ); From 7a49f4002159e6cf2a5ceea4d76f30e789bedc78 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 13:20:56 +0200 Subject: [PATCH 26/29] wip --- .github/workflows/integration-tests.yml | 2 +- tests/Fixtures/Config/database.postgres.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index dfb033409..8e98772df 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -8,7 +8,7 @@ on: env: POSTGRES_USER: runner POSTGRES_PASSWORD: '' - POSTGRES_DB: app + POSTGRES_DB: postgres jobs: vitest: diff --git a/tests/Fixtures/Config/database.postgres.php b/tests/Fixtures/Config/database.postgres.php index 0bdf2908f..5f8c5f683 100644 --- a/tests/Fixtures/Config/database.postgres.php +++ b/tests/Fixtures/Config/database.postgres.php @@ -9,4 +9,5 @@ return new PostgresConfig( username: env('POSTGRES_USER', 'postgres'), password: env('POSTGRES_PASSWORD', '') ?? '', + database: env('POSTGRES_DB', 'app'), ); From bb088e1a7d033677a2406028542383cbba59ed71 Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 13:24:00 +0200 Subject: [PATCH 27/29] wip --- .github/workflows/integration-tests.yml | 2 +- tests/Fixtures/Config/database.postgres.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8e98772df..77790816f 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -32,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 diff --git a/tests/Fixtures/Config/database.postgres.php b/tests/Fixtures/Config/database.postgres.php index 5f8c5f683..1d46bb4d1 100644 --- a/tests/Fixtures/Config/database.postgres.php +++ b/tests/Fixtures/Config/database.postgres.php @@ -5,7 +5,6 @@ use Tempest\Database\Config\PostgresConfig; use function Tempest\env; - return new PostgresConfig( username: env('POSTGRES_USER', 'postgres'), password: env('POSTGRES_PASSWORD', '') ?? '', From b64aba580a3c31f30a0549ff5b722cd79f607b3d Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 13:25:17 +0200 Subject: [PATCH 28/29] wip --- tests/Fixtures/Config/database.postgres.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Fixtures/Config/database.postgres.php b/tests/Fixtures/Config/database.postgres.php index 1d46bb4d1..5bc12675e 100644 --- a/tests/Fixtures/Config/database.postgres.php +++ b/tests/Fixtures/Config/database.postgres.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Tempest\Database\Config\PostgresConfig; + use function Tempest\env; return new PostgresConfig( From 7fa669bd734e3475f0edf8791337398e585bec4f Mon Sep 17 00:00:00 2001 From: brendt Date: Wed, 21 May 2025 13:38:08 +0200 Subject: [PATCH 29/29] wip --- .github/workflows/integration-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 77790816f..645adc9e6 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -64,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 }}"