Skip to content

Commit

Permalink
driver: implement pdo_sqlsrv driver support
Browse files Browse the repository at this point in the history
  • Loading branch information
hrach committed Nov 8, 2020
1 parent a16b14a commit 17f07b7
Show file tree
Hide file tree
Showing 12 changed files with 333 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:
- v*.*

env:
php-extensions: mbstring, intl, mysqli, pgsql, sqlsrv-5.9.0preview1
php-extensions: mbstring, intl, mysqli, pgsql, sqlsrv-5.9.0preview1, pdo_sqlsrv-5.9.0preview1
php-extensions-key: v1
php-tools: "composer:v2, pecl"

Expand Down
4 changes: 2 additions & 2 deletions doc/default.texy
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ Supported platforms:

- **MySQL** via `mysqli` or `pdo_mysql` extension,
- **Postgres** via `pgsql` or `pdo_pgsql` extension,
- **MS SQL Server** via `sqlsrv` extension.
- **MS SQL Server** via `sqlsrv` or `pdo_sqlsrv` extension.

Connection
==========

The connection instance is a main object that provides an API for accessing your database. Connection's constructor accepts a configuration array. The possible keys depend on the specific driver; some configuration keys are the shared for all drivers:

|* driver | driver name, use `mysqli`, `pgsql`, `sqlsrv`, `pdo_mysql`, `pdo_pgsql`
|* driver | driver name, use `mysqli`, `pgsql`, `sqlsrv`, `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv`
|* host | database server name
|* username | username for authentication
|* password | password for authentication
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A powerful abstraction layer for a database. **Fast & Safe**.
Supported platforms:
- **MySQL** via `mysqli` or `pdo_mysql` extension,
- **PostgreSQL** via `pgsql` or `pdo_pgsql` extension,
- **MS SQL Server** via `sqlsrv` extension.
- **MS SQL Server** via `sqlsrv` or `pdo_sqlsrv` extension.

Integrations:
- Symfony Bundle
Expand Down
182 changes: 182 additions & 0 deletions src/Drivers/PdoSqlsrv/PdoSqlsrvDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?php declare(strict_types = 1);

namespace Nextras\Dbal\Drivers\PdoSqlsrv;


use Exception;
use Nextras\Dbal\Connection;
use Nextras\Dbal\Drivers\Exception\ConnectionException;
use Nextras\Dbal\Drivers\Exception\DriverException;
use Nextras\Dbal\Drivers\Exception\ForeignKeyConstraintViolationException;
use Nextras\Dbal\Drivers\Exception\NotNullConstraintViolationException;
use Nextras\Dbal\Drivers\Exception\QueryException;
use Nextras\Dbal\Drivers\Exception\UniqueConstraintViolationException;
use Nextras\Dbal\Drivers\Pdo\PdoDriver;
use Nextras\Dbal\Exception\NotSupportedException;
use Nextras\Dbal\ILogger;
use Nextras\Dbal\Platforms\IPlatform;
use Nextras\Dbal\Platforms\SqlServerPlatform;
use Nextras\Dbal\Result\IResultAdapter;
use Nextras\Dbal\Utils\DateTimeImmutable;
use PDO;
use PDOStatement;
use function in_array;


