Fixed bug that database-pgsql does not support migration. (#5417)

This commit is contained in:
她和她的猫 2023-02-16 13:58:21 +08:00 committed by GitHub
parent f980943bf1
commit aeaf500517
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 651 additions and 33 deletions

View File

@ -14,6 +14,7 @@
## Fixed ## Fixed
- [#5405](https://github.com/hyperf/hyperf/pull/5405) Fixed get local ip error when IPv6 exists. - [#5405](https://github.com/hyperf/hyperf/pull/5405) Fixed get local ip error when IPv6 exists.
- [#5417](https://github.com/hyperf/hyperf/pull/5417) Fixed bug that database-pgsql does not support migration.
## Optimized ## Optimized

View File

@ -30,6 +30,7 @@
"psr/simple-cache": "^1.0|^2.0|^3.0" "psr/simple-cache": "^1.0|^2.0|^3.0"
}, },
"require-dev": { "require-dev": {
"doctrine/dbal": "^3.6",
"doctrine/inflector": "^2.0", "doctrine/inflector": "^2.0",
"doctrine/instantiator": "^1.0", "doctrine/instantiator": "^1.0",
"egulias/email-validator": "^3.0", "egulias/email-validator": "^3.0",

View File

@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Database\PgSQL\DBAL;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\ParameterType;
use Swoole\Coroutine\PostgreSQL;
use Swoole\Coroutine\PostgreSQLStatement;
class Connection implements \Doctrine\DBAL\Driver\Connection
{
/**
* Create a new PDO connection instance.
*/
public function __construct(private PostgreSQL $connection)
{
}
/**
* Execute an SQL statement.
*/
public function exec(string $sql): int
{
$stmt = $this->connection->query($sql);
\assert($stmt instanceof PostgreSQLStatement);
return $stmt->affectedRows();
}
/**
* Prepare a new SQL statement.
*/
public function prepare(string $sql): StatementInterface
{
$stmt = $this->connection->prepare($sql);
\assert($stmt instanceof PostgreSQLStatement);
return new Statement($stmt);
}
/**
* Execute a new query against the connection.
*/
public function query(string $sql): ResultInterface
{
$stmt = $this->connection->query($sql);
\assert($stmt instanceof PostgreSQLStatement);
return new Result($stmt);
}
/**
* Get the last insert ID.
*
* @param null|string $name
* @return string
*/
public function lastInsertId($name = null)
{
if ($name !== null) {
return $this->query(sprintf('SELECT CURRVAL(%s)', $this->quote($name)))->fetchOne();
}
return $this->query('SELECT LASTVAL()')->fetchOne();
}
/**
* Begin a new database transaction.
*/
public function beginTransaction(): bool
{
$this->exec('BEGIN');
return true;
}
/**
* Commit a database transaction.
*/
public function commit(): bool
{
$this->exec('COMMIT');
return true;
}
/**
* Roll back a database transaction.
*/
public function rollBack(): bool
{
$this->exec('ROLLBACK');
return true;
}
/**
* Wrap quotes around the given input.
*
* @param string $input
* @param string $type
* @return string
*/
public function quote($input, $type = ParameterType::STRING)
{
return $this->connection->escapeLiteral($input);
}
/**
* Get the server version for the connection.
*/
public function getServerVersion(): string
{
$result = $this->query('SHOW server_version');
$serverVersion = $result->fetchOne();
if ($version = strstr($serverVersion, ' ', true)) {
return $version;
}
return $serverVersion;
}
public function getNativeConnection(): PostgreSQL
{
return $this->connection;
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Database\PgSQL\DBAL;
use Doctrine\DBAL\Driver\Exception as DriverExceptionInterface;
use Exception as BaseException;
use Throwable;
class Exception extends BaseException implements DriverExceptionInterface
{
/**
* The SQLSTATE of the driver.
*/
private ?string $sqlState;
/**
* @param string $message the driver error message
* @param null|string $sqlState the SQLSTATE the driver is in at the time the error occurred, if any
* @param int $code the driver specific error code if any
* @param null|Throwable $previous the previous throwable used for the exception chaining
*/
public function __construct($message, $sqlState = null, $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->sqlState = $sqlState;
}
/**
* {@inheritdoc}
*/
public function getSQLState()
{
return $this->sqlState;
}
}

View File

@ -12,9 +12,20 @@ declare(strict_types=1);
namespace Hyperf\Database\PgSQL\DBAL; namespace Hyperf\Database\PgSQL\DBAL;
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver; use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
use Hyperf\Database\DBAL\Concerns\ConnectsToDatabase; use InvalidArgumentException;
use Swoole\Coroutine\PostgreSQL;
class PostgresDriver extends AbstractPostgreSQLDriver class PostgresDriver extends AbstractPostgreSQLDriver
{ {
use ConnectsToDatabase; /**
* Create a new database connection.
*/
public function connect(array $params)
{
if (! isset($params['pdo']) || ! $params['pdo'] instanceof PostgreSQL) {
throw new InvalidArgumentException('The "pdo" property must be required.');
}
return new Connection($params['pdo']);
}
} }

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Database\PgSQL\DBAL;
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Swoole\Coroutine\PostgreSQLStatement;
final class Result implements ResultInterface
{
public function __construct(private PostgreSQLStatement $result)
{
}
/** {@inheritdoc} */
public function fetchNumeric()
{
return $this->result->fetchArray(result_type: SW_PGSQL_NUM);
}
/** {@inheritdoc} */
public function fetchAssociative()
{
return $this->result->fetchAssoc();
}
/** {@inheritdoc} */
public function fetchOne()
{
$row = $this->fetchNumeric();
if ($row === false) {
return false;
}
return $row[0];
}
/** {@inheritdoc} */
public function fetchAllNumeric(): array
{
return $this->result->fetchAll(SW_PGSQL_NUM);
}
/** {@inheritdoc} */
public function fetchAllAssociative(): array
{
return $this->result->fetchAll(SW_PGSQL_ASSOC);
}
/** {@inheritdoc} */
public function fetchFirstColumn(): array
{
$resultSet = $this->result->fetchAll(SW_PGSQL_NUM);
if ($resultSet === false) {
return [];
}
return array_map(fn ($row) => $row[0], $resultSet);
}
public function rowCount(): int
{
return (int) $this->result->affectedRows();
}
public function columnCount(): int
{
return (int) $this->result->fieldCount();
}
public function free(): void
{
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Database\PgSQL\DBAL;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\ParameterType;
use Swoole\Coroutine\PostgreSQLStatement;
use function ksort;
final class Statement implements StatementInterface
{
private array $parameters = [];
public function __construct(private PostgreSQLStatement $stmt)
{
}
/** {@inheritdoc} */
public function bindValue($param, $value, $type = ParameterType::STRING): bool
{
$this->parameters[$param] = $value;
return true;
}
/** {@inheritdoc} */
public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool
{
$this->parameters[$param] = &$variable;
return true;
}
/** {@inheritdoc} */
public function execute($params = null): Result
{
if (! empty($params)) {
foreach ($params as $param => $value) {
if (is_int($param)) {
$this->bindValue($param + 1, $value, ParameterType::STRING);
} else {
$this->bindValue($param, $value, ParameterType::STRING);
}
}
}
ksort($this->parameters);
if (! $this->stmt->execute($this->parameters)) {
throw new Exception($this->stmt->error ?? 'Execute failed.');
}
return new Result($this->stmt);
}
}

View File

@ -13,14 +13,22 @@ namespace HyperfTest\Database\PgSQL\Cases;
use Exception; use Exception;
use Hyperf\Database\Connection; use Hyperf\Database\Connection;
use Hyperf\Database\ConnectionResolver;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Database\Connectors\ConnectionFactory; use Hyperf\Database\Connectors\ConnectionFactory;
use Hyperf\Database\Exception\QueryException; use Hyperf\Database\Exception\QueryException;
use Hyperf\Database\Migrations\DatabaseMigrationRepository;
use Hyperf\Database\Migrations\Migrator;
use Hyperf\Database\PgSQL\Connectors\PostgresSqlSwooleExtConnector; use Hyperf\Database\PgSQL\Connectors\PostgresSqlSwooleExtConnector;
use Hyperf\Database\PgSQL\PostgreSqlSwooleExtConnection; use Hyperf\Database\PgSQL\PostgreSqlSwooleExtConnection;
use Hyperf\Database\Query\Builder; use Hyperf\Database\Query\Builder;
use Hyperf\Database\Schema\Schema;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Filesystem\Filesystem;
use Mockery; use Mockery;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Style\OutputStyle;
/** /**
* @internal * @internal
@ -28,28 +36,24 @@ use Psr\Container\ContainerInterface;
*/ */
class PostgreSqlSwooleExtConnectionTest extends TestCase class PostgreSqlSwooleExtConnectionTest extends TestCase
{ {
protected ConnectionFactory $connectionFactory; protected Migrator $migrator;
public function setUp(): void public function setUp(): void
{
$container = Mockery::mock(ContainerInterface::class);
$container->shouldReceive('has')->andReturn(true);
$container->shouldReceive('get')->with('db.connector.pgsql-swoole')->andReturn(new PostgresSqlSwooleExtConnector());
$this->connectionFactory = new ConnectionFactory($container);
Connection::resolverFor('pgsql-swoole', static function ($connection, $database, $prefix, $config) {
return new PostgreSqlSwooleExtConnection($connection, $database, $prefix, $config);
});
}
public function testSelectMethodDuplicateKeyValueException()
{ {
if (SWOOLE_MAJOR_VERSION < 5) { if (SWOOLE_MAJOR_VERSION < 5) {
$this->markTestSkipped('PostgreSql requires Swoole version >= 5.0.0'); $this->markTestSkipped('PostgreSql requires Swoole version >= 5.0.0');
} }
$connection = $this->connectionFactory->make([ $container = Mockery::mock(ContainerInterface::class);
$container->shouldReceive('has')->andReturn(true);
$container->shouldReceive('get')->with('db.connector.pgsql-swoole')->andReturn(new PostgresSqlSwooleExtConnector());
$connector = new ConnectionFactory($container);
Connection::resolverFor('pgsql-swoole', static function ($connection, $database, $prefix, $config) {
return new PostgreSqlSwooleExtConnection($connection, $database, $prefix, $config);
});
$connection = $connector->make([
'driver' => 'pgsql-swoole', 'driver' => 'pgsql-swoole',
'host' => '127.0.0.1', 'host' => '127.0.0.1',
'port' => 5432, 'port' => 5432,
@ -58,6 +62,32 @@ class PostgreSqlSwooleExtConnectionTest extends TestCase
'password' => 'postgres', 'password' => 'postgres',
]); ]);
$resolver = new ConnectionResolver(['default' => $connection]);
$container->shouldReceive('get')->with(ConnectionResolverInterface::class)->andReturn($resolver);
ApplicationContext::setContainer($container);
$this->migrator = new Migrator(
$repository = new DatabaseMigrationRepository($resolver, 'migrations'),
$resolver,
new Filesystem()
);
$output = Mockery::mock(OutputStyle::class);
$output->shouldReceive('writeln');
$this->migrator->setOutput($output);
if (! $repository->repositoryExists()) {
$repository->createRepository();
}
}
public function testSelectMethodDuplicateKeyValueException()
{
$connection = ApplicationContext::getContainer()->get(ConnectionResolverInterface::class)->connection();
$builder = new Builder($connection); $builder = new Builder($connection);
$this->expectException(QueryException::class); $this->expectException(QueryException::class);
@ -73,18 +103,7 @@ class PostgreSqlSwooleExtConnectionTest extends TestCase
public function testAffectingStatementWithWrongSql() public function testAffectingStatementWithWrongSql()
{ {
if (SWOOLE_MAJOR_VERSION < 5) { $connection = ApplicationContext::getContainer()->get(ConnectionResolverInterface::class)->connection();
$this->markTestSkipped('PostgreSql requires Swoole version >= 5.0.0');
}
$connection = $this->connectionFactory->make([
'driver' => 'pgsql-swoole',
'host' => '127.0.0.1',
'port' => 5432,
'database' => 'postgres',
'username' => 'postgres',
'password' => 'postgres',
]);
$this->expectException(QueryException::class); $this->expectException(QueryException::class);
@ -93,11 +112,9 @@ class PostgreSqlSwooleExtConnectionTest extends TestCase
public function testCreateConnectionTimedOut() public function testCreateConnectionTimedOut()
{ {
if (SWOOLE_MAJOR_VERSION < 5) { $factory = new ConnectionFactory(ApplicationContext::getContainer());
$this->markTestSkipped('PostgreSql requires Swoole version >= 5.0.0');
}
$connection = $this->connectionFactory->make([ $connection = $factory->make([
'driver' => 'pgsql-swoole', 'driver' => 'pgsql-swoole',
'host' => 'non-existent-host.internal', 'host' => 'non-existent-host.internal',
'port' => 5432, 'port' => 5432,
@ -111,4 +128,32 @@ class PostgreSqlSwooleExtConnectionTest extends TestCase
$connection->affectingStatement('UPDATE xx SET x = 1 WHERE id = 1'); $connection->affectingStatement('UPDATE xx SET x = 1 WHERE id = 1');
} }
public function testCreateTableForMigration()
{
$queryCommentSQL = "select a.attname,
d.description
from pg_class c,
pg_attribute a,
pg_type t,
pg_description d
where c.relname = 'password_resets_for_pgsql'
and a.attnum > 0
and a.attrelid = c.oid
and a.atttypid = t.oid
and d.objoid = a.attrelid
and d.objsubid = a.attnum";
$schema = new Schema();
$this->migrator->rollback([__DIR__ . '/../migrations/two']);
$this->migrator->rollback([__DIR__ . '/../migrations/one']);
$this->migrator->run([__DIR__ . '/../migrations/one']);
$this->assertTrue($schema->hasTable('password_resets_for_pgsql'));
$this->assertSame('', $schema->connection()->selectOne($queryCommentSQL)['description'] ?? '');
$this->migrator->run([__DIR__ . '/../migrations/two']);
$this->assertSame('邮箱', $schema->connection()->selectOne($queryCommentSQL)['description'] ?? '');
}
} }

View File

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Database\PgSQL\DBAL;
use Hyperf\Database\PgSQL\DBAL\Connection;
use Hyperf\Database\PgSQL\DBAL\Result;
use Hyperf\Database\PgSQL\DBAL\Statement;
use PHPUnit\Framework\TestCase;
use Swoole\Coroutine\PostgreSQL;
/**
* @internal
* @coversNothing
*/
class ConnectionTest extends TestCase
{
protected Connection $connection;
public function setUp(): void
{
if (SWOOLE_MAJOR_VERSION < 5) {
$this->markTestSkipped('PostgreSql requires Swoole version >= 5.0.0');
}
$pgsql = new PostgreSQL();
$connected = $pgsql->connect('host=127.0.0.1 port=5432 dbname=postgres user=postgres password=postgres');
if (! $connected) {
$this->fail('Failed to connect to PostgreSQL server.');
}
$this->connection = new Connection($pgsql);
$pgsql->query('CREATE TABLE IF NOT EXISTS connection_tests_for_hyperf( id SERIAL constraint id primary key, val1 varchar(255) not null, val2 varchar(255) not null);');
}
public function tearDown(): void
{
$this->connection->exec('DROP TABLE IF EXISTS connection_tests_for_hyperf');
}
public function testExec()
{
$sql = sprintf('insert into "connection_tests_for_hyperf" ("val1", "val2") values (\'%s\', \'%s\');', uniqid(), uniqid());
$this->assertGreaterThan(0, $this->connection->exec($sql));
}
public function testPrepare()
{
$stmt = $this->connection->prepare('select * from "connection_tests_for_hyperf" where "val1" = $1');
$this->assertInstanceOf(Statement::class, $stmt);
}
public function testQuery()
{
$this->insertOneRow();
$result = $this->connection->query('select * from "connection_tests_for_hyperf"');
$this->assertInstanceOf(Result::class, $result);
$this->assertGreaterThan(0, $result->rowCount());
}
public function testLastInsertId()
{
$this->insertOneRow();
$one = $this->connection->lastInsertId();
$this->assertIsNumeric($one);
$this->assertGreaterThan(0, $one);
$this->insertOneRow();
$two = $this->connection->lastInsertId();
$this->assertIsNumeric($two);
$this->assertGreaterThan($one, $two);
$this->assertEquals($two, $this->connection->lastInsertId('connection_tests_for_hyperf_id_seq'));
}
public function testTransactions()
{
$count = $this->getRowCount();
$this->connection->beginTransaction();
$this->insertOneRow();
$this->connection->rollBack();
$this->assertEquals($count, $this->getRowCount());
$this->connection->beginTransaction();
$this->insertOneRow();
$this->connection->commit();
$this->assertEquals($count + 1, $this->getRowCount());
}
public function testQuote()
{
$this->assertSame("'hyperf'", $this->connection->quote('hyperf'));
}
public function testGetServerVersion()
{
$this->assertMatchesRegularExpression('/^(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?)?/', $this->connection->getServerVersion());
}
public function testGetNativeConnection()
{
$this->assertInstanceOf(PostgreSQL::class, $this->connection->getNativeConnection());
}
public function testResult()
{
$insertResult = $this->connection
->prepare('insert into "connection_tests_for_hyperf" ("val1", "val2") values ($1, $2)')
->execute([$val1 = uniqid(), $val2 = uniqid()]);
$this->assertEquals(1, $insertResult->rowCount());
$result = $this->connection
->prepare('select val1, val2, id from "connection_tests_for_hyperf" where "val1" = $1')
->execute([$val1]);
$row = $result->fetchNumeric();
$id = $row[2];
$this->assertSame([$val1, $val2, $id], $row);
$this->assertSame(['val1' => $val1, 'val2' => $val2, 'id' => $id], $result->fetchAssociative());
$this->assertSame($val1, $result->fetchOne());
$this->assertSame([[$val1, $val2, $id]], $result->fetchAllNumeric());
$this->assertSame([['val1' => $val1, 'val2' => $val2, 'id' => $id]], $result->fetchAllAssociative());
$this->assertSame([$val1], $result->fetchFirstColumn());
$this->assertEquals(1, $result->rowCount());
$this->assertEquals(3, $result->columnCount());
}
private function insertOneRow()
{
$stmt = $this->connection->prepare('insert into "connection_tests_for_hyperf" ("val1", "val2") values ($1, $2)');
$stmt->execute([uniqid(), uniqid()]);
}
private function getRowCount()
{
$result = $this->connection->query('select count(*) from "connection_tests_for_hyperf"');
return $result->fetchOne();
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
use Hyperf\Database\Migrations\Migration;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Schema\Schema;
class CreatePasswordResetsForPgsqlTable extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::create('password_resets_for_pgsql', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token')->index();
$table->timestamp('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::dropIfExists('password_resets_for_pgsql');
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
use Hyperf\Database\Migrations\Migration;
use Hyperf\Database\Schema\Blueprint;
use Hyperf\Database\Schema\Schema;
class AlterPasswordResetsForPgsqlEmailColumn extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('password_resets_for_pgsql', function (Blueprint $table) {
$table->string('email')->comment('邮箱')->change();
});
}
/**
* Reverse the migrations.
*/
public function down()
{
if (Schema::hasTable('password_resets_for_pgsql')) {
Schema::table('password_resets_for_pgsql', function (Blueprint $table) {
$table->string('email')->index()->change();
});
}
}
}