Skip to content

Commit 69d5e34

Browse files
authored
Merge pull request #6677 from morozov/deprecate-mixing-schema-name-types
Deprecate mixing qualified and unqualified names
2 parents d00d5c5 + dca4e60 commit 69d5e34

7 files changed

+270
-71
lines changed

UPGRADE.md

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ awareness about deprecated code.
88

99
# Upgrade to 4.3
1010

11+
## Deprecated `AbstractAsset::isIdentifierQuoted()`
12+
13+
The `AbstractAsset::isIdentifierQuoted()` method has been deprecated. Parse the name and introspect its identifiers
14+
individually using `Identifier::isQuoted()` instead.
15+
16+
## Deprecated mixing unqualified and qualified names in a schema without a default namespace
17+
18+
If a schema lacks a default namespace configuration and has at least one object with an unqualified name, adding or
19+
referencing objects with qualified names is deprecated.
20+
21+
If a schema lacks a default namespace configuration and has at least one object with a qualified name, adding or
22+
referencing objects with unqualified names is deprecated.
23+
24+
Mixing unqualified and qualified names is permitted as long as the schema is configured to use a default namespace. In
25+
this case, the default namespace will be used to resolve unqualified names.
26+
1127
## Deprecated `AbstractAsset::getQuotedName()`
1228

1329
The `AbstractAsset::getQuotedName()` method has been deprecated. Use `NamedObject::getObjectName()` or

psalm.xml.dist

+6
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@
147147
TODO: remove in 5.0.0
148148
-->
149149
<referencedMethod name="Doctrine\DBAL\Schema\AbstractAsset::getQuotedName" />
150+
151+
<!--
152+
https://github.com/doctrine/dbal/pull/6677
153+
TODO: remove in 5.0.0
154+
-->
155+
<referencedMethod name="Doctrine\DBAL\Schema\AbstractAsset::isIdentifierQuoted" />
150156
</errorLevel>
151157
</DeprecatedMethod>
152158
<DeprecatedProperty>

src/Schema/AbstractAsset.php

+10
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,19 @@ public function isQuoted(): bool
301301

302302
/**
303303
* Checks if this identifier is quoted.
304+
*
305+
* @deprecated Parse the name and introspect its identifiers individually using {@see Identifier::isQuoted()}
306+
* instead.
304307
*/
305308
protected function isIdentifierQuoted(string $identifier): bool
306309
{
310+
Deprecation::triggerIfCalledFromOutside(
311+
'doctrine/dbal',
312+
'https://github.com/doctrine/dbal/pull/6677',
313+
'%s is deprecated and will be removed in 5.0.',
314+
__METHOD__,
315+
);
316+
307317
return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '[');
308318
}
309319

src/Schema/SQLServerSchemaManager.php