/**
* Driver for php_pdo_sqlsrv ext available at PECL or github.com/microsoft/msphpsql.
*
* Supported configuration options:
* - host - server name to connect;
* - port - port to connect;
* - database - db name to connect;
* - username - username to connect;
* - password - password to connect;
* - other driver's config option:
* - App
* - ConnectionPooling
* - Encrypt
* - Failover_Partner
* - LoginTimeout
* - ReturnDatesAsStrings
* - TraceFile
* - TraceOn
* - TransactionIsolation
* - TrustServerCertificate
* - WSID
*/
class PdoSqlsrvDriver extends PdoDriver
{
public function connect(array $params, ILogger $logger): void
{
// see https://msdn.microsoft.com/en-us/library/ff628167.aspx
// see https://www.php.net/manual/en/ref.pdo-sqlsrv.connection.php
static $knownConnectionOptions = [
'App',
'ConnectionPooling',
'Encrypt',
'Failover_Partner',
'LoginTimeout',
'ReturnDatesAsStrings',
'TraceFile',
'TraceOn',
'TransactionIsolation',
'TrustServerCertificate',
'WSID',
];

$host = $params['host'] ?? '';
$port = $params['port'] ?? 5432;
$database = $params['database'] ?? '';
$username = $params['username'] ?? '';
$password = $params['password'] ?? '';

$dsn = "sqlsrv:Server=$host,$port;Database=$database";
foreach ($knownConnectionOptions as $knownOption) {
if (isset($params[$knownOption])) {
$dsn .= ";$knownOption={$params[$knownOption]}";
}
}

$options = [
PDO::SQLSRV_ATTR_DIRECT_QUERY => true,
];
$this->connectPdo($dsn, $username, $password, $options, $logger);

}


public function createPlatform(Connection $connection): IPlatform
{
return new SqlServerPlatform($connection);
}


public function getLastInsertedId(?string $sequenceName = null)
{
return $this->loggedQuery('SELECT SCOPE_IDENTITY()')->fetchField();
}


public function setTransactionIsolationLevel(int $level): void
{
static $levels = [
Connection::TRANSACTION_READ_UNCOMMITTED => 'READ UNCOMMITTED',
Connection::TRANSACTION_READ_COMMITTED => 'READ COMMITTED',
Connection::TRANSACTION_REPEATABLE_READ => 'REPEATABLE READ',
Connection::TRANSACTION_SERIALIZABLE => 'SERIALIZABLE',
];
if (!isset($levels[$level])) {
throw new NotSupportedException("Unsupported transaction level $level");
}
$this->loggedQuery("SET SESSION TRANSACTION ISOLATION LEVEL {$levels[$level]}");
}


public function createSavepoint(string $name): void
{
$this->loggedQuery('SAVE TRANSACTION ' . $this->convertIdentifierToSql($name));
}


public function releaseSavepoint(string $name): void
{
// transaction are released automatically
// http://stackoverflow.com/questions/3101312/sql-server-2008-no-release-savepoint-for-current-transaction
}


public function rollbackSavepoint(string $name): void
{
$this->loggedQuery('ROLLBACK TRANSACTION ' . $this->convertIdentifierToSql($name));
}


public function convertToPhp($value, $nativeType)
{
if (in_array($nativeType, ['numeric', 'decimal', 'money', 'smallmoney'], true)) {
return strpos($value, '.') === false ? (int) $value : (float) $value;

} elseif ($nativeType === 'datetimeoffset') {
return new DateTimeImmutable($value);

} else {
return parent::convertToPhp($value, $nativeType);
}
}


protected function createResultAdapter(PDOStatement $statement): IResultAdapter
{
return (new PdoSqlsrvResultAdapter($statement))->toBuffered();
}


protected function convertIdentifierToSql(string $identifier): string
{
return '[' . str_replace([']', '.'], [']]', '].['], $identifier) . ']';
}


protected function createException(string $error, int $errorNo, string $sqlState, ?string $query = null): Exception
{
if (in_array($sqlState, ['HYT00', '08001', '28000'], true) || stripos($error, 'Cannot open database') !== false) {
return new ConnectionException($error, $errorNo, $sqlState);

} elseif (in_array($errorNo, [547], true)) {
return new ForeignKeyConstraintViolationException($error, $errorNo, $sqlState, null, $query);

} elseif (in_array($errorNo, [2601, 2627], true)) {
return new UniqueConstraintViolationException($error, $errorNo, $sqlState, null, $query);

} elseif (in_array($errorNo, [515], true)) {
return new NotNullConstraintViolationException($error, $errorNo, $sqlState, null, $query);

} elseif ($query !== null) {
return new QueryException($error, $errorNo, $sqlState, null, $query);

} else {
return new DriverException($error, $errorNo, $sqlState);
}
}
}
115 changes: 115 additions & 0 deletions src/Drivers/PdoSqlsrv/PdoSqlsrvResultAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php declare(strict_types = 1);

namespace Nextras\Dbal\Drivers\PdoSqlsrv;


use Nextras\Dbal\Exception\InvalidStateException;
use Nextras\Dbal\Exception\NotSupportedException;
use Nextras\Dbal\Result\BufferedResultAdapter;
use Nextras\Dbal\Result\IResultAdapter;
use Nextras\Dbal\Utils\StrictObjectTrait;
use PDO;
use PDOStatement;


