Commit f8604e14 authored by Guilherme Blanco's avatar Guilherme Blanco

Merge pull request #113 from avit/master

Fixed SQL placeholder parsing
parents 328d8a53 153b752f
......@@ -32,6 +32,13 @@ use Doctrine\DBAL\Connection;
*/
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.
*
......@@ -49,27 +56,18 @@ class SQLParserUtils
return array();
}
$count = 0;
$inLiteral = false; // a valid query never starts with quotes
$stmtLen = strlen($statement);
$token = ($isPositional) ? self::POSITIONAL_TOKEN : self::NAMED_TOKEN;
$paramMap = array();
for ($i = 0; $i < $stmtLen; $i++) {
if ($statement[$i] == $match && !$inLiteral && ($isPositional || $statement[$i+1] != '=')) {
// real positional parameter detected
foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
preg_match_all("/$token/", $fragment[0], $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $placeholder) {
if ($isPositional) {
$paramMap[$count] = $i;
$paramMap[] = $placeholder[1] + $fragment[1];
} else {
$name = "";
// TODO: Something faster/better to match this than regex?
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;
$pos = $placeholder[1] + $fragment[1];
$paramMap[$pos] = substr($placeholder[0], 1, strlen($placeholder[0]));
}
++$count;
} else if ($statement[$i] == "'" || $statement[$i] == '"') {
$inLiteral = ! $inLiteral; // switch state!
}
}
......@@ -180,4 +178,23 @@ class SQLParserUtils
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
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(
<<<'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
array('SELECT :foo FROM :bar', false, array(7 => 'foo', 17 => 'bar')),
......@@ -37,6 +44,7 @@ class SQLParserUtilsTest extends \Doctrine\Tests\DbalTestCase
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 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