diff --git a/psalm.xml b/psalm.xml
index 2d8a65dc940..8ce2b9b9b11 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -75,14 +75,6 @@
-
-
-
-
-
-
diff --git a/src/ArrayParameters/Exception.php b/src/ArrayParameters/Exception.php
new file mode 100644
index 00000000000..cce84b25bf0
--- /dev/null
+++ b/src/ArrayParameters/Exception.php
@@ -0,0 +1,12 @@
+ 0) {
- [$sql, $params, $types] = SQLParserUtils::expandListParameters($sql, $params, $types);
+ if ($this->needsArrayParameterConversion($params, $types)) {
+ [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types);
+ }
$stmt = $connection->prepare($sql);
if (count($types) > 0) {
@@ -1118,7 +1124,9 @@ public function executeStatement($sql, array $params = [], array $types = [])
try {
if (count($params) > 0) {
- [$sql, $params, $types] = SQLParserUtils::expandListParameters($sql, $params, $types);
+ if ($this->needsArrayParameterConversion($params, $types)) {
+ [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types);
+ }
$stmt = $connection->prepare($sql);
@@ -1581,13 +1589,11 @@ private function _bindTypedValues(DriverStatement $stmt, array $params, array $t
{
// Check whether parameters are positional or named. Mixing is not allowed.
if (is_int(key($params))) {
- // Positional parameters
- $typeOffset = array_key_exists(0, $types) ? -1 : 0;
- $bindIndex = 1;
- foreach ($params as $value) {
- $typeIndex = $bindIndex + $typeOffset;
- if (isset($types[$typeIndex])) {
- $type = $types[$typeIndex];
+ $bindIndex = 1;
+
+ foreach ($params as $key => $value) {
+ if (isset($types[$key])) {
+ $type = $types[$key];
[$value, $bindingType] = $this->getBindingInfo($value, $type);
$stmt->bindValue($bindIndex, $value, $bindingType);
} else {
@@ -1669,6 +1675,48 @@ final public function convertException(Driver\Exception $e): DriverException
return $this->handleDriverException($e, null);
}
+ /**
+ * @param array|array $params
+ * @param array|array $types
+ *
+ * @return array{string, list, array}
+ */
+ private function expandArrayParameters(string $sql, array $params, array $types): array
+ {
+ if ($this->parser === null) {
+ $this->parser = $this->getDatabasePlatform()->createSQLParser();
+ }
+
+ $visitor = new ExpandArrayParameters($params, $types);
+
+ $this->parser->parse($sql, $visitor);
+
+ return [
+ $visitor->getSQL(),
+ $visitor->getParameters(),
+ $visitor->getTypes(),
+ ];
+ }
+
+ /**
+ * @param array|array $params
+ * @param array|array $types
+ */
+ private function needsArrayParameterConversion(array $params, array $types): bool
+ {
+ if (is_string(key($params))) {
+ return true;
+ }
+
+ foreach ($types as $type) {
+ if ($type === self::PARAM_INT_ARRAY || $type === self::PARAM_STR_ARRAY) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private function handleDriverException(
Driver\Exception $driverException,
?Query $query
diff --git a/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php b/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php
index ba09397f329..483879d4726 100644
--- a/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php
+++ b/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php
@@ -1,164 +1,58 @@
).
*
* Oracle does not support positional parameters, hence this method converts all
- * positional parameters into artificially named parameters. Note that this conversion
- * is not perfect. All question marks (?) in the original statement are treated as
- * placeholders and converted to a named parameter.
+ * positional parameters into artificially named parameters.
*
* @internal This class is not covered by the backward compatibility promise
*/
-final class ConvertPositionalToNamedPlaceholders
+final class ConvertPositionalToNamedPlaceholders implements Visitor
{
- /**
- * @param string $statement The SQL statement to convert.
- *
- * @return mixed[] [0] => the statement value (string), [1] => the paramMap value (array).
- *
- * @throws Exception
- */
- public function __invoke(string $statement): array
- {
- $fragmentOffset = $tokenOffset = 0;
- $fragments = $paramMap = [];
- $currentLiteralDelimiter = null;
-
- do {
- if ($currentLiteralDelimiter === null) {
- $result = $this->findPlaceholderOrOpeningQuote(
- $statement,
- $tokenOffset,
- $fragmentOffset,
- $fragments,
- $currentLiteralDelimiter,
- $paramMap
- );
- } else {
- $result = $this->findClosingQuote($statement, $tokenOffset, $currentLiteralDelimiter);
- }
- } while ($result);
+ /** @var list */
+ private $buffer = [];
- if ($currentLiteralDelimiter !== null) {
- throw NonTerminatedStringLiteral::new($tokenOffset - 1);
- }
+ /** @var array */
+ private $parameterMap = [];
- $fragments[] = substr($statement, $fragmentOffset);
- $statement = implode('', $fragments);
-
- return [$statement, $paramMap];
+ public function acceptOther(string $sql): void
+ {
+ $this->buffer[] = $sql;
}
- /**
- * Finds next placeholder or opening quote.
- *
- * @param string $statement The SQL statement to parse
- * @param int $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 string[] $paramMap Mapping of the original parameter positions
- * to their named replacements
- *
- * @return bool Whether the token was found
- */
- private function findPlaceholderOrOpeningQuote(
- string $statement,
- int &$tokenOffset,
- int &$fragmentOffset,
- array &$fragments,
- ?string &$currentLiteralDelimiter,
- array &$paramMap
- ): bool {
- $token = $this->findToken($statement, $tokenOffset, '/[?\'"]/');
-
- if ($token === null) {
- 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;
- }
+ public function acceptPositionalParameter(string $sql): void
+ {
+ $position = count($this->parameterMap) + 1;
+ $param = ':param' . $position;
- $currentLiteralDelimiter = $token;
- ++$tokenOffset;
+ $this->parameterMap[$position] = $param;
- return true;
+ $this->buffer[] = $param;
}
- /**
- * Finds closing quote
- *
- * @param string $statement The SQL statement to parse
- * @param int $tokenOffset The offset to start searching from
- * @param string $currentLiteralDelimiter The delimiter of the current string literal
- *
- * @return bool Whether the token was found
- */
- private function findClosingQuote(
- string $statement,
- int &$tokenOffset,
- string &$currentLiteralDelimiter
- ): bool {
- $token = $this->findToken(
- $statement,
- $tokenOffset,
- '/' . preg_quote($currentLiteralDelimiter, '/') . '/'
- );
-
- if ($token === null) {
- return false;
- }
-
- $currentLiteralDelimiter = null;
- ++$tokenOffset;
+ public function acceptNamedParameter(string $sql): void
+ {
+ $this->buffer[] = $sql;
+ }
- return true;
+ public function getSQL(): string
+ {
+ return implode('', $this->buffer);
}
/**
- * 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 int $offset The offset to start searching from
- * @param string $regex The regex containing token pattern
- *
- * @return string|null Token or NULL if not found
+ * @return array
*/
- private function findToken(string $statement, int &$offset, string $regex): ?string
+ public function getParameterMap(): array
{
- if (preg_match($regex, $statement, $matches, PREG_OFFSET_CAPTURE, $offset) === 1) {
- $offset = $matches[0][1];
-
- return $matches[0][0];
- }
-
- return null;
+ return $this->parameterMap;
}
}
diff --git a/src/Driver/OCI8/Statement.php b/src/Driver/OCI8/Statement.php
index 5257eb5453e..a1621c7ed1a 100644
--- a/src/Driver/OCI8/Statement.php
+++ b/src/Driver/OCI8/Statement.php
@@ -8,6 +8,7 @@
use Doctrine\DBAL\Driver\Result as ResultInterface;
use Doctrine\DBAL\Driver\Statement as StatementInterface;
use Doctrine\DBAL\ParameterType;
+use Doctrine\DBAL\SQL\Parser;
use function assert;
use function is_int;
@@ -60,14 +61,17 @@ final class Statement implements StatementInterface
*/
public function __construct($dbh, $query, ExecutionMode $executionMode)
{
- [$query, $paramMap] = (new ConvertPositionalToNamedPlaceholders())($query);
+ $parser = new Parser(false);
+ $visitor = new ConvertPositionalToNamedPlaceholders();
- $stmt = oci_parse($dbh, $query);
+ $parser->parse($query, $visitor);
+
+ $stmt = oci_parse($dbh, $visitor->getSQL());
assert(is_resource($stmt));
$this->_sth = $stmt;
$this->_dbh = $dbh;
- $this->_paramMap = $paramMap;
+ $this->_paramMap = $visitor->getParameterMap();
$this->executionMode = $executionMode;
}
diff --git a/src/ExpandArrayParameters.php b/src/ExpandArrayParameters.php
new file mode 100644
index 00000000000..063fd3f916f
--- /dev/null
+++ b/src/ExpandArrayParameters.php
@@ -0,0 +1,143 @@
+|array */
+ private $originalParameters;
+
+ /** @var array|array */
+ private $originalTypes;
+
+ /** @var int */
+ private $originalParameterIndex = 0;
+
+ /** @var list */
+ private $convertedSQL = [];
+
+ /** @var list */
+ private $convertedParameteres = [];
+
+ /** @var array */
+ private $convertedTypes = [];
+
+ /**
+ * @param array|array $parameters
+ * @param array|array $types
+ */
+ public function __construct(array $parameters, array $types)
+ {
+ $this->originalParameters = $parameters;
+ $this->originalTypes = $types;
+ }
+
+ public function acceptPositionalParameter(string $sql): void
+ {
+ $index = $this->originalParameterIndex;
+
+ if (! array_key_exists($index, $this->originalParameters)) {
+ throw MissingPositionalParameter::new($index);
+ }
+
+ $this->acceptParameter($index, $this->originalParameters[$index]);
+
+ $this->originalParameterIndex++;
+ }
+
+ public function acceptNamedParameter(string $sql): void
+ {
+ $name = substr($sql, 1);
+
+ if (! array_key_exists($name, $this->originalParameters)) {
+ throw MissingNamedParameter::new($name);
+ }
+
+ $this->acceptParameter($name, $this->originalParameters[$name]);
+ }
+
+ public function acceptOther(string $sql): void
+ {
+ $this->convertedSQL[] = $sql;
+ }
+
+ public function getSQL(): string
+ {
+ return implode('', $this->convertedSQL);
+ }
+
+ /**
+ * @return list
+ */
+ public function getParameters(): array
+ {
+ return $this->convertedParameteres;
+ }
+
+ /**
+ * @param int|string $key
+ * @param mixed $value
+ */
+ private function acceptParameter($key, $value): void
+ {
+ if (! isset($this->originalTypes[$key])) {
+ $this->convertedSQL[] = '?';
+ $this->convertedParameteres[] = $value;
+
+ return;
+ }
+
+ $type = $this->originalTypes[$key];
+
+ if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
+ $this->appendTypedParameter([$value], $type);
+
+ return;
+ }
+
+ if (count($value) === 0) {
+ $this->convertedSQL[] = 'NULL';
+
+ return;
+ }
+
+ $this->appendTypedParameter($value, $type - Connection::ARRAY_PARAM_OFFSET);
+ }
+
+ /**
+ * @return array
+ */
+ public function getTypes(): array
+ {
+ return $this->convertedTypes;
+ }
+
+ /**
+ * @param list $values
+ * @param Type|int|string|null $type
+ */
+ private function appendTypedParameter(array $values, $type): void
+ {
+ $this->convertedSQL[] = implode(', ', array_fill(0, count($values), '?'));
+
+ $index = count($this->convertedParameteres);
+
+ foreach ($values as $value) {
+ $this->convertedParameteres[] = $value;
+ $this->convertedTypes[$index] = $type;
+
+ $index++;
+ }
+ }
+}
diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php
index b0dcef25521..6ef2831958e 100644
--- a/src/Platforms/AbstractPlatform.php
+++ b/src/Platforms/AbstractPlatform.php
@@ -24,6 +24,7 @@
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Schema\UniqueConstraint;
+use Doctrine\DBAL\SQL\Parser;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;
use Doctrine\DBAL\Types\Type;
@@ -3543,6 +3544,14 @@ final public function escapeStringForLike(string $inputString, string $escapeCha
);
}
+ /**
+ * @internal
+ */
+ public function createSQLParser(): Parser
+ {
+ return new Parser(false);
+ }
+
protected function getLikeWildcardCharacters(): string
{
return '%_';
diff --git a/src/Platforms/MySQL57Platform.php b/src/Platforms/MySQL57Platform.php
index 669cac6e718..a3ec50441e4 100644
--- a/src/Platforms/MySQL57Platform.php
+++ b/src/Platforms/MySQL57Platform.php
@@ -4,6 +4,7 @@
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\TableDiff;
+use Doctrine\DBAL\SQL\Parser;
use Doctrine\DBAL\Types\Types;
/**
@@ -27,6 +28,11 @@ public function getJsonTypeDeclarationSQL(array $column)
return 'JSON';
}
+ public function createSQLParser(): Parser
+ {
+ return new Parser(true);
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/SQL/Parser.php b/src/SQL/Parser.php
new file mode 100644
index 00000000000..32d306ca0f1
--- /dev/null
+++ b/src/SQL/Parser.php
@@ -0,0 +1,117 @@
+getMySQLStringLiteralPattern("'"),
+ $this->getMySQLStringLiteralPattern('"'),
+ ];
+ } else {
+ $patterns = [
+ $this->getAnsiSQLStringLiteralPattern("'"),
+ $this->getAnsiSQLStringLiteralPattern('"'),
+ ];
+ }
+
+ $patterns = array_merge($patterns, [
+ self::BACKTICK_IDENTIFIER,
+ self::BRACKET_IDENTIFIER,
+ self::MULTICHAR,
+ self::ONE_LINE_COMMENT,
+ self::MULTI_LINE_COMMENT,
+ self::OTHER,
+ ]);
+
+ $this->sqlPattern = sprintf('(%s)+', implode('|', $patterns));
+ }
+
+ /**
+ * Parses the given SQL statement
+ */
+ public function parse(string $sql, Visitor $visitor): void
+ {
+ /** @var array $patterns */
+ $patterns = [
+ self::NAMED_PARAMETER => static function (string $sql) use ($visitor): void {
+ $visitor->acceptNamedParameter($sql);
+ },
+ self::POSITIONAL_PARAMETER => static function (string $sql) use ($visitor): void {
+ $visitor->acceptPositionalParameter($sql);
+ },
+ $this->sqlPattern => static function (string $sql) use ($visitor): void {
+ $visitor->acceptOther($sql);
+ },
+ self::SPECIAL => static function (string $sql) use ($visitor): void {
+ $visitor->acceptOther($sql);
+ },
+ ];
+
+ $offset = 0;
+
+ while (($handler = current($patterns)) !== false) {
+ if (preg_match('~\G' . key($patterns) . '~s', $sql, $matches, 0, $offset) === 1) {
+ $handler($matches[0]);
+ reset($patterns);
+
+ $offset += strlen($matches[0]);
+ } else {
+ next($patterns);
+ }
+ }
+
+ assert($offset === strlen($sql));
+ }
+
+ private function getMySQLStringLiteralPattern(string $delimiter): string
+ {
+ return $delimiter . '((\\\\' . self::ANY . ')|(?![' . $delimiter . '\\\\])' . self::ANY . ')*' . $delimiter;
+ }
+
+ private function getAnsiSQLStringLiteralPattern(string $delimiter): string
+ {
+ return $delimiter . '[^' . $delimiter . ']*' . $delimiter;
+ }
+}
diff --git a/src/SQL/Parser/Visitor.php b/src/SQL/Parser/Visitor.php
new file mode 100644
index 00000000000..574ba1bf166
--- /dev/null
+++ b/src/SQL/Parser/Visitor.php
@@ -0,0 +1,26 @@
+
- */
- private static function getPositionalPlaceholderPositions(string $statement): array
- {
- return self::collectPlaceholders(
- $statement,
- '?',
- self::POSITIONAL_TOKEN,
- static function (string $_, int $placeholderPosition, int $fragmentPosition, array &$carry): void {
- $carry[] = $placeholderPosition + $fragmentPosition;
- }
- );
- }
-
- /**
- * Returns a map of placeholder positions to their parameter names.
- *
- * @return array
- */
- private static function getNamedPlaceholderPositions(string $statement): array
- {
- return self::collectPlaceholders(
- $statement,
- ':',
- self::NAMED_TOKEN,
- static function (
- string $placeholder,
- int $placeholderPosition,
- int $fragmentPosition,
- array &$carry
- ): void {
- $carry[$placeholderPosition + $fragmentPosition] = substr($placeholder, 1);
- }
- );
- }
-
- /**
- * @return mixed[]
- */
- private static function collectPlaceholders(
- string $statement,
- string $match,
- string $token,
- callable $collector
- ): array {
- if (strpos($statement, $match) === false) {
- return [];
- }
-
- $carry = [];
-
- foreach (self::getUnquotedStatementFragments($statement) as $fragment) {
- preg_match_all('/' . $token . '/', $fragment[0], $matches, PREG_OFFSET_CAPTURE);
- foreach ($matches[0] as $placeholder) {
- $collector($placeholder[0], $placeholder[1], $fragment[1], $carry);
- }
- }
-
- return $carry;
- }
-
- /**
- * For a positional query this method can rewrite the sql statement with regard to array parameters.
- *
- * @param string $query SQL query
- * @param mixed[] $params Query parameters
- * @param array|array $types Parameter types
- *
- * @return mixed[]
- *
- * @throws SQLParserUtilsException
- */
- public static function expandListParameters($query, $params, $types)
- {
- $isPositional = is_int(key($params));
- $arrayPositions = [];
- $bindIndex = -1;
-
- if ($isPositional) {
- // make sure that $types has the same keys as $params
- // to allow omitting parameters with unspecified types
- $types += array_fill_keys(array_keys($params), null);
-
- ksort($params);
- ksort($types);
- }
-
- foreach ($types as $name => $type) {
- ++$bindIndex;
-
- if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) {
- continue;
- }
-
- if ($isPositional) {
- $name = $bindIndex;
- }
-
- $arrayPositions[$name] = false;
- }
-
- if ($isPositional && count($arrayPositions) === 0) {
- return [$query, $params, $types];
- }
-
- if ($isPositional) {
- $paramOffset = 0;
- $queryOffset = 0;
- $params = array_values($params);
- $types = array_values($types);
-
- $paramPos = self::getPositionalPlaceholderPositions($query);
-
- foreach ($paramPos as $needle => $needlePos) {
- if (! isset($arrayPositions[$needle])) {
- continue;
- }
-
- $needle += $paramOffset;
- $needlePos += $queryOffset;
- $count = count($params[$needle]);
-
- $params = array_merge(
- array_slice($params, 0, $needle),
- $params[$needle],
- array_slice($params, $needle + 1)
- );
-
- $types = array_merge(
- array_slice($types, 0, $needle),
- $count > 0 ?
- // array needles are at {@link \Doctrine\DBAL\ParameterType} constants
- // + {@link \Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET}
- array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET) :
- [],
- array_slice($types, $needle + 1)
- );
-
- $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
- $query = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1);
-
- $paramOffset += $count - 1; // Grows larger by number of parameters minus the replaced needle.
- $queryOffset += strlen($expandStr) - 1;
- }
-
- return [$query, $params, $types];
- }
-
- $queryOffset = 0;
- $typesOrd = [];
- $paramsOrd = [];
-
- $paramPos = self::getNamedPlaceholderPositions($query);
-
- foreach ($paramPos as $pos => $paramName) {
- $paramLen = strlen($paramName) + 1;
- $value = static::extractParam($paramName, $params, true);
-
- if (! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) {
- $pos += $queryOffset;
- $queryOffset -= $paramLen - 1;
- $paramsOrd[] = $value;
- $typesOrd[] = static::extractParam($paramName, $types, false, ParameterType::STRING);
- $query = substr($query, 0, $pos) . '?' . substr($query, $pos + $paramLen);
-
- continue;
- }
-
- $count = count($value);
- $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : 'NULL';
-
- foreach ($value as $val) {
- $paramsOrd[] = $val;
- $typesOrd[] = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET;
- }
-
- $pos += $queryOffset;
- $queryOffset += strlen($expandStr) - $paramLen;
- $query = substr($query, 0, $pos) . $expandStr . substr($query, $pos + $paramLen);
- }
-
- return [$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 mixed[][]
- */
- private static function getUnquotedStatementFragments($statement)
- {
- $literal = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' .
- self::ESCAPED_DOUBLE_QUOTED_TEXT . '|' .
- self::ESCAPED_BACKTICK_QUOTED_TEXT . '|' .
- self::ESCAPED_BRACKET_QUOTED_TEXT;
- $expression = sprintf('/((.+(?i:ARRAY)\\[.+\\])|([^\'"`\\[]+))(?:%s)?/s', $literal);
-
- preg_match_all($expression, $statement, $fragments, PREG_OFFSET_CAPTURE);
-
- return $fragments[1];
- }
-
- /**
- * @param string $paramName The name of the parameter (without a colon in front)
- * @param mixed $paramsOrTypes A hash of parameters or types
- * @param bool $isParam
- * @param mixed $defaultValue An optional default value. If omitted, an exception is thrown
- *
- * @return mixed
- *
- * @throws SQLParserUtilsException
- */
- private static function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null)
- {
- if (array_key_exists($paramName, $paramsOrTypes)) {
- return $paramsOrTypes[$paramName];
- }
-
- if ($defaultValue !== null) {
- return $defaultValue;
- }
-
- if ($isParam) {
- throw SQLParserUtilsException::missingParam($paramName);
- }
-
- throw SQLParserUtilsException::missingType($paramName);
- }
-}
diff --git a/src/SQLParserUtilsException.php b/src/SQLParserUtilsException.php
deleted file mode 100644
index 297b0761b8d..00000000000
--- a/src/SQLParserUtilsException.php
+++ /dev/null
@@ -1,37 +0,0 @@
- 42, 0 => 30]], // explicit keys
- ];
- }
-
- /**
- * @return mixed[][]
- */
- public function dataGetPlaceholderNamedPositions(): iterable
- {
- return [
- // none
- ['SELECT * FROM Foo', []],
-
- // named
- ['SELECT :foo FROM :bar', [7 => 'foo', 17 => 'bar']],
- ['SELECT * FROM Foo WHERE bar IN (:name1, :name2)', [32 => 'name1', 40 => 'name2']],
- ['SELECT ":foo" FROM Foo WHERE bar IN (:name1, :name2)', [37 => 'name1', 45 => 'name2']],
- ["SELECT ':foo' FROM Foo WHERE bar IN (:name1, :name2)", [37 => 'name1', 45 => 'name2']],
- ['SELECT :foo_id', [7 => 'foo_id']], // Ticket DBAL-231
- ['SELECT @rank := 1', []], // Ticket DBAL-398
- ['SELECT @rank := 1 AS rank, :foo AS foo FROM :bar', [27 => 'foo', 44 => 'bar']], // Ticket DBAL-398
- // Ticket GH-113
- [
- 'SELECT * FROM Foo WHERE bar > :start_date AND baz > :start_date',
- [
- 30 => 'start_date',
- 52 => 'start_date',
- ],
- ],
-
- // Ticket GH-259
- [
- 'SELECT foo::date as date FROM Foo WHERE bar > :start_date AND baz > :start_date',
- [
- 46 => 'start_date',
- 68 => 'start_date',
- ],
- ],
-
- // Ticket DBAL-552
- [
- 'SELECT `d.ns:col_name` FROM my_table d WHERE `d.date` >= :param1',
- [57 => 'param1'],
- ],
-
- // Ticket DBAL-552
- ['SELECT [d.ns:col_name] FROM my_table d WHERE [d.date] >= :param1', [57 => 'param1']],
- ['SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[:foo])', [56 => 'foo']], // Ticket GH-2295
- ['SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, array[:foo])', [56 => 'foo']],[
- 'SELECT table.column1, ARRAY[\'3\'] FROM schema.table table WHERE table.f1 = :foo AND ARRAY[\'3\']',
- [74 => 'foo'],
- ],
- [
- 'SELECT table.column1, ARRAY[\'3\']::integer[] FROM schema.table table'
- . ' WHERE table.f1 = :foo AND ARRAY[\'3\']::integer[]',
- [85 => 'foo'],
- ],
- [
- 'SELECT table.column1, ARRAY[:foo] FROM schema.table table WHERE table.f1 = :bar AND ARRAY[\'3\']',
- [
- 28 => 'foo',
- 75 => 'bar',
- ],
- ],
- [
- 'SELECT table.column1, ARRAY[:foo]::integer[] FROM schema.table table'
- . ' WHERE table.f1 = :bar AND ARRAY[\'3\']::integer[]',
- [
- 28 => 'foo',
- 86 => 'bar',
- ],
- ],
- [
- <<<'SQLDATA'
-SELECT * FROM foo WHERE
-bar = ':not_a_param1 ''":not_a_param2"'''
-OR bar=:a_param1
-OR bar=:a_param2||':not_a_param3'
-OR bar=':not_a_param4 '':not_a_param5'' :not_a_param6'
-OR bar=''
-OR bar=:a_param3
-SQLDATA
-,
- [
- 74 => 'a_param1',
- 91 => 'a_param2',
- 190 => 'a_param3',
- ],
- ],
- [
- 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
- . " WHERE (data.description LIKE :condition_0 ESCAPE '\\\\')"
- . " AND (data.description LIKE :condition_1 ESCAPE '\\\\') ORDER BY id ASC",
- [
- 121 => 'condition_0',
- 174 => 'condition_1',
- ],
- ],
- [
- 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
- . ' WHERE (data.description LIKE :condition_0 ESCAPE "\\\\")'
- . ' AND (data.description LIKE :condition_1 ESCAPE "\\\\") ORDER BY id ASC',
- [
- 121 => 'condition_0',
- 174 => 'condition_1',
- ],
- ],
- [
- 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
- . ' WHERE (data.description LIKE :condition_0 ESCAPE "\\\\")'
- . ' AND (data.description LIKE :condition_1 ESCAPE \'\\\\\') ORDER BY id ASC',
- [
- 121 => 'condition_0',
- 174 => 'condition_1',
- ],
- ],
- [
- 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
- . ' WHERE (data.description LIKE :condition_0 ESCAPE `\\\\`)'
- . ' AND (data.description LIKE :condition_1 ESCAPE `\\\\`) ORDER BY id ASC',
- [
- 121 => 'condition_0',
- 174 => 'condition_1',
- ],
- ],
- [
- 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
- . ' WHERE (data.description LIKE :condition_0 ESCAPE \'\\\\\')'
- . ' AND (data.description LIKE :condition_1 ESCAPE `\\\\`) ORDER BY id ASC',
- [
- 121 => 'condition_0',
- 174 => 'condition_1',
- ],
- ],
- [
- "SELECT * FROM Foo WHERE (foo.bar LIKE :condition_0 ESCAPE '\')"
- . " AND (foo.baz = :condition_1) AND (foo.bak LIKE :condition_2 ESCAPE '\')",
- [
- 38 => 'condition_0',
- 78 => 'condition_1',
- 110 => 'condition_2',
- ],
- ],
- ];
- }
-
- /**
- * @param int[] $expectedParamPos
- *
- * @dataProvider dataGetPlaceholderPositionalPositions
- */
- public function testGetPositionalPlaceholderPositions(string $query, array $expectedParamPos): void
- {
- $reflection = new ReflectionMethod(SQLParserUtils::class, 'getPositionalPlaceholderPositions');
- $reflection->setAccessible(true);
-
- self::assertEquals($expectedParamPos, $reflection->invoke(null, $query));
- }
-
- /**
- * @param string[] $expectedParamPos
- *
- * @dataProvider dataGetPlaceholderNamedPositions
- */
- public function testGetNamedPlaceholderPositions(string $query, array $expectedParamPos): void
- {
- $reflection = new ReflectionMethod(SQLParserUtils::class, 'getNamedPlaceholderPositions');
- $reflection->setAccessible(true);
-
- self::assertEquals($expectedParamPos, $reflection->invoke(null, $query));
- }
-
/**
* @return mixed[][]
*/
@@ -301,7 +93,7 @@ public static function dataExpandListParameters(): iterable
[2 => ParameterType::STRING, 1 => ParameterType::STRING],
'SELECT * FROM Foo WHERE foo = ? AND bar = ? AND baz = ?',
[1 => 'bar', 0 => 1, 2 => 'baz'],
- [1 => ParameterType::STRING, 2 => ParameterType::STRING, 0 => null],
+ [1 => ParameterType::STRING, 2 => ParameterType::STRING],
],
'Positional: explicit keys for array params and array types' => [
'SELECT * FROM Foo WHERE foo IN (?) AND bar IN (?) AND baz = ?',
@@ -318,27 +110,6 @@ public static function dataExpandListParameters(): iterable
ParameterType::BOOLEAN,
],
],
- 'Positional starts from 1: One non-list before and one after list-needle' => [
- 'SELECT * FROM Foo WHERE foo = ? AND bar IN (?) AND baz = ? AND foo IN (?)',
- [1 => 1, 2 => [1, 2, 3], 3 => 4, 4 => [5, 6]],
- [
- 1 => ParameterType::INTEGER,
- 2 => Connection::PARAM_INT_ARRAY,
- 3 => ParameterType::INTEGER,
- 4 => Connection::PARAM_INT_ARRAY,
- ],
- 'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?) AND baz = ? AND foo IN (?, ?)',
- [1, 1, 2, 3, 4, 5, 6],
- [
- ParameterType::INTEGER,
- ParameterType::INTEGER,
- ParameterType::INTEGER,
- ParameterType::INTEGER,
- ParameterType::INTEGER,
- ParameterType::INTEGER,
- ParameterType::INTEGER,
- ],
- ],
'Named: Very simple with param int' => [
'SELECT * FROM Foo WHERE foo = :foo',
['foo' => 1],
@@ -464,7 +235,11 @@ public static function dataExpandListParameters(): iterable
['foo' => Connection::PARAM_INT_ARRAY, 'baz' => 'string'],
'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ? OR baz = ?',
[1, 2, 'bar', 'baz'],
- [ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::STRING, 'string'],
+ [
+ 0 => ParameterType::INTEGER,
+ 1 => ParameterType::INTEGER,
+ 3 => 'string',
+ ],
],
[
'SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar',
@@ -472,7 +247,7 @@ public static function dataExpandListParameters(): iterable
['foo' => Connection::PARAM_INT_ARRAY],
'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ?',
[1, 2, 'bar'],
- [ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::STRING],
+ [ParameterType::INTEGER, ParameterType::INTEGER],
],
'Named parameters and partially implicit types' => [
'SELECT * FROM Foo WHERE foo = :foo OR bar = :bar',
@@ -480,7 +255,7 @@ public static function dataExpandListParameters(): iterable
['foo' => ParameterType::INTEGER],
'SELECT * FROM Foo WHERE foo = ? OR bar = ?',
['foo', 'bar'],
- [ParameterType::INTEGER, ParameterType::STRING],
+ [ParameterType::INTEGER],
],
'Named parameters and explicit types' => [
'SELECT * FROM Foo WHERE foo = :foo OR bar = :bar',
@@ -520,16 +295,16 @@ public static function dataExpandListParameters(): iterable
[1 => Connection::PARAM_STR_ARRAY],
'SELECT NULL FROM dummy WHERE ? IN (?, ?)',
['foo', 'bar', 'baz'],
- [null, ParameterType::STRING, ParameterType::STRING],
+ [1 => ParameterType::STRING, ParameterType::STRING],
],
];
}
/**
- * @param mixed[] $params
- * @param mixed[] $types
- * @param mixed[] $expectedParams
- * @param mixed[] $expectedTypes
+ * @param array|array $params
+ * @param array|array $types
+ * @param list $expectedParams
+ * @param array $expectedTypes
*
* @dataProvider dataExpandListParameters
*/
@@ -541,7 +316,7 @@ public function testExpandListParameters(
array $expectedParams,
array $expectedTypes
): void {
- [$query, $params, $types] = SQLParserUtils::expandListParameters($query, $params, $types);
+ [$query, $params, $types] = $this->expandArrayParameters($query, $params, $types);
self::assertEquals($expectedQuery, $query, 'Query was not rewritten correctly.');
self::assertEquals($expectedParams, $params, 'Params dont match');
@@ -551,7 +326,7 @@ public function testExpandListParameters(
/**
* @return mixed[][]
*/
- public static function dataQueryWithMissingParameters(): iterable
+ public static function missingNamedParameterProvider(): iterable
{
return [
[
@@ -564,16 +339,6 @@ public static function dataQueryWithMissingParameters(): iterable
[],
[],
],
- [
- 'SELECT * FROM foo WHERE bar = :param',
- [],
- ['param' => Connection::PARAM_INT_ARRAY],
- ],
- [
- 'SELECT * FROM foo WHERE bar = :param',
- [],
- [':param' => Connection::PARAM_INT_ARRAY],
- ],
[
'SELECT * FROM foo WHERE bar = :param',
[],
@@ -588,16 +353,64 @@ public static function dataQueryWithMissingParameters(): iterable
}
/**
- * @param mixed[] $params
- * @param mixed[] $types
+ * @param array|array $params
+ * @param array|array $types
+ *
+ * @dataProvider missingNamedParameterProvider
+ */
+ public function testMissingNamedParameter(string $query, array $params, array $types = []): void
+ {
+ $this->expectException(MissingNamedParameter::class);
+
+ $this->expandArrayParameters($query, $params, $types);
+ }
+
+ /**
+ * @param array|array $params
*
- * @dataProvider dataQueryWithMissingParameters
+ * @dataProvider missingPositionalParameterProvider
*/
- public function testExceptionIsThrownForMissingParam(string $query, array $params, array $types = []): void
+ public function testMissingPositionalParameter(string $query, array $params): void
{
- $this->expectException(SQLParserUtilsException::class);
- $this->expectExceptionMessage('Value for :param not found in params array. Params array key should be "param"');
+ $this->expectException(MissingPositionalParameter::class);
- SQLParserUtils::expandListParameters($query, $params, $types);
+ $this->expandArrayParameters($query, $params, []);
+ }
+
+ /**
+ * @return mixed[][]
+ */
+ public static function missingPositionalParameterProvider(): iterable
+ {
+ return [
+ 'No parameters' => [
+ 'SELECT * FROM foo WHERE bar = ?',
+ [],
+ ],
+ 'Too few parameters' => [
+ 'SELECT * FROM foo WHERE bar = ? AND baz = ?',
+ [1],
+ ],
+ ];
+ }
+
+ /**
+ * @param array|array $params
+ * @param array|array $types
+ *
+ * @return array{0: string, 1: list, 2: array}
+ */
+ private function expandArrayParameters(string $sql, array $params, array $types): array
+ {
+ $parser = new Parser(true);
+ $visitor = new ExpandArrayParameters($params, $types);
+
+ $parser->parse($sql, $visitor);
+
+ return [
+ $visitor->getSQL(),
+ $visitor->getParameters(),
+ $visitor->getTypes(),
+ ];
}
}
diff --git a/tests/Driver/OCI8/ConvertPositionalToNamedPlaceholdersTest.php b/tests/Driver/OCI8/ConvertPositionalToNamedPlaceholdersTest.php
index 17046373a7e..d3779a6419a 100644
--- a/tests/Driver/OCI8/ConvertPositionalToNamedPlaceholdersTest.php
+++ b/tests/Driver/OCI8/ConvertPositionalToNamedPlaceholdersTest.php
@@ -5,19 +5,11 @@
namespace Doctrine\Tests\DBAL\Driver\OCI8;
use Doctrine\DBAL\Driver\OCI8\ConvertPositionalToNamedPlaceholders;
-use Doctrine\DBAL\Driver\OCI8\Exception\NonTerminatedStringLiteral;
+use Doctrine\DBAL\SQL\Parser;
use PHPUnit\Framework\TestCase;
class ConvertPositionalToNamedPlaceholdersTest extends TestCase
{
- /** @var ConvertPositionalToNamedPlaceholders */
- private $convertPositionalToNamedPlaceholders;
-
- protected function setUp(): void
- {
- $this->convertPositionalToNamedPlaceholders = new ConvertPositionalToNamedPlaceholders();
- }
-
/**
* @param mixed[] $expectedOutputParamsMap
*
@@ -28,10 +20,13 @@ public function testConvertPositionalToNamedParameters(
string $expectedOutputSQL,
array $expectedOutputParamsMap
): void {
- [$statement, $params] = ($this->convertPositionalToNamedPlaceholders)($inputSQL);
+ $parser = new Parser(false);
+ $visitor = new ConvertPositionalToNamedPlaceholders();
+
+ $parser->parse($inputSQL, $visitor);
- self::assertEquals($expectedOutputSQL, $statement);
- self::assertEquals($expectedOutputParamsMap, $params);
+ self::assertEquals($expectedOutputSQL, $visitor->getSQL());
+ self::assertEquals($expectedOutputParamsMap, $visitor->getParameterMap());
}
/**
@@ -92,35 +87,4 @@ public static function positionalToNamedPlaceholdersProvider(): iterable
],
];
}
-
- /**
- * @dataProvider nonTerminatedLiteralProvider
- */
- public function testConvertNonTerminatedLiteral(string $sql, string $expectedExceptionMessageRegExp): void
- {
- $this->expectException(NonTerminatedStringLiteral::class);
- $this->expectExceptionMessageMatches($expectedExceptionMessageRegExp);
- ($this->convertPositionalToNamedPlaceholders)($sql);
- }
-
- /**
- * @return array>
- */
- public static function nonTerminatedLiteralProvider(): iterable
- {
- return [
- 'no-matching-quote' => [
- "SELECT 'literal FROM DUAL",
- '/offset 7./',
- ],
- 'no-matching-double-quote' => [
- 'SELECT 1 "COL1 FROM DUAL',
- '/offset 9./',
- ],
- 'incorrect-escaping-syntax' => [
- "SELECT 'quoted \\'string' FROM DUAL",
- '/offset 23./',
- ],
- ];
- }
}
diff --git a/tests/Functional/DataAccessTest.php b/tests/Functional/DataAccessTest.php
index db109d56513..4e942b792ce 100644
--- a/tests/Functional/DataAccessTest.php
+++ b/tests/Functional/DataAccessTest.php
@@ -271,8 +271,8 @@ public function testExecuteQueryBindDateTimeType(): void
{
$value = $this->connection->fetchOne(
'SELECT count(*) AS c FROM fetch_table WHERE test_datetime = ?',
- [1 => new DateTime('2010-01-01 10:10:10')],
- [1 => Types::DATETIME_MUTABLE]
+ [new DateTime('2010-01-01 10:10:10')],
+ [Types::DATETIME_MUTABLE]
);
self::assertEquals(1, $value);
@@ -284,20 +284,20 @@ public function testExecuteStatementBindDateTimeType(): void
$sql = 'INSERT INTO fetch_table (test_int, test_string, test_datetime) VALUES (?, ?, ?)';
$affectedRows = $this->connection->executeStatement($sql, [
- 1 => 50,
- 2 => 'foo',
- 3 => $datetime,
+ 50,
+ 'foo',
+ $datetime,
], [
- 1 => ParameterType::INTEGER,
- 2 => ParameterType::STRING,
- 3 => Types::DATETIME_MUTABLE,
+ ParameterType::INTEGER,
+ ParameterType::STRING,
+ Types::DATETIME_MUTABLE,
]);
self::assertEquals(1, $affectedRows);
self::assertEquals(1, $this->connection->executeQuery(
'SELECT count(*) AS c FROM fetch_table WHERE test_datetime = ?',
- [1 => $datetime],
- [1 => Types::DATETIME_MUTABLE]
+ [$datetime],
+ [Types::DATETIME_MUTABLE]
)->fetchOne());
}
diff --git a/tests/SQL/ParserTest.php b/tests/SQL/ParserTest.php
new file mode 100644
index 00000000000..d7ca6930bc5
--- /dev/null
+++ b/tests/SQL/ParserTest.php
@@ -0,0 +1,463 @@
+ */
+ private $result = [];
+
+ /**
+ * @dataProvider statementsWithParametersProvider
+ */
+ public function testStatementsWithParameters(bool $mySQLStringEscaping, string $sql, string $expected): void
+ {
+ $parser = new Parser($mySQLStringEscaping);
+ $parser->parse($sql, $this);
+
+ $this->assertParsed($expected);
+ }
+
+ /**
+ * @return iterable>
+ */
+ public static function statementsWithParametersProvider(): iterable
+ {
+ foreach (self::getModes() as $mode => $mySQLStringEscaping) {
+ foreach (self::getStatementsWithParameters() as $item => $arguments) {
+ yield sprintf('%s: %s', $mode, $item) => array_merge([$mySQLStringEscaping], $arguments);
+ }
+ }
+ }
+
+ /**
+ * @return iterable>
+ */
+ private static function getStatementsWithParameters(): iterable
+ {
+ yield [
+ 'SELECT ?',
+ 'SELECT {?}',
+ ];
+
+ yield [
+ 'SELECT * FROM Foo WHERE bar IN (?, ?, ?)',
+ 'SELECT * FROM Foo WHERE bar IN ({?}, {?}, {?})',
+ ];
+
+ yield [
+ 'SELECT ? FROM ?',
+ 'SELECT {?} FROM {?}',
+ ];
+
+ yield [
+ 'SELECT "?" FROM foo WHERE bar = ?',
+ 'SELECT "?" FROM foo WHERE bar = {?}',
+ ];
+
+ yield [
+ "SELECT '?' FROM foo WHERE bar = ?",
+ "SELECT '?' FROM foo WHERE bar = {?}",
+ ];
+
+ yield [
+ 'SELECT `?` FROM foo WHERE bar = ?',
+ 'SELECT `?` FROM foo WHERE bar = {?}',
+ ];
+
+ yield [
+ 'SELECT [?] FROM foo WHERE bar = ?',
+ 'SELECT [?] FROM foo WHERE bar = {?}',
+ ];
+
+ yield [
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[?])',
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[{?}])',
+ ];
+
+ yield [
+ "SELECT 'Doctrine\DBAL?' FROM foo WHERE bar = ?",
+ "SELECT 'Doctrine\DBAL?' FROM foo WHERE bar = {?}",
+ ];
+
+ yield [
+ 'SELECT "Doctrine\DBAL?" FROM foo WHERE bar = ?',
+ 'SELECT "Doctrine\DBAL?" FROM foo WHERE bar = {?}',
+ ];
+
+ yield [
+ 'SELECT `Doctrine\DBAL?` FROM foo WHERE bar = ?',
+ 'SELECT `Doctrine\DBAL?` FROM foo WHERE bar = {?}',
+ ];
+
+ yield [
+ 'SELECT [Doctrine\DBAL?] FROM foo WHERE bar = ?',
+ 'SELECT [Doctrine\DBAL?] FROM foo WHERE bar = {?}',
+ ];
+
+ yield [
+ 'SELECT :foo FROM :bar',
+ 'SELECT {:foo} FROM {:bar}',
+ ];
+
+ yield [
+ 'SELECT * FROM Foo WHERE bar IN (:name1, :name2)',
+ 'SELECT * FROM Foo WHERE bar IN ({:name1}, {:name2})',
+ ];
+
+ yield [
+ 'SELECT ":foo" FROM Foo WHERE bar IN (:name1, :name2)',
+ 'SELECT ":foo" FROM Foo WHERE bar IN ({:name1}, {:name2})',
+ ];
+
+ yield [
+ "SELECT ':foo' FROM Foo WHERE bar IN (:name1, :name2)",
+ "SELECT ':foo' FROM Foo WHERE bar IN ({:name1}, {:name2})",
+ ];
+
+ yield [
+ 'SELECT :foo_id',
+ 'SELECT {:foo_id}',
+ ];
+
+ yield [
+ 'SELECT @rank := 1 AS rank, :foo AS foo FROM :bar',
+ 'SELECT @rank := 1 AS rank, {:foo} AS foo FROM {:bar}',
+ ];
+
+ yield [
+ 'SELECT * FROM Foo WHERE bar > :start_date AND baz > :start_date',
+ 'SELECT * FROM Foo WHERE bar > {:start_date} AND baz > {:start_date}',
+ ];
+
+ yield [
+ 'SELECT foo::date as date FROM Foo WHERE bar > :start_date AND baz > :start_date',
+ 'SELECT foo::date as date FROM Foo WHERE bar > {:start_date} AND baz > {:start_date}',
+ ];
+
+ yield [
+ 'SELECT `d.ns:col_name` FROM my_table d WHERE `d.date` >= :param1',
+ 'SELECT `d.ns:col_name` FROM my_table d WHERE `d.date` >= {:param1}',
+ ];
+
+ yield [
+ 'SELECT [d.ns:col_name] FROM my_table d WHERE [d.date] >= :param1',
+ 'SELECT [d.ns:col_name] FROM my_table d WHERE [d.date] >= {:param1}',
+ ];
+
+ yield [
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[:foo])',
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[{:foo}])',
+ ];
+
+ yield [
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, array[:foo])',
+ 'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, array[{:foo}])',
+ ];
+
+ yield [
+ "SELECT table.column1, ARRAY['3'] FROM schema.table table WHERE table.f1 = :foo AND ARRAY['3']",
+ "SELECT table.column1, ARRAY['3'] FROM schema.table table WHERE table.f1 = {:foo} AND ARRAY['3']",
+ ];
+
+ yield [
+ "SELECT table.column1, ARRAY['3']::integer[] FROM schema.table table"
+ . " WHERE table.f1 = :foo AND ARRAY['3']::integer[]",
+ "SELECT table.column1, ARRAY['3']::integer[] FROM schema.table table"
+ . " WHERE table.f1 = {:foo} AND ARRAY['3']::integer[]",
+ ];
+
+ yield [
+ "SELECT table.column1, ARRAY[:foo] FROM schema.table table WHERE table.f1 = :bar AND ARRAY['3']",
+ "SELECT table.column1, ARRAY[{:foo}] FROM schema.table table WHERE table.f1 = {:bar} AND ARRAY['3']",
+ ];
+
+ yield [
+ 'SELECT table.column1, ARRAY[:foo]::integer[] FROM schema.table table'
+ . " WHERE table.f1 = :bar AND ARRAY['3']::integer[]",
+ 'SELECT table.column1, ARRAY[{:foo}]::integer[] FROM schema.table table'
+ . " WHERE table.f1 = {:bar} AND ARRAY['3']::integer[]",
+ ];
+
+ yield 'Quotes inside literals escaped by doubling' => [
+ <<<'SQL'
+SELECT * FROM foo
+WHERE bar = ':not_a_param1 ''":not_a_param2"'''
+ OR bar=:a_param1
+ OR bar=:a_param2||':not_a_param3'
+ OR bar=':not_a_param4 '':not_a_param5'' :not_a_param6'
+ OR bar=''
+ OR bar=:a_param3
+SQL
+,
+ <<<'SQL'
+SELECT * FROM foo
+WHERE bar = ':not_a_param1 ''":not_a_param2"'''
+ OR bar={:a_param1}
+ OR bar={:a_param2}||':not_a_param3'
+ OR bar=':not_a_param4 '':not_a_param5'' :not_a_param6'
+ OR bar=''
+ OR bar={:a_param3}
+SQL
+,
+ ];
+
+ yield [
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
+ . " WHERE (data.description LIKE :condition_0 ESCAPE '\\\\')"
+ . " AND (data.description LIKE :condition_1 ESCAPE '\\\\') ORDER BY id ASC",
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
+ . " WHERE (data.description LIKE {:condition_0} ESCAPE '\\\\')"
+ . " AND (data.description LIKE {:condition_1} ESCAPE '\\\\') ORDER BY id ASC",
+ ];
+
+ yield [
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
+ . ' WHERE (data.description LIKE :condition_0 ESCAPE "\\\\")'
+ . ' AND (data.description LIKE :condition_1 ESCAPE "\\\\") ORDER BY id ASC',
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
+ . ' WHERE (data.description LIKE {:condition_0} ESCAPE "\\\\")'
+ . ' AND (data.description LIKE {:condition_1} ESCAPE "\\\\") ORDER BY id ASC',
+ ];
+
+ yield 'Combined single and double quotes' => [
+ <<<'SQL'
+SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id
+ FROM test_data data
+ WHERE (data.description LIKE :condition_0 ESCAPE "\\")
+ AND (data.description LIKE :condition_1 ESCAPE '\\') ORDER BY id ASC
+SQL
+,
+ <<<'SQL'
+SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id
+ FROM test_data data
+ WHERE (data.description LIKE {:condition_0} ESCAPE "\\")
+ AND (data.description LIKE {:condition_1} ESCAPE '\\') ORDER BY id ASC
+SQL
+,
+ ];
+
+ yield [
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
+ . ' WHERE (data.description LIKE :condition_0 ESCAPE `\\\\`)'
+ . ' AND (data.description LIKE :condition_1 ESCAPE `\\\\`) ORDER BY id ASC',
+ 'SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data'
+ . ' WHERE (data.description LIKE {:condition_0} ESCAPE `\\\\`)'
+ . ' AND (data.description LIKE {:condition_1} ESCAPE `\\\\`) ORDER BY id ASC',
+ ];
+
+ yield 'Combined single quotes and backticks' => [
+ <<<'SQL'
+SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id
+ FROM test_data data
+ WHERE (data.description LIKE :condition_0 ESCAPE '\\')
+ AND (data.description LIKE :condition_1 ESCAPE `\\`) ORDER BY id ASC
+SQL
+,
+ <<<'SQL'
+SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id
+ FROM test_data data
+ WHERE (data.description LIKE {:condition_0} ESCAPE '\\')
+ AND (data.description LIKE {:condition_1} ESCAPE `\\`) ORDER BY id ASC
+SQL
+,
+ ];
+
+ yield 'Placeholders inside comments' => [
+ <<<'SQL'
+/*
+ * test placeholder ?
+ */
+SELECT dummy as "dummy?"
+ FROM DUAL
+ WHERE '?' = '?'
+-- AND dummy <> ?
+ AND dummy = ?
+SQL
+,
+ <<<'SQL'
+/*
+ * test placeholder ?
+ */
+SELECT dummy as "dummy?"
+ FROM DUAL
+ WHERE '?' = '?'
+-- AND dummy <> ?
+ AND dummy = {?}
+SQL
+,
+ ];
+ }
+
+ /**
+ * @dataProvider statementsWithoutParametersProvider
+ */
+ public function testStatementsWithoutParameters(bool $mySQLStringEscaping, string $sql): void
+ {
+ $parser = new Parser($mySQLStringEscaping);
+ $parser->parse($sql, $this);
+
+ $this->assertParsed($sql);
+ }
+
+ /**
+ * @return iterable>
+ */
+ public static function statementsWithoutParametersProvider(): iterable
+ {
+ foreach (self::getModes() as $mode => $mySQLStringEscaping) {
+ foreach (self::getStatementsWithoutParameters() as $sql) {
+ yield sprintf('%s: %s', $mode, $sql) => [$mySQLStringEscaping, $sql];
+ }
+ }
+ }
+
+ /**
+ * @return iterable
+ */
+ private static function getStatementsWithoutParameters(): iterable
+ {
+ yield 'SELECT * FROM Foo';
+ yield "SELECT '?' FROM foo";
+ yield 'SELECT "?" FROM foo';
+ yield 'SELECT `?` FROM foo';
+ yield 'SELECT [?] FROM foo';
+ yield "SELECT 'Doctrine\DBAL?' FROM foo";
+ yield 'SELECT "Doctrine\DBAL?" FROM foo';
+ yield 'SELECT `Doctrine\DBAL?` FROM foo';
+ yield 'SELECT [Doctrine\DBAL?] FROM foo';
+ yield 'SELECT @rank := 1';
+ }
+
+ /**
+ * @dataProvider ansiParametersProvider
+ */
+ public function testAnsiEscaping(string $sql, string $expected): void
+ {
+ $parser = new Parser(false);
+ $parser->parse($sql, $this);
+
+ $this->assertParsed($expected);
+ }
+
+ /**
+ * @return iterable>
+ */
+ public static function ansiParametersProvider(): iterable
+ {
+ yield 'Quotes inside literals escaped by doubling' => [
+ <<<'SQL'
+SELECT * FROM FOO WHERE bar = 'it''s a trap? \' OR bar = ?
+AND baz = """quote"" me on it? \" OR baz = ?
+SQL
+,
+ <<<'SQL'
+SELECT * FROM FOO WHERE bar = 'it''s a trap? \' OR bar = {?}
+AND baz = """quote"" me on it? \" OR baz = {?}
+SQL
+,
+ ];
+
+ yield 'Backslash inside literals does not need escaping' => [
+ <<<'SQL'
+SELECT * FROM Foo
+WHERE (foo.bar LIKE :condition_0 ESCAPE '\')
+ AND (foo.baz = :condition_1)
+ AND (foo.bak LIKE :condition_2 ESCAPE '\')
+SQL,
+ <<<'SQL'
+SELECT * FROM Foo
+WHERE (foo.bar LIKE {:condition_0} ESCAPE '\')
+ AND (foo.baz = {:condition_1})
+ AND (foo.bak LIKE {:condition_2} ESCAPE '\')
+SQL,
+ ];
+ }
+
+ /**
+ * @dataProvider mySQLParametersProvider
+ */
+ public function testMySQLEscaping(string $sql, string $expected): void
+ {
+ $parser = new Parser(true);
+ $parser->parse($sql, $this);
+
+ $this->assertParsed($expected);
+ }
+
+ /**
+ * @return iterable>
+ */
+ public static function mySQLParametersProvider(): iterable
+ {
+ yield 'Quotes inside literals escaped by backslash' => [
+ <<<'SQL'
+SELECT * FROM FOO
+ WHERE bar = 'it\'s a trap? \\' OR bar = ?
+ AND baz = "\"quote\" me on it? \\" OR baz = ?
+SQL
+,
+ <<<'SQL'
+SELECT * FROM FOO
+ WHERE bar = 'it\'s a trap? \\' OR bar = {?}
+ AND baz = "\"quote\" me on it? \\" OR baz = {?}
+SQL
+,
+ ];
+
+ yield 'Backslash inside literals needs escaping' => [
+ <<<'SQL'
+SELECT * FROM Foo
+WHERE (foo.bar LIKE :condition_0 ESCAPE '\\')
+ AND (foo.baz = :condition_1)
+ AND (foo.bak LIKE :condition_2 ESCAPE '\\')
+SQL
+,
+ <<<'SQL'
+SELECT * FROM Foo
+WHERE (foo.bar LIKE {:condition_0} ESCAPE '\\')
+ AND (foo.baz = {:condition_1})
+ AND (foo.bak LIKE {:condition_2} ESCAPE '\\')
+SQL
+,
+ ];
+ }
+
+ public function acceptPositionalParameter(string $sql): void
+ {
+ $this->result[] = sprintf('{%s}', $sql);
+ }
+
+ public function acceptNamedParameter(string $sql): void
+ {
+ $this->result[] = sprintf('{%s}', $sql);
+ }
+
+ public function acceptOther(string $sql): void
+ {
+ $this->result[] = $sql;
+ }
+
+ /**
+ * @return iterable
+ */
+ private static function getModes(): iterable
+ {
+ yield 'ANSI' => false;
+
+ yield 'MySQL' => true;
+ }
+
+ private function assertParsed(string $expected): void
+ {
+ self::assertSame($expected, implode('', $this->result));
+ }
+}