Skip to content

Commit

Permalink
Introduce ODBC drivers
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Jun 27, 2023
1 parent d722992 commit a11eb3c
Show file tree
Hide file tree
Showing 22 changed files with 946 additions and 79 deletions.
9 changes: 6 additions & 3 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ on:
- src/**
- tests/**
push:
branches:
- "*.x"
# branches:
# - "*.x"
paths:
- .github/workflows/continuous-integration.yml
- ci/**
Expand Down Expand Up @@ -476,6 +476,9 @@ jobs:
- collation: "Latin1_General_100_CS_AS_SC_UTF8"
php-version: "7.4"
extension: "pdo_sqlsrv"
- collation: "Latin1_General_100_CI_AS_SC_UTF8"
php-version: "8.2"
extension: "odbc_sqlsrv"

services:
mssql:
Expand Down Expand Up @@ -504,7 +507,7 @@ jobs:
coverage: "pcov"
ini-values: "zend.assertions=1"
tools: "pecl"
extensions: "${{ matrix.extension }}-5.10.0beta1"
extensions: "sqlsrv pdo_sqlsrv"

- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v2"
Expand Down
36 changes: 36 additions & 0 deletions ci/github/phpunit/odbc_sqlsrv.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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="odbc_sqlsrv"/>
<var name="db_host" value="127.0.0.1" />
<var name="db_user" value="sa" />
<var name="db_password" value="Doctrine2018" />
<var name="db_dbname" value="doctrine" />

<var name="db_driver_option_Charset" value="UTF-8"/>
<var name="db_driver_option_Driver" value="ODBC Driver 17 for SQL Server"/>
<var name="db_driver_option_Encrypt" value="no"/>
</php>

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

<coverage>
<include>
<directory suffix=".php">../../../src</directory>
</include>
</coverage>
</phpunit>
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ parameters:
- '~^Parameter #1 \$row of method Doctrine\\DBAL\\Driver\\PgSQL\\Result\:\:mapNumericRow\(\) expects array<int, string\|null>, array<int\|string, string\|null> given\.$~'

# Ignore isset() checks in destructors.
- '~^Property Doctrine\\DBAL\\Driver\\ODBC\\Connection\:\:\$connection \(resource\) in isset\(\) is not nullable\.$~'
- '~^Property Doctrine\\DBAL\\Driver\\PgSQL\\Connection\:\:\$connection \(PgSql\\Connection\|resource\) in isset\(\) is not nullable\.$~'
- '~^Property Doctrine\\DBAL\\Driver\\PgSQL\\Statement\:\:\$connection \(PgSql\\Connection\|resource\) in isset\(\) is not nullable\.$~'

Expand Down
40 changes: 40 additions & 0 deletions src/Driver/API/ODBC/ExceptionConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\API\ODBC;

use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\ConstraintViolationException;
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Query;

final class ExceptionConverter implements ExceptionConverterInterface
{
public function convert(Exception $exception, ?Query $query): DriverException
{
switch ($exception->getSQLState()) {
case '23000':
return new ConstraintViolationException($exception, $query);
case '28000':
case 'S1T00':
return new ConnectionException($exception, $query);
case '37000':
return new SyntaxErrorException($exception, $query);
case 'S0001':
return new TableExistsException($exception, $query);
case 'S0002':
return new TableNotFoundException($exception, $query);
case 'S0022':
return new DatabaseObjectNotFoundException($exception, $query);
}

return new DriverException($exception, $query);
}
}
112 changes: 112 additions & 0 deletions src/Driver/ODBC/Connection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\ODBC;

use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver\ODBC\Exception\Error;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\SQL\Parser;
use LogicException;

use function odbc_autocommit;
use function odbc_close;
use function odbc_commit;
use function odbc_exec;
use function odbc_rollback;

class Connection implements ConnectionInterface
{
/** @var resource */
private $connection;

private Parser $parser;

/** @param resource $connection */
public function __construct($connection)
{
$this->connection = $connection;
$this->parser = new Parser(false);
}

