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 Dec 30, 2024
2 parents 3363654 + 69d5e34 commit 7af9a12
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 60 deletions.
16 changes: 16 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ all drivers and middleware.

# Upgrade to 4.3

## Deprecated `AbstractAsset::isIdentifierQuoted()`

The `AbstractAsset::isIdentifierQuoted()` method has been deprecated. Parse the name and introspect its identifiers
individually using `Identifier::isQuoted()` instead.

## Deprecated mixing unqualified and qualified names in a schema without a default namespace

If a schema lacks a default namespace configuration and has at least one object with an unqualified name, adding or
referencing objects with qualified names is deprecated.

If a schema lacks a default namespace configuration and has at least one object with a qualified name, adding or
referencing objects with unqualified names is deprecated.

Mixing unqualified and qualified names is permitted as long as the schema is configured to use a default namespace. In
this case, the default namespace will be used to resolve unqualified names.

## Deprecated `AbstractAsset::getQuotedName()`

The `AbstractAsset::getQuotedName()` method has been deprecated. Use `NamedObject::getObjectName()` or
Expand Down
6 changes: 6 additions & 0 deletions psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@
TODO: remove in 5.0.0
-->
<referencedMethod name="Doctrine\DBAL\Schema\AbstractAsset::getQuotedName" />

<!--
https://github.com/doctrine/dbal/pull/6677
TODO: remove in 5.0.0
-->
<referencedMethod name="Doctrine\DBAL\Schema\AbstractAsset::isIdentifierQuoted" />
</errorLevel>
</DeprecatedMethod>
<DocblockTypeContradiction>
Expand Down
11 changes: 11 additions & 0 deletions src/Schema/AbstractAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
use Doctrine\DBAL\Schema\Name\Parser;
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
use Doctrine\Deprecations\Deprecation;

use function array_map;
use function assert;
Expand Down Expand Up @@ -119,9 +120,19 @@ public function isQuoted(): bool

/**
* Checks if this identifier is quoted.
*
* @deprecated Parse the name and introspect its identifiers individually using {@see Identifier::isQuoted()}
* instead.
*/
protected function isIdentifierQuoted(string $identifier): bool
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6677',
'%s is deprecated and will be removed in 5.0.',
__METHOD__,
);

return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '[');
}

