Skip to content

Commit

Permalink
Introduce ODBC drivers
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Aug 24, 2023
1 parent d279b8f commit a8811ac
Show file tree
Hide file tree
Showing 19 changed files with 921 additions and 78 deletions.
7 changes: 4 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 @@ -470,6 +470,7 @@ jobs:
extension:
- "sqlsrv"
- "pdo_sqlsrv"
- "odbc_sqlsrv"
collation:
- "Latin1_General_100_CI_AS_SC_UTF8"
include:
Expand Down Expand Up @@ -507,7 +508,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>
8 changes: 8 additions & 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 All @@ -140,5 +141,12 @@ parameters:
# Ignore the possible false return value of db2_num_rows().
- '~^Method Doctrine\\DBAL\\Driver\\IBMDB2\\Connection\:\:exec\(\) should return int but returns int<0, max>\|false\.$~'
- '~^Method Doctrine\\DBAL\\Driver\\IBMDB2\\Result\:\:rowCount\(\) should return int but returns int<0, max>\|false\.$~'

# On PHP 7.4, odbc_autocommit() requires the second parameter to be an integer.
-
message: '~^Parameter #2 \$enable of function odbc_autocommit expects bool, int given\.$~'
paths:
- src/Driver/ODBC/Connection.php

includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
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);
}
}
126 changes: 126 additions & 0 deletions src/Driver/ODBC/Connection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?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;

use const PHP_VERSION_ID;

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
{
if (PHP_VERSION_ID < 80000) {
return (bool) odbc_autocommit($this->connection, 0);
}

return (bool) odbc_autocommit($this->connection, false);
}

public function commit(): bool
{
$result = odbc_commit($this->connection);
if (PHP_VERSION_ID < 80000) {
odbc_autocommit($this->connection, 1);
} else {
odbc_autocommit($this->connection, true);
}

return $result;
}

public function rollBack(): bool
{
$result = odbc_rollback($this->connection);
if (PHP_VERSION_ID < 80000) {
odbc_autocommit($this->connection, 1);
} else {
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;
}
}
Loading

0 comments on commit a8811ac

Please sign in to comment.