Skip to content

Commit

Permalink
Merge branch '4.3.x' into 5.0.x
Browse files Browse the repository at this point in the history
  • Loading branch information
morozov committed Nov 13, 2024
2 parents a56e4c1 + 43d8490 commit 6456ffd
Show file tree
Hide file tree
Showing 15 changed files with 628 additions and 4 deletions.
22 changes: 22 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@ all drivers and middleware.

# Upgrade to 4.3

## Deprecated relying on the current implementation of the database object name parser

The current object name parser implicitly quotes identifiers in the following cases:

1. If the object name is a reserved keyword (e.g., `select`).
2. If an unquoted identifier is preceded by a quoted identifier (e.g., `"inventory".product`).

As a result, the original case of such identifiers is preserved on platforms that respect the SQL-92 standard (i.e.,
identifiers are not upper-cased on Oracle and IBM DB2, and not lower-cased on PostgreSQL). This behavior is deprecated.

If preserving the original case of an identifier is required, please explicitly quote it (e.g., `select``"select"`).

Additionally, the current parser exhibits the following defects:

1. It ignores a missing closing quote in a quoted identifier (e.g., `"inventory`).
2. It allows names with more than two identifiers (e.g., `warehouse.inventory.product`) but only uses the first two,
ignoring the remaining ones.
3. If a quoted identifier contains a dot, it incorrectly treats the part before the dot as a qualifier, despite the
identifier being quoted.

Relying on the above behaviors is deprecated.

## Deprecated `AbstractPlatform::quoteIdentifier()` and `Connection::quoteIdentifier()`

The `AbstractPlatform::quoteIdentifier()` and `Connection::quoteIdentifier()` methods have been deprecated.
Expand Down
5 changes: 5 additions & 0 deletions src/Platforms/AbstractMySQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -868,4 +868,9 @@ public function fetchTableOptionsByTable(bool $includeTableName): string

return $sql . ' WHERE ' . implode(' AND ', $conditions);
}

public function normalizeUnquotedIdentifier(string $identifier): string
{
return $identifier;
}
}
12 changes: 12 additions & 0 deletions src/Platforms/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -2247,6 +2247,18 @@ public function getUnionDistinctSQL(): string
return 'UNION';
}

/**
* Changes the case of unquoted identifier in the same way as the given platform would change it if it was specified
* in an SQL statement.
*
* Even though the default behavior is not the most common across supported platforms, it is part of the SQL92
* standard.
*/
public function normalizeUnquotedIdentifier(string $identifier): string
{
return strtoupper($identifier);
}

/**
* Creates the schema manager that can be used to inspect and change the underlying
* database schema according to the dialect of the platform.
Expand Down
5 changes: 5 additions & 0 deletions src/Platforms/PostgreSQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -773,4 +773,9 @@ public function createSchemaManager(Connection $connection): PostgreSQLSchemaMan
{
return new PostgreSQLSchemaManager($connection, $this);
}

public function normalizeUnquotedIdentifier(string $identifier): string
{
return strtolower($identifier);
}
}
5 changes: 5 additions & 0 deletions src/Platforms/SQLServerPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -1241,4 +1241,9 @@ public function createSchemaManager(Connection $connection): SQLServerSchemaMana
{
return new SQLServerSchemaManager($connection, $this);
}

public function normalizeUnquotedIdentifier(string $identifier): string
{
return $identifier;
}
}
5 changes: 5 additions & 0 deletions src/Platforms/SQLitePlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -973,4 +973,9 @@ public function getUnionSelectPartSQL(string $subQuery): string
{
return $subQuery;
}

public function normalizeUnquotedIdentifier(string $identifier): string
{
return $identifier;
}
}
130 changes: 126 additions & 4 deletions src/Schema/AbstractAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
namespace Doctrine\DBAL\Schema;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Name\Parser;
use Doctrine\DBAL\Schema\Name\Parser\Identifier;
use Doctrine\Deprecations\Deprecation;

use function array_map;
use function count;
use function crc32;
use function dechex;
use function explode;
use function implode;
use function sprintf;
use function str_contains;
use function str_replace;
use function strtolower;
Expand All @@ -35,11 +40,18 @@ abstract class AbstractAsset

protected bool $_quoted = false;

/** @var list<Identifier> */
private array $identifiers = [];