Expand Down
18 changes: 18 additions & 0 deletions src/Schema/SQLServerSchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ public function createComparator(/* ComparatorConfig $config = new ComparatorCon
);
}

public function createSchemaConfig(): SchemaConfig
{
$config = parent::createSchemaConfig();

$config->setName($this->getCurrentSchemaName());

return $config;
}

/** @throws Exception */
private function getDatabaseCollation(): string
{
Expand All @@ -291,6 +300,15 @@ private function getDatabaseCollation(): string
return $this->databaseCollation;
}

/** @throws Exception */
private function getCurrentSchemaName(): ?string
{
$schemaName = $this->connection->fetchOne('SELECT SCHEMA_NAME()');
assert($schemaName !== false);

return $schemaName;
}

protected function selectTableNames(string $databaseName): Result
{
// The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
Expand Down
158 changes: 110 additions & 48 deletions src/Schema/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@

use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\Exception\InvalidName;
use Doctrine\DBAL\Schema\Exception\NamespaceAlreadyExists;
use Doctrine\DBAL\Schema\Exception\SequenceAlreadyExists;
use Doctrine\DBAL\Schema\Exception\SequenceDoesNotExist;
use Doctrine\DBAL\Schema\Exception\TableAlreadyExists;
use Doctrine\DBAL\Schema\Exception\TableDoesNotExist;
use Doctrine\DBAL\Schema\Name\Identifier;
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
use Doctrine\DBAL\Schema\Name\Parser;
use Doctrine\DBAL\Schema\Name\Parser\UnqualifiedNameParser;
use Doctrine\DBAL\Schema\Name\Parsers;
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
use Doctrine\DBAL\SQL\Builder\CreateSchemaObjectsSQLBuilder;
use Doctrine\DBAL\SQL\Builder\DropSchemaObjectsSQLBuilder;
use Doctrine\Deprecations\Deprecation;

use function array_values;
use function str_contains;
use function count;
use function strtolower;

/**
Expand All @@ -35,10 +39,16 @@
* Every asset in the doctrine schema has a name. A name consists of either a
* namespace.local name pair or just a local unqualified name.
*
* The abstraction layer that covers a PostgreSQL schema is the namespace of an
* Objects in a schema can be referenced by unqualified names or qualified
* names but not both. Whether a given schema uses qualified or unqualified
* names is determined at runtime by the presence of objects with unqualified
* names and namespaces.
*
* The abstraction layer that covers a PostgreSQL schema is the namespace of a
* database object (asset). A schema can have a name, which will be used as
* default namespace for the unqualified database objects that are created in
* the schema.
* the schema. If a schema uses qualified names and has a name, unqualified
* names will be resolved against the corresponding namespace.
*
* In the case of MySQL where cross-database queries are allowed this leads to
* databases being "misinterpreted" as namespaces. This is intentional, however
Expand All @@ -65,6 +75,12 @@ class Schema extends AbstractOptionallyNamedObject

protected SchemaConfig $_schemaConfig;

/**
* Indicates whether the schema uses unqualified names for its objects. Once this flag is set to true, it won't be
* unset even after the objects with unqualified names have been dropped from the schema.
*/
private bool $usesUnqualifiedNames = false;

/**
* @param array<Table> $tables
* @param array<Sequence> $sequences
Expand Down Expand Up @@ -104,39 +120,39 @@ protected function getNameParser(): UnqualifiedNameParser

protected function _addTable(Table $table): void
{
$name = $table->getObjectName();
$resolvedName = $this->resolveName($table->getObjectName());

$normalizedName = $this->normalizeName($name);
$key = $this->getKeyFromResolvedName($resolvedName);

if (isset($this->_tables[$normalizedName])) {
throw TableAlreadyExists::new($normalizedName);
if (isset($this->_tables[$key])) {
throw TableAlreadyExists::new($resolvedName->toString());
}

$this->ensureNamespaceExists($name);
$this->registerQualifier($resolvedName->getQualifier());

$this->_tables[$normalizedName] = $table;
$this->_tables[$key] = $table;
}

protected function _addSequence(Sequence $sequence): void
{
$name = $sequence->getObjectName();
$resolvedName = $this->resolveName($sequence->getObjectName());

$normalizedName = $this->normalizeName($name);
$key = $this->getKeyFromResolvedName($resolvedName);

if (isset($this->_sequences[$normalizedName])) {
throw SequenceAlreadyExists::new($normalizedName);
if (isset($this->_sequences[$key])) {
throw SequenceAlreadyExists::new($resolvedName->toString());
}

$this->ensureNamespaceExists($name);
$this->registerQualifier($resolvedName->getQualifier());

$this->_sequences[$normalizedName] = $sequence;
$this->_sequences[$key] = $sequence;
}

private function ensureNamespaceExists(OptionallyQualifiedName $name): void
private function registerQualifier(?Identifier $qualifier): void
{
$qualifier = $name->getQualifier();

if ($qualifier === null) {
$this->usesUnqualifiedNames = true;

return;
}

Expand Down Expand Up @@ -175,41 +191,84 @@ public function getTables(): array

public function getTable(string $name): Table
{
$name = $this->getFullQualifiedAssetName($name);
if (! isset($this->_tables[$name])) {
$key = $this->getKeyFromName($name);
if (! isset($this->_tables[$key])) {
throw TableDoesNotExist::new($name);
}

return $this->_tables[$name];
return $this->_tables[$key];
}

private function getFullQualifiedAssetName(string $name): string
/**
* Returns the key that will be used to store the given object in a collection of such objects based on its name.
*
* If the schema uses unqualified names, the object name must be unqualified. If the schema uses qualified names,
* the object name must be qualified.
*
* The resulting key is the lower-cased full object name. Lower-casing is
* actually wrong, but we have to do it to keep our sanity. If you are
* using database objects that only differentiate in the casing (FOO vs
* Foo) then you will NOT be able to use Doctrine Schema abstraction.
*/
private function getKeyFromResolvedName(OptionallyQualifiedName $name): string
{
$name = $this->getUnquotedAssetName($name);
$key = $name->getUnqualifiedName()->getValue();
$qualifier = $name->getQualifier();

if (! str_contains($name, '.')) {
$name = $this->getName() . '.' . $name;
if ($qualifier !== null) {
if ($this->usesUnqualifiedNames) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6677#user-content-qualified-names',
'Using qualified names to create or reference objects in a schema that uses unqualified '
. 'names is deprecated.',
);
}

$key = $qualifier->getValue() . '.' . $key;
} elseif (count($this->namespaces) > 0) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/pull/6677#user-content-unqualified-names',
'Using unqualified names to create or reference objects in a schema that uses qualified '
. 'names and lacks a default namespace configuration is deprecated.',
);
}