+18
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,15 @@ public function createComparator(/* ComparatorConfig $config = new ComparatorCon
273273
);
274274
}
275275

276+
public function createSchemaConfig(): SchemaConfig
277+
{
278+
$config = parent::createSchemaConfig();
279+
280+
$config->setName($this->getCurrentSchemaName());
281+
282+
return $config;
283+
}
284+
276285
/** @throws Exception */
277286
private function getDatabaseCollation(): string
278287
{
@@ -291,6 +300,15 @@ private function getDatabaseCollation(): string
291300
return $this->databaseCollation;
292301
}
293302

303+
/** @throws Exception */
304+
private function getCurrentSchemaName(): ?string
305+
{
306+
$schemaName = $this->connection->fetchOne('SELECT SCHEMA_NAME()');
307+
assert($schemaName !== false);
308+
309+
return $schemaName;
310+
}
311+
294312
protected function selectTableNames(string $databaseName): Result
295313
{
296314
// The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams

src/Schema/Schema.php

+123-59
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
use Doctrine\DBAL\Schema\Exception\SequenceDoesNotExist;
1212
use Doctrine\DBAL\Schema\Exception\TableAlreadyExists;
1313
use Doctrine\DBAL\Schema\Exception\TableDoesNotExist;
14-
use Doctrine\DBAL\Schema\Name\OptionallyQualifiedName;
1514
use Doctrine\DBAL\Schema\Name\Parser\UnqualifiedNameParser;
1615
use Doctrine\DBAL\Schema\Name\Parsers;
1716
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
1817
use Doctrine\DBAL\SQL\Builder\CreateSchemaObjectsSQLBuilder;
1918
use Doctrine\DBAL\SQL\Builder\DropSchemaObjectsSQLBuilder;
19+
use Doctrine\Deprecations\Deprecation;
2020

2121
use function array_values;
22-
use function str_contains;
22+
use function count;
2323
use function strtolower;
2424

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

6672
protected SchemaConfig $_schemaConfig;
6773

74+
/**
75+
* Indicates whether the schema uses unqualified names for its objects. Once this flag is set to true, it won't be
76+
* unset even after the objects with unqualified names have been dropped from the schema.
77+
*/
78+
private bool $usesUnqualifiedNames = false;
79+
6880
/**
6981
* @param array<Table> $tables
7082
* @param array<Sequence> $sequences
@@ -105,42 +117,54 @@ protected function getNameParser(): UnqualifiedNameParser
105117

106118
protected function _addTable(Table $table): void
107119
{
108-
$namespaceName = $table->getNamespaceName();
109-
$tableName = $this->normalizeName($table);
120+
$resolvedName = $this->resolveName($table);
110121

111-
if (isset($this->_tables[$tableName])) {
112-
throw TableAlreadyExists::new($tableName);
122+
$key = $this->getKeyFromResolvedName($resolvedName);
123+
124+
if (isset($this->_tables[$key])) {
125+
throw TableAlreadyExists::new($resolvedName->getName());
113126
}
114127

115-
if (
116-
$namespaceName !== null
117-
&& ! $table->isInDefaultNamespace($this->getName())
118-
&& ! $this->hasNamespace($namespaceName)
119-
) {
120-
$this->createNamespace($namespaceName);
128+
$namespaceName = $resolvedName->getNamespaceName();
129+
130+
if ($namespaceName !== null) {
131+
if (
132+
! $table->isInDefaultNamespace($this->getName())
133+
&& ! $this->hasNamespace($namespaceName)
134+
) {
135+
$this->createNamespace($namespaceName);
136+
}
137+
} else {
138+
$this->usesUnqualifiedNames = true;
121139
}
122140

123-
$this->_tables[$tableName] = $table;
141+
$this->_tables[$key] = $table;
124142
}
125143

126144
protected function _addSequence(Sequence $sequence): void
127145
{
128-
$namespaceName = $sequence->getNamespaceName();
129-
$seqName = $this->normalizeName($sequence);
146+
$resolvedName = $this->resolveName($sequence);
147+
148+
$key = $this->getKeyFromResolvedName($resolvedName);
130149

131-
if (isset($this->_sequences[$seqName])) {
132-
throw SequenceAlreadyExists::new($seqName);
150+
if (isset($this->_sequences[$key])) {
151+
throw SequenceAlreadyExists::new($resolvedName->getName());
133152
}
134153

135-
if (
136-
$namespaceName !== null
137-
&& ! $sequence->isInDefaultNamespace($this->getName())
138-
&& ! $this->hasNamespace($namespaceName)
139-
) {
140-
$this->createNamespace($namespaceName);
154+
$namespaceName = $resolvedName->getNamespaceName();
155+
156+
if ($namespaceName !== null) {
157+
if (
158+
! $sequence->isInDefaultNamespace($this->getName())
159+
&& ! $this->hasNamespace($namespaceName)
160+
) {
161+
$this->createNamespace($namespaceName);
162+
}
163+
} else {
164+
$this->usesUnqualifiedNames = true;
141165
}
142166

143-
$this->_sequences[$seqName] = $sequence;
167+
$this->_sequences[$key] = $sequence;
144168
}
145169

146170
/**
@@ -165,44 +189,81 @@ public function getTables(): array
165189

166190
public function getTable(string $name): Table
167191
{
168-
$name = $this->getFullQualifiedAssetName($name);
169-
if (! isset($this->_tables[$name])) {
192+
$key = $this->getKeyFromName($name);
193+
if (! isset($this->_tables[$key])) {
170194
throw TableDoesNotExist::new($name);
171195
}
172196

173-
return $this->_tables[$name];
197+
return $this->_tables[$key];
174198
}
175199

176-
private function getFullQualifiedAssetName(string $name): string
200+
/**
201+
* Returns the key that will be used to store the given object in a collection of such objects based on its name.
202+
*
203+
* If the schema uses unqualified names, the object name must be unqualified. If the schema uses qualified names,
204+
* the object name must be qualified.
205+
*
206+
* The resulting key is the lower-cased full object name. Lower-casing is
207+
* actually wrong, but we have to do it to keep our sanity. If you are
208+
* using database objects that only differentiate in the casing (FOO vs
209+
* Foo) then you will NOT be able to use Doctrine Schema abstraction.
210+
*
211+
* @param AbstractAsset<N> $asset
212+
*
213+
* @template N of Name
214+
*/
215+
private function getKeyFromResolvedName(AbstractAsset $asset): string
177216
{
178-
$name = $this->getUnquotedAssetName($name);
179-
180-
if (! str_contains($name, '.')) {
181-
$name = $this->getName() . '.' . $name;
217+
$key = $asset->getName();
218+
219+
if ($asset->getNamespaceName() !== null) {
220+
if ($this->usesUnqualifiedNames) {
221+
Deprecation::trigger(
222+
'doctrine/dbal',
223+
'https://github.com/doctrine/dbal/pull/6677#user-content-qualified-names',
224+
'Using qualified names to create or reference objects in a schema that uses unqualified '
225+
. 'names is deprecated.',
226+
);
227+
}
228+
229+
$key = $this->getName() . '.' . $key;
230+
} elseif (count($this->namespaces) > 0) {
231+
Deprecation::trigger(
232+
'doctrine/dbal',
233+
'https://github.com/doctrine/dbal/pull/6677#user-content-unqualified-names',
234+
'Using unqualified names to create or reference objects in a schema that uses qualified '
235+
. 'names and lacks a default namespace configuration is deprecated.',
236+
);
182237
}
183238

184-
return strtolower($name);
239+
return strtolower($key);
185240
}
186241

187242
/**
188-
* The normalized name is qualified and lower-cased. Lower-casing is
189-
* actually wrong, but we have to do it to keep our sanity. If you are
190-
* using database objects that only differentiate in the casing (FOO vs
191-
* Foo) then you will NOT be able to use Doctrine Schema abstraction.
243+
* Returns the key that will be used to store the given object with the given name in a collection of such objects.
192244
*
193-
* Every non-namespaced element is prefixed with this schema name.
194-
*
195-
* @param AbstractAsset<OptionallyQualifiedName> $asset
245+
* If the schema configuration has the default namespace, an unqualified name will be resolved to qualified against
246+
* that namespace.
196247
*/
197-
private function normalizeName(AbstractAsset $asset): string
248+
private function getKeyFromName(string $name): string
198249
{
199-
$name = $asset->getName();
250+
return $this->getKeyFromResolvedName($this->resolveName(new Identifier($name)));
251+
}
200252

201-
if ($asset->getNamespaceName() === null) {
202-
$name = $this->getName() . '.' . $name;
253+
/**
254+
* Resolves the qualified or unqualified name against the current schema name and returns a qualified name.
255+
*
256+
* @param AbstractAsset<N> $asset A database object with optionally qualified name.
257+
*
258+
* @template N of Name
259+
*/
260+
private function resolveName(AbstractAsset $asset): AbstractAsset
261+
{
262+
if ($asset->getNamespaceName() === null && $this->name !== null) {
263+
return new Identifier($this->getName() . '.' . $asset->getName());
203264
}
204265

205-
return strtolower($name);
266+
return $asset;
206267
}
207268

208269
/**
@@ -232,26 +293,26 @@ public function hasNamespace(string $name): bool
232293
*/
233294
public function hasTable(string $name): bool
234295
{
235-
$name = $this->getFullQualifiedAssetName($name);
296+
$key = $this->getKeyFromName($name);
236297

237-
return isset($this->_tables[$name]);
298+
return isset($this->_tables[$key]);
238299
}
239300

240301
public function hasSequence(string $name): bool
241302
{
242-
$name = $this->getFullQualifiedAssetName($name);
303+
$key = $this->getKeyFromName($name);
243304

244-
return isset($this->_sequences[$name]);
305+
return isset($this->_sequences[$key]);
245306
}
246307

247308
public function getSequence(string $name): Sequence
248309
{
249-
$name = $this->getFullQualifiedAssetName($name);
250-
if (! $this->hasSequence($name)) {
310+
$key = $this->getKeyFromName($name);
311+
if (! isset($this->_sequences[$key])) {
251312
throw SequenceDoesNotExist::new($name);
252313
}
253314

254-
return $this->_sequences[$name];
315+
return $this->_sequences[$key];
255316
}
256317

257318
/** @return list<Sequence> */
@@ -322,9 +383,12 @@ public function renameTable(string $oldName, string $newName): self
322383
*/
323384
public function dropTable(string $name): self
324385
{
325-
$name = $this->getFullQualifiedAssetName($name);
326-
$this->getTable($name);
327-
unset($this->_tables[$name]);
386+
$key = $this->getKeyFromName($name);
387+
if (! isset($this->_tables[$key])) {
388+
throw TableDoesNotExist::new($name);
389+
}
390+
391+
unset($this->_tables[$key]);
328392

329393
return $this;
330394
}
@@ -343,8 +407,8 @@ public function createSequence(string $name, int $allocationSize = 1, int $initi
343407
/** @return $this */
344408
public function dropSequence(string $name): self
345409
{
346-
$name = $this->getFullQualifiedAssetName($name);
347-
unset($this->_sequences[$name]);
410+
$key = $this->getKeyFromName($name);
411+
unset($this->_sequences[$key]);
348412

349413
return $this;
350414
}

0 commit comments

Comments
 (0)