Skip to content

Commit

Permalink
SQLSrv, restore driver resource offsets
Browse files Browse the repository at this point in the history
Adding drivers to completed set.
  • Loading branch information
mcurland committed Dec 10, 2023
1 parent 5d5bccc commit 0d333a7
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 3 deletions.
98 changes: 98 additions & 0 deletions src/Driver/PDO/SQLSrv/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,26 @@
use Doctrine\DBAL\Driver\Exception\UnknownParameterType;
use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware;
use Doctrine\DBAL\Driver\PDO\Statement as PDOStatement;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\ParameterType;
use Doctrine\Deprecations\Deprecation;
use PDO;

use function fseek;
use function ftell;
use function func_num_args;
use function is_resource;
use function stream_get_meta_data;

use const SEEK_SET;

final class Statement extends AbstractStatementMiddleware
{
private PDOStatement $statement;

/** @var mixed[]|null */
private ?array $paramResources = null;

/** @internal The statement can be only instantiated by its driver connection. */
public function __construct(PDOStatement $statement)
{
Expand Down Expand Up @@ -104,6 +114,94 @@ public function bindValue($param, $value, $type = ParameterType::STRING): bool
);
}

if ($type === ParameterType::LARGE_OBJECT || $type === ParameterType::BINARY) {
$this->trackParamResource($value);
}

return $this->bindParam($param, $value, $type);
}

/**
* {@inheritDoc}
*/
public function execute($params = null): Result
{
$resourceOffsets = $this->getResourceOffsets();
try {
return parent::execute($params);
} finally {
if ($resourceOffsets !== null) {
$this->restoreResourceOffsets($resourceOffsets);
}
}
}

/**
* Track a binary parameter reference at binding time. These
* are cached for later analysis by the getResourceOffsets.
*
* @param mixed $resource
*/
private function trackParamResource($resource): void
{
if (! is_resource($resource)) {
return;
}

$this->paramResources ??= [];
$this->paramResources[] = $resource;
}

/**
* Determine the offset that any resource parameters needs to be
* restored to after the statement is executed. Call immediately
* before execute (not during bindValue) to get the most accurate offset.
*
* @return int[]|null Return offsets to restore if needed. The array may be sparse.
*/
private function getResourceOffsets(): ?array
{
if ($this->paramResources === null) {
return null;
}

$resourceOffsets = null;
foreach ($this->paramResources as $index => $resource) {
$position = false;
if (stream_get_meta_data($resource)['seekable']) {
$position = ftell($resource);
}

if ($position === false) {
continue;

Check warning on line 176 in src/Driver/PDO/SQLSrv/Statement.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/PDO/SQLSrv/Statement.php#L176

Added line #L176 was not covered by tests
}

$resourceOffsets ??= [];
$resourceOffsets[$index] = $position;
}

if ($resourceOffsets === null) {
$this->paramResources = null;

Check warning on line 184 in src/Driver/PDO/SQLSrv/Statement.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/PDO/SQLSrv/Statement.php#L184

Added line #L184 was not covered by tests
}

return $resourceOffsets;
}

/**
* Restore resource offsets moved by PDOStatement->execute
*
* @param int[]|null $resourceOffsets The offsets returned by getResourceOffsets.
*/
private function restoreResourceOffsets(?array $resourceOffsets): void
{
if ($resourceOffsets === null || $this->paramResources === null) {
return;

Check warning on line 198 in src/Driver/PDO/SQLSrv/Statement.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/PDO/SQLSrv/Statement.php#L198

Added line #L198 was not covered by tests
}

foreach ($resourceOffsets as $index => $offset) {
fseek($this->paramResources[$index], $offset, SEEK_SET);
}

$this->paramResources = null;
}
}
98 changes: 95 additions & 3 deletions src/Driver/SQLSrv/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@
use Doctrine\Deprecations\Deprecation;