private bool $validateFuture = false;

/**
* Sets the name of this asset.
*/
protected function _setName(string $name): void
{
$input = $name;

if ($this->isIdentifierQuoted($name)) {
$this->_quoted = true;
$name = $this->trimQuotes($name);
Expand All @@ -52,6 +64,81 @@ protected function _setName(string $name): void
}

$this->_name = $name;

$this->validateFuture = false;

if ($input !== '') {
$parser = new Parser();

try {
$identifiers = $parser->parse($input);
} catch (Parser\Exception $e) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6592',
'Unable to parse object name: %s.',
$e->getMessage(),
);

return;
}
} else {
$identifiers = [];
}

switch (count($identifiers)) {
case 0:
$this->identifiers = [];

return;
case 1:
$namespace = null;
$name = $identifiers[0];
break;

case 2:
/** @psalm-suppress PossiblyUndefinedArrayOffset */
[$namespace, $name] = $identifiers;
break;

default:
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6592',
'An object name may consist of at most 2 identifiers (<namespace>.<name>), %d given.',
count($identifiers),
);

return;
}

$this->identifiers = $identifiers;
$this->validateFuture = true;

$futureName = $name->getValue();
$futureNamespace = $namespace?->getValue();

if ($this->_name !== $futureName) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6592',
'Instead of "%s", this name will be interpreted as "%s" in 5.0',
$this->_name,
$futureName,
);
}

if ($this->_namespace === $futureNamespace) {
return;
}

Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6592',
'Instead of %s, the namespace in this name will be interpreted as %s in 5.0.',
$this->_namespace !== null ? sprintf('"%s"', $this->_namespace) : 'null',
$futureNamespace !== null ? sprintf('"%s"', $futureNamespace) : 'null',
);
}

