Skip to content

Commit

Permalink
Merge pull request #4109 from morozov/oci8-finalization-backports
Browse files Browse the repository at this point in the history
Preparation to marking OCI8 driver classes final
  • Loading branch information
morozov authored Jun 26, 2020
2 parents 8a57bcb + 2dfe0af commit 9d45766
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 246 deletions.
2 changes: 1 addition & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
This one is just too convoluted for Psalm to figure out, by
its author's own admission
-->
<file name="src/Driver/OCI8/OCI8Statement.php"/>
<file name="src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php"/>
</errorLevel>
</ConflictingReferenceConstraint>
<FalsableReturnStatement>
Expand Down
162 changes: 162 additions & 0 deletions src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\OCI8;

use Doctrine\DBAL\Driver\OCI8\Exception\NonTerminatedStringLiteral;

use function count;
use function implode;
use function preg_match;
use function preg_quote;
use function substr;

use const PREG_OFFSET_CAPTURE;

/**
* Converts positional (?) into named placeholders (:param<num>).
*
* Oracle does not support positional parameters, hence this method converts all
* positional parameters into artificially named parameters. Note that this conversion
* is not perfect. All question marks (?) in the original statement are treated as
* placeholders and converted to a named parameter.
*
* @internal This class is not covered by the backward compatibility promise
*/
final class ConvertPositionalToNamedPlaceholders
{
/**
* @param string $statement The SQL statement to convert.
*
* @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
*
* @throws OCI8Exception
*/
public function __invoke(string $statement): array
{
$fragmentOffset = $tokenOffset = 0;
$fragments = $paramMap = [];
$currentLiteralDelimiter = null;

do {
if ($currentLiteralDelimiter === null) {
$result = $this->findPlaceholderOrOpeningQuote(
$statement,
$tokenOffset,
$fragmentOffset,
$fragments,
$currentLiteralDelimiter,
$paramMap
);
} else {
$result = $this->findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
}
} while ($result);

if ($currentLiteralDelimiter !== null) {
throw NonTerminatedStringLiteral::new($tokenOffset - 1);
}

$fragments[] = substr($statement, $fragmentOffset);
$statement = implode('', $fragments);

return [$statement, $paramMap];
}

/**
* Finds next placeholder or opening quote.
*
* @param string $statement The SQL statement to parse
* @param int $tokenOffset The offset to start searching from
* @param int $fragmentOffset The offset to build the next fragment from
* @param string[] $fragments Fragments of the original statement not containing placeholders
* @param string|null $currentLiteralDelimiter The delimiter of the current string literal
* or NULL if not currently in a literal
* @param string[] $paramMap Mapping of the original parameter positions to their named replacements
*
* @return bool Whether the token was found
*/
private function findPlaceholderOrOpeningQuote(
string $statement,
int &$tokenOffset,
int &$fragmentOffset,
array &$fragments,
?string &$currentLiteralDelimiter,
array &$paramMap
): bool {
$token = $this->findToken($statement, $tokenOffset, '/[?\'"]/');

if ($token === null) {
return false;
}

if ($token === '?') {
$position = count($paramMap) + 1;
$param = ':param' . $position;
$fragments[] = substr($statement, $fragmentOffset, $tokenOffset - $fragmentOffset);
$fragments[] = $param;
$paramMap[$position] = $param;
$tokenOffset += 1;
$fragmentOffset = $tokenOffset;

return true;
}

$currentLiteralDelimiter = $token;
++$tokenOffset;

return true;
}

/**
* Finds closing quote
*
* @param string $statement The SQL statement to parse
* @param int $tokenOffset The offset to start searching from
* @param string $currentLiteralDelimiter The delimiter of the current string literal
*
* @return bool Whether the token was found
*/
private function findClosingQuote(
string $statement,
int &$tokenOffset,
string &$currentLiteralDelimiter
): bool {
$token = $this->findToken(
$statement,
$tokenOffset,
'/' . preg_quote($currentLiteralDelimiter, '/') . '/'
);

if ($token === null) {
return false;
}

$currentLiteralDelimiter = null;
++$tokenOffset;

return true;
}

/**
* Finds the token described by regex starting from the given offset. Updates the offset with the position
* where the token was found.
*
* @param string $statement The SQL statement to parse
* @param int $offset The offset to start searching from
* @param string $regex The regex containing token pattern
*
* @return string|null Token or NULL if not found
*/
private function findToken(string $statement, int &$offset, string $regex): ?string
{
if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset) === 1) {
$offset = $matches[0][1];

return $matches[0][0];
}

return null;
}
}
31 changes: 31 additions & 0 deletions src/Driver/OCI8/ExecutionMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Driver\OCI8;

/**
* Encapsulates the execution mode that is shared between the connection and its statements.
*
* @internal This class is not covered by the backward compatibility promise
*/
final class ExecutionMode
{
/** @var bool */
private $isAutoCommitEnabled = true;

public function enableAutoCommit(): void
{
$this->isAutoCommitEnabled = true;
}

public function disableAutoCommit(): void
{
$this->isAutoCommitEnabled = false;
}

public function isAutoCommitEnabled(): bool
{
return $this->isAutoCommitEnabled;
}
}
26 changes: 8 additions & 18 deletions src/Driver/OCI8/OCI8Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
use function sprintf;
use function str_replace;

use const OCI_COMMIT_ON_SUCCESS;
use const OCI_NO_AUTO_COMMIT;

/**
Expand All @@ -36,8 +35,8 @@ class OCI8Connection implements ConnectionInterface, ServerInfoAwareConnection
/** @var resource */
protected $dbh;

/** @var int */
protected $executeMode = OCI_COMMIT_ON_SUCCESS;
/** @var ExecutionMode */
private $executionMode;

/**
* Creates a Connection to an Oracle Database using oci8 extension.
Expand Down Expand Up @@ -67,7 +66,8 @@ public function __construct(
throw OCI8Exception::fromErrorInfo(oci_error());
}

$this->dbh = $dbh;
$this->dbh = $dbh;
$this->executionMode = new ExecutionMode();
}

/**
Expand Down Expand Up @@ -107,7 +107,7 @@ public function requiresQueryForServerVersion()

public function prepare(string $sql): DriverStatement
{
return new Statement($this->dbh, $sql, $this);
return new Statement($this->dbh, $sql, $this->executionMode);
}

public function query(string $sql): ResultInterface
Expand Down Expand Up @@ -154,22 +154,12 @@ public function lastInsertId($name = null)
return (int) $result;
}

/**
* Returns the current execution mode.
*
* @return int
*/
public function getExecuteMode()
{
return $this->executeMode;
}

/**
* {@inheritdoc}
*/
public function beginTransaction()
{
$this->executeMode = OCI_NO_AUTO_COMMIT;
$this->executionMode->disableAutoCommit();

return true;
}
Expand All @@ -183,7 +173,7 @@ public function commit()
throw OCI8Exception::fromErrorInfo(oci_error($this->dbh));
}

$this->executeMode = OCI_COMMIT_ON_SUCCESS;
$this->executionMode->enableAutoCommit();

return true;
}
Expand All @@ -197,7 +187,7 @@ public function rollBack()
throw OCI8Exception::fromErrorInfo(oci_error($this->dbh));
}

$this->executeMode = OCI_COMMIT_ON_SUCCESS;
$this->executionMode->enableAutoCommit();

return true;
}
Expand Down
Loading

0 comments on commit 9d45766

Please sign in to comment.