Skip to content

Commit

Permalink
Add the PgSQL driver
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Jan 29, 2023
1 parent a340f1f commit 06f853e
Show file tree
Hide file tree
Showing 20 changed files with 1,183 additions and 47 deletions.
17 changes: 14 additions & 3 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,13 +224,23 @@ jobs:
- "7.4"
postgres-version:
- "9.4"
- "14"
- "15"
extension:
- "pgsql"
- "pdo_pgsql"
include:
- php-version: "8.1"
postgres-version: "15"
extension: "pgsql"
- php-version: "8.2"
postgres-version: "15"
extension: "pgsql"
- php-version: "8.1"
postgres-version: "15"
extension: "pdo_pgsql"
- php-version: "8.2"
postgres-version: "15"
extension: "pdo_pgsql"

services:
postgres:
Expand All @@ -254,6 +264,7 @@ jobs:
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-version }}"
extensions: "pgsql pdo_pgsql"
coverage: "pcov"
ini-values: "zend.assertions=1"

Expand All @@ -263,12 +274,12 @@ jobs:
composer-options: "--ignore-platform-req=php+"

- name: "Run PHPUnit"
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"

- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
with:
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}.coverage"
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.extension }}-${{ matrix.php-version }}.coverage"
path: "coverage.xml"

phpunit-mariadb:
Expand Down
32 changes: 32 additions & 0 deletions ci/github/phpunit/pgsql.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
convertDeprecationsToExceptions="true"
>
<php>
<ini name="error_reporting" value="-1" />

<var name="db_driver" value="pgsql"/>
<var name="db_host" value="localhost" />
<var name="db_user" value="postgres" />
<var name="db_password" value="postgres" />
<var name="db_dbname" value="doctrine_tests" />
</php>

<testsuites>
<testsuite name="Doctrine DBAL Test Suite">
<directory>../../../tests</directory>
</testsuite>
</testsuites>

<coverage>
<include>
<directory suffix=".php">../../../src</directory>
</include>
</coverage>
</phpunit>
5 changes: 3 additions & 2 deletions docs/en/reference/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ interfaces to use. It can be configured in one of three ways:
- ``sqlite3``: An SQLite driver that uses the sqlite3 extension.
- ``pdo_pgsql``: A PostgreSQL driver that uses the pdo_pgsql PDO
extension.
- ``pgsql``: A PostgreSQL driver that uses the pgsql extension.
- ``pdo_oci``: An Oracle driver that uses the pdo_oci PDO
extension.
**Note that this driver caused problems in our tests. Prefer the oci8 driver if possible.**
Expand Down Expand Up @@ -235,8 +236,8 @@ mysqli
- ``ssl_cipher`` (string): A list of allowable ciphers to use for SSL encryption.
- ``driverOptions`` Any supported flags for mysqli found on `http://www.php.net/manual/en/mysqli.real-connect.php`

pdo_pgsql
^^^^^^^^^
pdo_pgsql / pgsql
^^^^^^^^^^^^^^^^^

- ``user`` (string): Username to use when connecting to the
database.
Expand Down
13 changes: 13 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,18 @@ parameters:
paths:
- src/Schema/PostgreSQLSchemaManager.php

# PgSql objects are represented as resources in PHP 7.4
- '~ expects PgSql\\Connection(:?\|(?:string|null))?, PgSql\\Connection\|resource given\.$~'
- '~ expects PgSql\\Result, PgSql\\Result\|resource given\.$~'

# PHPStan does not understand the array shapes returned by pg_fetch_*() methods.
- '~^Parameter #1 \$row of method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:mapAssociativeRow\(\) expects array<string, string\|null>, array<int\|string, string\|null> given\.$~'
- '~^Parameter #1 \$row of method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:mapNumericRow\(\) expects array<int, string\|null>, array<int\|string, string\|null> given\.$~'

# On PHP 7.4, pg_fetch_all() might return false for empty result sets.
-
message: '~^Strict comparison using === between array<int, array> and false will always evaluate to false\.$~'
paths:
- src/Driver/PgSQL/Result.php
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
32 changes: 31 additions & 1 deletion psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<file name="vendor/jetbrains/phpstorm-stubs/ibm_db2/ibm_db2.php" />
<file name="vendor/jetbrains/phpstorm-stubs/mysqli/mysqli.php" />
<file name="vendor/jetbrains/phpstorm-stubs/oci8/oci8.php" />
<file name="vendor/jetbrains/phpstorm-stubs/pgsql/pgsql.php" />
<file name="vendor/jetbrains/phpstorm-stubs/sqlsrv/sqlsrv.php" />
</stubs>
<issueHandlers>
Expand Down Expand Up @@ -567,6 +566,7 @@
<file name="src/Cache/QueryCacheProfile.php"/>
<file name="src/Connection.php"/>
<file name="src/Driver/IBMDB2/Statement.php"/>
<directory name="src/Driver/PgSQL"/>
<file name="src/DriverManager.php"/>
<file name="src/Platforms/AbstractMySQLPlatform.php"/>
<file name="src/Platforms/AbstractPlatform.php"/>
Expand Down Expand Up @@ -641,6 +641,27 @@
<file name="src/Schema/PostgreSQLSchemaManager.php"/>
</errorLevel>
</NullableReturnStatement>
<PossiblyInvalidArgument>
<errorLevel type="suppress">
<!-- PgSql objects are represented as resources in PHP 7.4 -->
<referencedFunction name="pg_affected_rows"/>
<referencedFunction name="pg_escape_bytea"/>
<referencedFunction name="pg_escape_string"/>
<referencedFunction name="pg_fetch_all"/>
<referencedFunction name="pg_fetch_all_columns"/>
<referencedFunction name="pg_fetch_assoc"/>
<referencedFunction name="pg_fetch_row"/>
<referencedFunction name="pg_field_name"/>
<referencedFunction name="pg_field_type"/>
<referencedFunction name="pg_free_result"/>
<referencedFunction name="pg_get_result"/>
<referencedFunction name="pg_num_fields"/>
<referencedFunction name="pg_result_error_field"/>
<referencedFunction name="pg_send_execute"/>
<referencedFunction name="pg_send_prepare"/>
<referencedFunction name="pg_version"/>
</errorLevel>
</PossiblyInvalidArgument>
<PossiblyInvalidArrayOffset>
<errorLevel type="suppress">
<!-- $array[key($array)] is safe. -->
Expand Down Expand Up @@ -716,6 +737,9 @@
-->
<file name="src/Platforms/AbstractMySQLPlatform.php"/>
<file name="tests/Functional/Driver/AbstractDriverTest.php"/>

