-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Changes from all commits
724a662
4863bd2
c1d2c6a
3b55e2b
9c3afca
8cbf648
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) { | ||
$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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
&$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} | ||
*/ | ||
|
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' => '', | ||
), | ||
), | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
.