From 153b752fa64b85bbc29559933cd90466c2aa8c33 Mon Sep 17 00:00:00 2001 From: Andrew Vit Date: Mon, 13 Feb 2012 23:45:59 +0000 Subject: [PATCH] Fix SQL placeholder parsing for consistent names and escaped literals --- lib/Doctrine/DBAL/SQLParserUtils.php | 53 ++++++++++++------- .../Tests/DBAL/SQLParserUtilsTest.php | 8 +++ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/lib/Doctrine/DBAL/SQLParserUtils.php b/lib/Doctrine/DBAL/SQLParserUtils.php index 8e3827fbea4..502c0fc3419 100644 --- a/lib/Doctrine/DBAL/SQLParserUtils.php +++ b/lib/Doctrine/DBAL/SQLParserUtils.php @@ -32,6 +32,13 @@ */ 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 @@ static public function getPlaceholderPositions($statement, $isPositional = true) 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 @@ static public function expandListParameters($query, $params, $types) 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 diff --git a/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php b/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php index ce2e7ba709d..ea341b1451b 100644 --- a/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php +++ b/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php @@ -28,6 +28,13 @@ static public function dataGetPlaceholderPositions() 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 @@ static public function dataGetPlaceholderPositions() 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 ); }