<!-- We're checking for invalid input. -->
<directory name="src/Driver/PgSQL"/>
</errorLevel>
</RedundantConditionGivenDocblockType>
<RedundantPropertyInitializationCheck>
Expand Down Expand Up @@ -747,6 +771,12 @@
<file name="tests/Platforms/AbstractMySQLPlatformTestCase.php"/>
</errorLevel>
</TooManyArguments>
<TypeDoesNotContainType>
<errorLevel type="suppress">
<!-- On PHP 7.4, pg_fetch_all() might return false for empty result sets. -->
<file name="src/Driver/PgSQL/Result.php"/>
</errorLevel>
</TypeDoesNotContainType>
<UndefinedDocblockClass>
<errorLevel type="suppress">
<!-- See https://github.com/vimeo/psalm/issues/5472 -->
Expand Down
130 changes: 130 additions & 0 deletions src/Driver/PgSQL/Connection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

namespace Doctrine\DBAL\Driver\PgSQL;

use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\SQL\Parser;
use Doctrine\Deprecations\Deprecation;
use PgSql\Connection as PgSqlConnection;
use TypeError;

use function assert;
use function get_class;
use function gettype;
use function is_object;
use function is_resource;
use function pg_escape_string;
use function pg_get_result;
use function pg_result_error;
use function pg_send_prepare;
use function pg_version;
use function sprintf;
use function uniqid;

final class Connection implements ServerInfoAwareConnection
{
/** @var PgSqlConnection|resource */
private $connection;

private Parser $parser;

/** @param PgSqlConnection|resource $connection */
public function __construct($connection)
{
if (! is_resource($connection) && ! $connection instanceof PgSqlConnection) {
throw new TypeError(sprintf(
'Expected connection to be a resource or an instance of %s, got %s.',
PgSqlConnection::class,
is_object($connection) ? get_class($connection) : gettype($connection),
));
}

$this->connection = $connection;
$this->parser = new Parser(false);
}

public function prepare(string $sql): Statement
{
$visitor = new ConvertParameters();
$this->parser->parse($sql, $visitor);

$statementName = uniqid('dbal', true);
$success = (bool) pg_send_prepare($this->connection, $statementName, $visitor->getSQL());

assert($success);

$result = @pg_get_result($this->connection);
assert($result !== false);

if ((bool) pg_result_error($result)) {
throw Exception::fromResult($result);
}

return new Statement($this->connection, $statementName, $visitor->getParameterMap());
}

public function query(string $sql): Result
{
return $this->prepare($sql)->execute();
}

/** @inheritdoc */
public function quote($value, $type = ParameterType::STRING)
{
return sprintf("'%s'", pg_escape_string($this->connection, $value));
}

public function exec(string $sql): int
{
return $this->prepare($sql)->execute()->rowCount();
}

/** @inheritdoc */
public function lastInsertId($name = null)
{
if ($name !== null) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/4687',
'The usage of Connection::lastInsertId() with a sequence name is deprecated.',
);

return $this->query(sprintf('SELECT CURRVAL(%s)', $this->quote($name)))->fetchOne();
}

return $this->query('SELECT LASTVAL()')->fetchOne();
}

public function beginTransaction(): bool
{
$this->exec('BEGIN');

return true;
}

public function commit(): bool
{
$this->exec('COMMIT');

return true;
}

public function rollBack(): bool
{
$this->exec('ROLLBACK');

return true;
}

public function getServerVersion(): string
{
return (string) pg_version($this->connection)['server'];
}

/** @return PgSqlConnection|resource */
public function getNativeConnection()
{
return $this->connection;
}
}
49 changes: 49 additions & 0 deletions src/Driver/PgSQL/ConvertParameters.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\PgSQL;

use Doctrine\DBAL\SQL\Parser\Visitor;

use function count;
use function implode;

final class ConvertParameters implements Visitor
{
/** @var list<string> */
private array $buffer = [];

/** @var array<array-key, int> */
private array $parameterMap = [];

public function acceptPositionalParameter(string $sql): void
{
$position = count($this->parameterMap) + 1;
$this->parameterMap[$position] = $position;
$this->buffer[] = '$' . $position;
}

public function acceptNamedParameter(string $sql): void
{
$position = count($this->parameterMap) + 1;
$this->parameterMap[$sql] = $position;
$this->buffer[] = '$' . $position;
}

public function acceptOther(string $sql): void
{
$this->buffer[] = $sql;
}

public function getSQL(): string
{
return implode('', $this->buffer);
}

/** @return array<array-key, int> */
public function getParameterMap(): array
{
return $this->parameterMap;
}
}
Loading

0 comments on commit 06f853e

Please sign in to comment.