8000 Optimize and improve parameter conversion in OCI8Statement by morozov · Pull Request #2495 · doctrine/dbal · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Optimize and improve parameter conversion in OCI8Statement #2495

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 120 additions & 15 deletions lib/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,30 +121,135 @@ public function __construct($dbh, $statement, OCI8Connection $conn)
* @param string $statement The SQL statement to convert.
*
* @return string
* @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception
*/
static public function convertPositionalToNamedPlaceholders($statement)
{
$count = 1;
$inLiteral = false; // a valid query never starts with quotes
$stmtLen = strlen($statement);
$paramMap = array();
for ($i = 0; $i < $stmtLen; $i++) {
if ($statement[$i] == '?' && !$inLiteral) {
// real positional parameter detected
$paramMap[$count] = ":param$count";
$len = strlen($paramMap[$count]);
$statement = substr_replace($statement, ":param$count", $i, 1);
$i += $len-1; // jump ahead
$stmtLen = strlen($statement); // adjust statement length
++$count;
} elseif ($statement[$i] == "'" || $statement[$i] == '"') {
$inLiteral = ! $inLiteral; // switch state!
$fragmentOffset = $tokenOffset = 0;
$fragments = $paramMap = array();
$currentLiteralDelimiter = null;

do {
if (!$currentLiteralDelimiter) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand the code correctly, you are currently looping over the structure, and adding to the $paramMap as you go, checking for the " starting and closing delimiters. Is that correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this logic currently work with multiline literals?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It works with multiline literals same as with others (I'll add tests for that). In regexes, we don't search for any character classes which do or do not match the newline characters. We only search for ", ' and ?.

$result = self::findPlaceholderOrOpeningQuote(
$statement,
$tokenOffset,
$fragmentOffset,
$fragments,
$currentLiteralDelimiter,
$paramMap
);
} else {
$result = self::findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
}
} while ($result);

if ($currentLiteralDelimiter) {
throw new OCI8Exception(sprintf(
'The statement contains non-terminated string literal starting at offset %d',
$tokenOffset - 1
));
}

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

return array($statement, $paramMap);
}

/**
* Finds next placeholder or opening quote.
*
* @param string $statement The SQL statement to parse
* @param string $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 array<int, string> $paramMap Mapping of the original parameter positions to their named replacements
* @return bool Whether the token was found
*/
private static function findPlaceholderOrOpeningQuote(
$statement,
&$tokenOffset,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having by-ref parameters is not something I'd want to maintain/debug later on. I see why it's made, but is there a way to put them together instead? Can we create an object that is passed down instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can move all the parsing logic (not only the by-ref variables) into a different class. It might look like:

$foo = new Foo($sql);
$newSql = $foo->getSql();
$paramMap = $foo->getParameterMap();

But I don't like that it only does get*() but doesn't do do*(). Maybe it could be a proxy (or sub-) class which would translate all messages with positional parameters (__construct(), bind*()) to the other object which only understands the named ones?

&$fragmentOffset,
&$fragments,
&$currentLiteralDelimiter,
&$paramMap
) {
$token = self::findToken($statement, $tokenOffset, '/[?\'"]/');

if (!$token) {
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 string $tokenOffset The offset to start searching from
* @param string|null $currentLiteralDelimiter The delimiter of the current string literal
* or NULL if not currently in a literal
* @return bool Whether the token was found
*/
private static function findClosingQuote(
$statement,
&$tokenOffset,
&$currentLiteralDelimiter
) {
$token = self::findToken(
$statement,
$tokenOffset,
'/' . preg_quote($currentLiteralDelimiter, '/') . '/'
);

if (!$token) {
return false;
}

$currentLiteralDelimiter = false;
++$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 string $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 static function findToken($statement, &$offset, $regex)
{
if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset)) {
$offset = $matches[0][1];
return $matches[0][0];
}

return null;
}

/**
* {@inheritdoc}
*/
Expand Down
33 changes: 32 additions & 1 deletion tests/Doctrine/Tests/DBAL/Driver/OCI8/OCI8StatementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

namespace Doctrine\Tests\DBAL;

class OCI8StatementTest extends \Doctrine\Tests\DbalTestCase
use Doctrine\DBAL\Driver\OCI8\OCI8Exception;
use Doctrine\DBAL\Driver\OCI8\OCI8Statement;
use Doctrine\Tests\DbalTestCase;

class OCI8StatementTest extends DbalTestCase
{
protected function setUp()
{
Expand Down Expand Up @@ -81,4 +85,31 @@ public static function executeDataProvider()
);
}

/**
* @dataProvider nonTerminatedLiteralProvider
*/
public function testConvertNonTerminatedLiteral($sql, $message)
{
$this->expectException(OCI8Exception::class);
$this->expectExceptionMessageRegExp($message);
OCI8Statement::convertPositionalToNamedPlaceholders($sql);
}

public static function nonTerminatedLiteralProvider()
{
return array(
'no-matching-quote' => array(
"SELECT 'literal FROM DUAL",
'/offset 7/',
),
'no-matching-double-quote' => array(
'SELECT 1 "COL1 FROM DUAL',
'/offset 9/',
),
'incorrect-escaping-syntax' => array(
"SELECT 'quoted \\'string' FROM DUAL",
'/offset 23/',
),
);
}
}
85 changes: 85 additions & 0 deletions tests/Doctrine/Tests/DBAL/Functional/Driver/OCI8/StatementTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace Doctrine\Tests\DBAL\Functional\Driver\OCI8;

use Doctrine\DBAL\Driver\OCI8\Driver;
use Doctrine\Tests\DbalFunctionalTestCase;

class StatementTest extends DbalFunctionalTestCase
{
protected function setUp()
{
if (! extension_loaded('oci8')) {
$this->markTestSkipped('oci8 is not installed.');
}

parent::setUp();

if (! $this->_conn->getDriver() instanceof Driver) {
$this->markTestSkipped('oci8 only test.');
}
}

/**
* @dataProvider queryConversionProvider
*/
public function testQueryConversion($query, array $params, array $expected)
{
$this->assertEquals(
$expected,
$this->_conn->executeQuery($query, $params)->fetch()
);
}

public static function queryConversionProvider()
{
return array(
'simple' => array(
'SELECT ? COL1 FROM DUAL',
array(1),
array(
'COL1' => 1,
),
),
'literal-with-placeholder' => array(
"SELECT '?' COL1, ? COL2 FROM DUAL",
array(2),
array(
'COL1' => '?',
'COL2' => 2,
),
),
'literal-with-quotes' => array(
"SELECT ? COL1, '?\"?''?' \"COL?\" FROM DUAL",
array(3),
array(
'COL1' => 3,
'COL?' => '?"?\'?',
),
),
'placeholder-at-the-end' => array(
'SELECT ? COL1 FROM DUAL WHERE 1 = ?',
array(4, 1),
array(
'COL1' => 4,
),
),
'multi-line-literal' => array(
"SELECT 'Hello,
World?!' COL1 FROM DUAL WHERE 1 = ?",
array(1),
array(
'COL1' => 'Hello,
World?!',
),
),
'empty-literal' => array(
"SELECT '' COL1 FROM DUAL",
array(),
array(
'COL1' => '',
),
),
);
}
}
0