return strtolower($name);
return strtolower($key);
}

/**
* The normalized name is qualified and lower-cased. Lower-casing is
* actually wrong, but we have to do it to keep our sanity. If you are
* using database objects that only differentiate in the casing (FOO vs
* Foo) then you will NOT be able to use Doctrine Schema abstraction.
* Returns the key that will be used to store the given object with the given name in a collection of such objects.
*
* Every non-namespaced element is prefixed with this schema name.
* If the schema configuration has the default namespace, an unqualified name will be resolved to qualified against
* that namespace.
*/
private function normalizeName(OptionallyQualifiedName $name): string
private function getKeyFromName(string $input): string
{
$namespaceName = $name->getQualifier()?->getValue()
?? $this->getName();
$parser = Parsers::getOptionallyQualifiedNameParser();

try {
$name = $parser->parse($input);
} catch (Parser\Exception $e) {
throw InvalidName::fromParserException($input, $e);
}

return $this->getKeyFromResolvedName(
$this->resolveName($name),
);
}

$name = $namespaceName . '.' . $name->getUnqualifiedName()->getValue();
/**
* Resolves the qualified or unqualified name against the current schema name and returns a qualified name.
*/
private function resolveName(OptionallyQualifiedName $name): OptionallyQualifiedName
{
if ($name->getQualifier() === null && $this->name !== null) {
return new OptionallyQualifiedName($name->getUnqualifiedName(), $this->name->getIdentifier());
}

return strtolower($name);
return $name;
}

/**
Expand Down Expand Up @@ -239,26 +298,26 @@ public function hasNamespace(string $name): bool
*/
public function hasTable(string $name): bool
{
$name = $this->getFullQualifiedAssetName($name);
$key = $this->getKeyFromName($name);

return isset($this->_tables[$name]);
return isset($this->_tables[$key]);
}

public function hasSequence(string $name): bool
{
$name = $this->getFullQualifiedAssetName($name);
$key = $this->getKeyFromName($name);

return isset($this->_sequences[$name]);
return isset($this->_sequences[$key]);
}

public function getSequence(string $name): Sequence
{
$name = $this->getFullQualifiedAssetName($name);
if (! $this->hasSequence($name)) {
$key = $this->getKeyFromName($name);
if (! isset($this->_sequences[$key])) {
throw SequenceDoesNotExist::new($name);
}

return $this->_sequences[$name];
return $this->_sequences[$key];
}

/** @return list<Sequence> */
Expand Down Expand Up @@ -326,9 +385,12 @@ public function renameTable(string $oldName, string $newName): self
*/
public function dropTable(string $name): self
{
$name = $this->getFullQualifiedAssetName($name);
$this->getTable($name);
unset($this->_tables[$name]);
$key = $this->getKeyFromName($name);
if (! isset($this->_tables[$key])) {
throw TableDoesNotExist::new($name);
}

unset($this->_tables[$key]);

return $this;
}
Expand All @@ -347,8 +409,8 @@ public function createSequence(string $name, int $allocationSize = 1, int $initi
/** @return $this */
public function dropSequence(string $name): self
{
$name = $this->getFullQualifiedAssetName($name);
unset($this->_sequences[$name]);
$key = $this->getKeyFromName($name);
unset($this->_sequences[$key]);

return $this;
}
Expand Down
Loading

0 comments on commit 7af9a12

Please sign in to comment.