public function __destruct()
{
if (! isset($this->connection)) {
return;
}

@odbc_rollback($this->connection);
@odbc_close($this->connection);
}

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

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

public function query(string $sql): Result
{
$result = @odbc_exec($this->connection, $sql);
if ($result === false) {
throw Error::new($this->connection);
}

return new Result($result);
}

/**
* {@inheritDoc}
*
* @psalm-return never
*/
public function quote($value, $type = ParameterType::STRING): string
{
throw new LogicException('The ODBC driver does not support quoting values.');
}

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

/**
* {@inheritDoc}
*
* @psalm-return never
*/
public function lastInsertId($name = null)
{
throw new LogicException('The ODBC driver does not support retrieving the last inserted ID.');
}

public function beginTransaction(): bool
{
return (bool) odbc_autocommit($this->connection, false);
}

public function commit(): bool
{
$result = odbc_commit($this->connection);
odbc_autocommit($this->connection, true);

return $result;
}

public function rollBack(): bool
{
$result = odbc_rollback($this->connection);
odbc_autocommit($this->connection, true);

return $result;
}

/** @return resource */
public function getNativeConnection()
{
return $this->connection;
}
}
49 changes: 49 additions & 0 deletions src/Driver/ODBC/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\ODBC;

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[] = '?';
}

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

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;
}
}
95 changes: 95 additions & 0 deletions src/Driver/ODBC/Driver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\ODBC;

use Doctrine\DBAL\Connection as ConnectionInterface;
use Doctrine\DBAL\Driver as DriverInterface;
use Doctrine\DBAL\Driver\API\ODBC\ExceptionConverter;
use Doctrine\DBAL\Driver\ODBC\Exception\ConnectionFailed;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\Deprecations\Deprecation;
use LogicException;
use RuntimeException;
use SensitiveParameter;

use function is_bool;
use function odbc_connect;
use function odbc_connection_string_quote;
use function odbc_connection_string_should_quote;

use const PHP_VERSION_ID;

final class Driver implements DriverInterface
{
/** {@inheritDoc} */
public function connect(
#[SensitiveParameter]
array $params
): Connection {
$connection = @odbc_connect(
$this->assembleDsn($params['driverOptions'] ?? []),
$params['user'] ?? '',
$params['password'] ?? '',
);

if ($connection === false) {
throw ConnectionFailed::new();
}

return new Connection($connection);
}

public function getDatabasePlatform(): AbstractPlatform
{
throw new LogicException('The ODBC driver does not support platform detection.');
}

public function getSchemaManager(ConnectionInterface $conn, AbstractPlatform $platform): AbstractSchemaManager
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/5458',
'%s() is deprecated. Use AbstractPlatform::createSchemaManager() instead.',
__METHOD__,
);

return $platform->createSchemaManager($conn);
}

public function getExceptionConverter(): ExceptionConverter
{
return new ExceptionConverter();
}

/** @param array<string, mixed> $driverOptions */
private function assembleDsn(array $driverOptions): string
{
$dsn = '';
foreach ($driverOptions as $key => $value) {
$dsn .= $key . '=' . $this->quoteForDsn($value) . ';';
}

return $dsn;
}

/** @param mixed $value */
private function quoteForDsn($value): string
{
if (is_bool($value)) {
$value = $value ? 'true' : 'false';
} else {
$value = (string) $value;
}

if (PHP_VERSION_ID >= 80200) {
return odbc_connection_string_should_quote($value)
? odbc_connection_string_quote($value)
: $value;
}

throw new RuntimeException('TODO');
}
}
19 changes: 19 additions & 0 deletions src/Driver/ODBC/Exception/ConnectionFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\ODBC\Exception;

use Doctrine\DBAL\Driver\AbstractException;

use function odbc_error;
use function odbc_errormsg;

/** @psalm-immutable */
final class ConnectionFailed extends AbstractException
{
public static function new(): self
{
return new self(odbc_errormsg(), odbc_error());
}
}
Loading

0 comments on commit a11eb3c

Please sign in to comment.