use function assert;
use function fseek;
use function ftell;
use function func_num_args;
use function is_int;
use function is_resource;
use function sqlsrv_execute;
use function SQLSRV_PHPTYPE_STREAM;
use function SQLSRV_PHPTYPE_STRING;
use function sqlsrv_prepare;
use function SQLSRV_SQLTYPE_VARBINARY;
use function stream_get_meta_data;
use function stripos;

use const SEEK_SET;
use const SQLSRV_ENC_BINARY;
use const SQLSRV_ENC_CHAR;
use const SQLSRV_PARAM_IN;
Expand Down Expand Up @@ -58,6 +63,13 @@ final class Statement implements StatementInterface
*/
private array $types = [];

/**
* Resources used as bound values.
*
* @var mixed[]|null
*/
private ?array $paramResources = null;

/**
* Append to any INSERT query to retrieve the last insert id.
*/
Expand Down Expand Up @@ -97,6 +109,10 @@ public function bindValue($param, $value, $type = ParameterType::STRING): bool
);
}

if ($type === ParameterType::LARGE_OBJECT || $type === ParameterType::BINARY) {
$this->trackParamResource($value);
}

$this->variables[$param] = $value;
$this->types[$param] = $type;

Expand Down Expand Up @@ -159,10 +175,17 @@ public function execute($params = null): ResultInterface
}
}

$this->stmt ??= $this->prepare();
$resourceOffsets = $this->getResourceOffsets();
try {
$this->stmt ??= $this->prepare();

if (! sqlsrv_execute($this->stmt)) {
throw Error::new();
if (! sqlsrv_execute($this->stmt)) {
throw Error::new();
}
} finally {
if ($resourceOffsets !== null) {
$this->restoreResourceOffsets($resourceOffsets);
}
}

return new Result($this->stmt);
Expand Down Expand Up @@ -220,4 +243,73 @@ private function prepare()

return $stmt;
}

/**
* Track a binary parameter reference at binding time. These
* are cached for later analysis by the getResourceOffsets.
*
* @param mixed $resource
*/
private function trackParamResource($resource): void
{
if (! is_resource($resource)) {
return;
}

$this->paramResources ??= [];
$this->paramResources[] = $resource;
}

/**
* Determine the offset that any resource parameters needs to be
* restored to after the statement is executed. Call immediately
* before execute (not during bindValue) to get the most accurate offset.
*
* @return int[]|null Return offsets to restore if needed. The array may be sparse.
*/
private function getResourceOffsets(): ?array
{
if ($this->paramResources === null) {
return null;
}

$resourceOffsets = null;
foreach ($this->paramResources as $index => $resource) {
$position = false;
if (stream_get_meta_data($resource)['seekable']) {
$position = ftell($resource);
}

if ($position === false) {
continue;

Check warning on line 284 in src/Driver/SQLSrv/Statement.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/SQLSrv/Statement.php#L284

Added line #L284 was not covered by tests
}

$resourceOffsets ??= [];
$resourceOffsets[$index] = $position;
}

if ($resourceOffsets === null) {
$this->paramResources = null;

Check warning on line 292 in src/Driver/SQLSrv/Statement.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/SQLSrv/Statement.php#L292

Added line #L292 was not covered by tests
}

return $resourceOffsets;
}

/**
* Restore resource offsets moved by PDOStatement->execute
*
* @param int[]|null $resourceOffsets The offsets returned by getResourceOffsets.
*/
private function restoreResourceOffsets(?array $resourceOffsets): void
{
if ($resourceOffsets === null || $this->paramResources === null) {
return;

Check warning on line 306 in src/Driver/SQLSrv/Statement.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/SQLSrv/Statement.php#L306

Added line #L306 was not covered by tests
}

foreach ($resourceOffsets as $index => $offset) {
fseek($this->paramResources[$index], $offset, SEEK_SET);
}

$this->paramResources = null;
}
}

0 comments on commit 0d333a7

Please sign in to comment.