class PdoSqlsrvResultAdapter implements IResultAdapter
{
use StrictObjectTrait;


/** @var array<string, int> */
protected static $types = [
'bit' => self::TYPE_BOOL,

'bigint' => self::TYPE_INT,
'int' => self::TYPE_INT,
'smallint' => self::TYPE_INT,
'tinyint' => self::TYPE_INT,

'real' => self::TYPE_FLOAT,
'numeric' => self::TYPE_DRIVER_SPECIFIC,
'decimal' => self::TYPE_DRIVER_SPECIFIC,
'money' => self::TYPE_DRIVER_SPECIFIC,
'smallmoney' => self::TYPE_DRIVER_SPECIFIC,

'time' => self::TYPE_DATETIME,
'date' => self::TYPE_DATETIME,
'smalldatetime' => self::TYPE_DATETIME,
'datetimeoffset' => self::TYPE_DRIVER_SPECIFIC,
'datetime' => self::TYPE_DATETIME,
'datetime2' => self::TYPE_DATETIME,
];

/** @var PDOStatement<mixed> */
protected $statement;

/** @var bool */
protected $beforeFirstFetch = true;


/**
* @param PDOStatement<mixed> $statement
*/
public function __construct(PDOStatement $statement)
{
$this->statement = $statement;
}


public function toBuffered(): IResultAdapter
{
return new BufferedResultAdapter($this);
}


public function toUnbuffered(): IResultAdapter
{
return $this;
}


public function seek(int $index): void
{
if ($index === 0 && $this->beforeFirstFetch) {
return;
}

throw new NotSupportedException("PDO does not support rewinding or seeking. Use Result::buffered() before first consume of the result.");
}


public function fetch(): ?array
{
$this->beforeFirstFetch = false;
$fetched = $this->statement->fetch(PDO::FETCH_ASSOC);
return $fetched !== false ? $fetched : null;
}


public function getTypes(): array
{
$types = [];
$count = $this->statement->columnCount();

for ($i = 0; $i < $count; $i++) {
$field = $this->statement->getColumnMeta($i);
if ($field === false) { // @phpstan-ignore-line
throw new InvalidStateException("Should not happen.");
}
$types[(string) $field['name']] = [
0 => self::$types[$field['sqlsrv:decl_type']]
?? self::$types[substr($field['sqlsrv:decl_type'], 0, -9)] // strip " identity" suffix
?? self::TYPE_AS_IS,
1 => $field['sqlsrv:decl_type'],
];
}

return $types;
}


public function getRowsCount(): int
{
return $this->statement->rowCount();
}
}
9 changes: 4 additions & 5 deletions src/Drivers/Sqlsrv/SqlsrvDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@


/**
* Driver for php-sqlsrv ext available at github.com/microsoft/msphpsql.
* Driver for php-sqlsrv ext available at PECL or github.com/microsoft/msphpsql.
*
* Supported configuration options:
* - host - server name to connect;
Expand All @@ -39,7 +39,7 @@
* - CharacterSet
* - ConnectionPooling
* - Encrypt
* - Falover_Partner
* - Failover_Partner
* - LoginTimeout
* - MultipleActiveResultSet
* - MultiSubnetFailover
Expand Down Expand Up @@ -86,7 +86,7 @@ public function connect(array $params, ILogger $logger): void
'CharacterSet',
'ConnectionPooling',
'Encrypt',
'Falover_Partner',
'Failover_Partner',
'LoginTimeout',
'MultipleActiveResultSet',
'MultiSubnetFailover',
Expand Down Expand Up @@ -122,7 +122,6 @@ public function connect(array $params, ILogger $logger): void
throw new NotSupportedException("SqlsrvDriver does not allow to modify 'ReturnDatesAsStrings' parameter.");
}
$connectionOptions['ReturnDatesAsStrings'] = true;

$connectionResource = sqlsrv_connect($connectionString, $connectionOptions);
if ($connectionResource === false) {
$this->throwErrors();
Expand Down Expand Up @@ -236,7 +235,7 @@ public function setTransactionIsolationLevel(int $level): void
Connection::TRANSACTION_SERIALIZABLE => 'SERIALIZABLE',
];
if (!isset($levels[$level])) {
throw new NotSupportedException("Unsupported transation level $level");
throw new NotSupportedException("Unsupported transaction level $level");
}
$this->loggedQuery("SET SESSION TRANSACTION ISOLATION LEVEL {$levels[$level]}");
}
Expand Down
6 changes: 6 additions & 0 deletions tests/cases/integration/connection.sqlserver.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

namespace NextrasTests\Dbal;


use Nextras\Dbal\Drivers\Exception\QueryException;
use Tester\Assert;


Expand All @@ -29,6 +31,10 @@ class ConnectionSqlServerTest extends IntegrationTestCase

public function testLastInsertId()
{
$this->lockConnection($this->connection);
$this->connection->query('DROP TABLE IF EXISTS autoi_1');
$this->connection->query('DROP TABLE IF EXISTS autoi_2');

$this->connection->query('CREATE TABLE autoi_1 (a int NOT NULL IDENTITY PRIMARY KEY)');
$this->connection->query('CREATE TABLE autoi_2 (b int NOT NULL IDENTITY PRIMARY KEY)');

Expand Down
Loading

0 comments on commit 17f07b7

Please sign in to comment.