/**
Expand Down Expand Up @@ -129,12 +216,47 @@ public function getName(): string
public function getQuotedName(AbstractPlatform $platform): string
{
$keywords = $platform->getReservedKeywordsList();
$parts = explode('.', $this->getName());
foreach ($parts as $k => $v) {
$parts[$k] = $this->_quoted || $keywords->isKeyword($v) ? $platform->quoteSingleIdentifier($v) : $v;
$parts = $normalizedParts = [];

foreach (explode('.', $this->getName()) as $identifier) {
$isQuoted = $this->_quoted || $keywords->isKeyword($identifier);

if (! $isQuoted) {
$parts[] = $identifier;
$normalizedParts[] = $platform->normalizeUnquotedIdentifier($identifier);
} else {
$parts[] = $platform->quoteSingleIdentifier($identifier);
$normalizedParts[] = $identifier;
}
}

$name = implode('.', $parts);

if ($this->validateFuture) {
$futureParts = array_map(static function (Identifier $identifier) use ($platform): string {
$value = $identifier->getValue();

if (! $identifier->isQuoted()) {
$value = $platform->normalizeUnquotedIdentifier($value);
}

return $value;
}, $this->identifiers);

if ($normalizedParts !== $futureParts) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6592',
'Relying on implicitly quoted identifiers preserving their original case is deprecated. '
. 'The current name %s will become %s in 5.0. '
. 'Please quote the name if the case needs to be preserved.',
$name,
implode('.', array_map([$platform, 'quoteSingleIdentifier'], $futureParts)),
);
}
}

return implode('.', $parts);
return $name;
}

/**
Expand Down
103 changes: 103 additions & 0 deletions src/Schema/Name/Parser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Schema\Name;

use Doctrine\DBAL\Schema\Name\Parser\Exception;
use Doctrine\DBAL\Schema\Name\Parser\Exception\ExpectedDot;
use Doctrine\DBAL\Schema\Name\Parser\Exception\ExpectedNextIdentifier;
use Doctrine\DBAL\Schema\Name\Parser\Exception\UnableToParseIdentifier;
use Doctrine\DBAL\Schema\Name\Parser\Identifier;

use function assert;
use function preg_match;
use function str_replace;
use function strlen;

/**
* Parses a qualified or unqualified SQL-like name.
*
* A name can be either unqualified or qualified:
* - An unqualified name consists of a single identifier.
* - A qualified name is a sequence of two or more identifiers separated by dots.
*
* An identifier can be quoted or unquoted:
* - A quoted identifier is enclosed in double quotes ("), backticks (`), or square brackets ([]).
* The closing quote character can be escaped by doubling it.
* - An unquoted identifier may contain any character except whitespace, dots, or any of the quote characters.
*
* Differences from SQL:
* 1. Identifiers that are reserved keywords or start with a digit do not need to be quoted.
* 2. Whitespace is not allowed between identifiers.
*
* @internal
*/
final class Parser
{
private const IDENTIFIER_PATTERN = <<<'PATTERN'
/\G
(?:
"(?<ansi>[^"]*(?:""[^"]*)*)" # ANSI SQL double-quoted
| `(?<mysql>[^`]*(?:``[^`]*)*)` # MySQL-style backtick-quoted
| \[(?<sqlserver>[^]]*(?:]][^]]*)*)] # SQL Server-style square-bracket-quoted
| (?<unquoted>[^\s."`\[\]]+) # Unquoted
)
/x
PATTERN;

/**
* @return list<Identifier>
*
* @throws Exception
*/
public function parse(string $input): array
{
if ($input === '') {
return [];
}

$offset = 0;
$identifiers = [];
$length = strlen($input);

while (true) {
if ($offset >= $length) {
throw ExpectedNextIdentifier::new();
}

if (preg_match(self::IDENTIFIER_PATTERN, $input, $matches, 0, $offset) === 0) {
throw UnableToParseIdentifier::new($offset);
}

if (isset($matches['ansi']) && strlen($matches['ansi']) > 0) {
$identifier = Identifier::quoted(str_replace('""', '"', $matches['ansi']));
} elseif (isset($matches['mysql']) && strlen($matches['mysql']) > 0) {
$identifier = Identifier::quoted(str_replace('``', '`', $matches['mysql']));
} elseif (isset($matches['sqlserver']) && strlen($matches['sqlserver']) > 0) {
$identifier = Identifier::quoted(str_replace(']]', ']', $matches['sqlserver']));
} else {
assert(isset($matches['unquoted']) && strlen($matches['unquoted']) > 0);
$identifier = Identifier::unquoted($matches['unquoted']);
}

$identifiers[] = $identifier;

$offset += strlen($matches[0]);

if ($offset >= $length) {
break;
}

$character = $input[$offset];

if ($character !== '.') {
throw ExpectedDot::new($offset, $character);
}

$offset++;
}

return $identifiers;
}
}
11 changes: 11 additions & 0 deletions src/Schema/Name/Parser/Exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Schema\Name\Parser;

use Throwable;

interface Exception extends Throwable
{
}
19 changes: 19 additions & 0 deletions src/Schema/Name/Parser/Exception/ExpectedDot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Schema\Name\Parser\Exception;

use Doctrine\DBAL\Schema\Name\Parser\Exception;
use LogicException;

use function sprintf;

/** @internal */
class ExpectedDot extends LogicException implements Exception
{
public static function new(int $position, string $got): self
{
return new self(sprintf('Expected dot at position %d, got "%s".', $position, $got));
}
}
Loading

0 comments on commit 6456ffd

Please sign in to comment.