Commit c8aa1f07 authored by Andrew Vit's avatar Andrew Vit Committed by Benjamin Eberlei

Fix SQL placeholder parsing for consistent names and escaped literals

parent 4c33f633
...@@ -32,6 +32,13 @@ use Doctrine\DBAL\Connection; ...@@ -32,6 +32,13 @@ use Doctrine\DBAL\Connection;
*/ */
class SQLParserUtils class SQLParserUtils
{ {
const POSITIONAL_TOKEN = '\?';
const NAMED_TOKEN = ':[a-zA-Z_][a-zA-Z0-9_]*';
// Quote characters within string literals can be preceded by a backslash.
const ESCAPED_SINGLE_QUOTED_TEXT = "'(?:[^'\\\\]|\\\\'|\\\\\\\\)*'";
const ESCAPED_DOUBLE_QUOTED_TEXT = '"(?:[^"\\\\]|\\\\"|\\\\\\\\)*"';
/** /**
* Get an array of the placeholders in an sql statements as keys and their positions in the query string. * Get an array of the placeholders in an sql statements as keys and their positions in the query string.
* *
...@@ -49,27 +56,18 @@ class SQLParserUtils ...@@ -49,27 +56,18 @@ class SQLParserUtils
return array(); return array();
} }
$count = 0; $token = ($isPositional) ? self::POSITIONAL_TOKEN : self::NAMED_TOKEN;
$inLiteral = false; // a valid query never starts with quotes
$stmtLen = strlen($statement);
$paramMap = array(); $paramMap = array();
for ($i = 0; $i < $stmtLen; $i++) {
if ($statement[$i] == $match && !$inLiteral && ($isPositional || $statement[$i+1] != '=')) { foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
// real positional parameter detected preg_match_all("/$token/", $fragment[0], $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $placeholder) {
if ($isPositional) { if ($isPositional) {
$paramMap[$count] = $i; $paramMap[] = $placeholder[1] + $fragment[1];
} else { } else {
$name = ""; $pos = $placeholder[1] + $fragment[1];
// TODO: Something faster/better to match this than regex? $paramMap[$pos] = substr($placeholder[0], 1, strlen($placeholder[0]));
for ($j = $i + 1; ($j < $stmtLen && preg_match('(([a-zA-Z0-9_]{1}))', $statement[$j])); $j++) {
$name .= $statement[$j];
}
$paramMap[$i] = $name; // named parameters can be duplicated!
$i = $j;
} }
++$count;
} else if ($statement[$i] == "'" || $statement[$i] == '"') {
$inLiteral = ! $inLiteral; // switch state!
} }
} }
...@@ -180,4 +178,23 @@ class SQLParserUtils ...@@ -180,4 +178,23 @@ class SQLParserUtils
return array($query, $paramsOrd, $typesOrd); return array($query, $paramsOrd, $typesOrd);
} }
}
/**
* Slice the SQL statement around pairs of quotes and
* return string fragments of SQL outside of quoted literals.
* Each fragment is captured as a 2-element array:
*
* 0 => matched fragment string,
* 1 => offset of fragment in $statement
*
* @param string $statement
* @return array
*/
static private function getUnquotedStatementFragments($statement)
{
$literal = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' . self::ESCAPED_DOUBLE_QUOTED_TEXT;
preg_match_all("/([^'\"]+)(?:$literal)?/s", $statement, $fragments, PREG_OFFSET_CAPTURE);
return $fragments[1];
}
}
\ No newline at end of file
...@@ -28,6 +28,13 @@ class SQLParserUtilsTest extends \Doctrine\Tests\DbalTestCase ...@@ -28,6 +28,13 @@ class SQLParserUtilsTest extends \Doctrine\Tests\DbalTestCase
array("SELECT '?' FROM foo", true, array()), array("SELECT '?' FROM foo", true, array()),
array('SELECT "?" FROM foo WHERE bar = ?', true, array(32)), array('SELECT "?" FROM foo WHERE bar = ?', true, array(32)),
array("SELECT '?' FROM foo WHERE bar = ?", true, array(32)), array("SELECT '?' FROM foo WHERE bar = ?", true, array(32)),
array(
<<<'SQLDATA'
SELECT * FROM foo WHERE bar = 'it\'s a trap? \\' OR bar = ?
AND baz = "\"quote\" me on it? \\" OR baz = ?
SQLDATA
, true, array(58, 104)
),
// named // named
array('SELECT :foo FROM :bar', false, array(7 => 'foo', 17 => 'bar')), array('SELECT :foo FROM :bar', false, array(7 => 'foo', 17 => 'bar')),
...@@ -37,6 +44,7 @@ class SQLParserUtilsTest extends \Doctrine\Tests\DbalTestCase ...@@ -37,6 +44,7 @@ class SQLParserUtilsTest extends \Doctrine\Tests\DbalTestCase
array('SELECT :foo_id', false, array(7 => 'foo_id')), // Ticket DBAL-231 array('SELECT :foo_id', false, array(7 => 'foo_id')), // Ticket DBAL-231
array('SELECT @rank := 1', false, array()), // Ticket DBAL-398 array('SELECT @rank := 1', false, array()), // Ticket DBAL-398
array('SELECT @rank := 1 AS rank, :foo AS foo FROM :bar', false, array(27 => 'foo', 44 => 'bar')), // Ticket DBAL-398 array('SELECT @rank := 1 AS rank, :foo AS foo FROM :bar', false, array(27 => 'foo', 44 => 'bar')), // Ticket DBAL-398
array('SELECT * FROM Foo WHERE bar > :start_date AND baz > :start_date', false, array(30 => 'start_date', 52 => 'start_date')) // Ticket GH-113
); );
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment