1<?php
2
3namespace Doctrine\DBAL\Schema;
4
5use Doctrine\DBAL\Platforms\AbstractPlatform;
6use Doctrine\DBAL\Schema\Visitor\CreateSchemaSqlCollector;
7use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
8use Doctrine\DBAL\Schema\Visitor\NamespaceVisitor;
9use Doctrine\DBAL\Schema\Visitor\Visitor;
10
11use function array_keys;
12use function strpos;
13use function strtolower;
14
15/**
16 * Object representation of a database schema.
17 *
18 * Different vendors have very inconsistent naming with regard to the concept
19 * of a "schema". Doctrine understands a schema as the entity that conceptually
20 * wraps a set of database objects such as tables, sequences, indexes and
21 * foreign keys that belong to each other into a namespace. A Doctrine Schema
22 * has nothing to do with the "SCHEMA" defined as in PostgreSQL, it is more
23 * related to the concept of "DATABASE" that exists in MySQL and PostgreSQL.
24 *
25 * Every asset in the doctrine schema has a name. A name consists of either a
26 * namespace.local name pair or just a local unqualified name.
27 *
28 * The abstraction layer that covers a PostgreSQL schema is the namespace of an
29 * database object (asset). A schema can have a name, which will be used as
30 * default namespace for the unqualified database objects that are created in
31 * the schema.
32 *
33 * In the case of MySQL where cross-database queries are allowed this leads to
34 * databases being "misinterpreted" as namespaces. This is intentional, however
35 * the CREATE/DROP SQL visitors will just filter this queries and do not
36 * execute them. Only the queries for the currently connected database are
37 * executed.
38 */
39class Schema extends AbstractAsset
40{
41    /**
42     * The namespaces in this schema.
43     *
44     * @var string[]
45     */
46    private $namespaces = [];
47
48    /** @var Table[] */
49    protected $_tables = [];
50
51    /** @var Sequence[] */
52    protected $_sequences = [];
53
54    /** @var SchemaConfig */
55    protected $_schemaConfig;
56
57    /**
58     * @param Table[]    $tables
59     * @param Sequence[] $sequences
60     * @param string[]   $namespaces
61     *
62     * @throws SchemaException
63     */
64    public function __construct(
65        array $tables = [],
66        array $sequences = [],
67        ?SchemaConfig $schemaConfig = null,
68        array $namespaces = []
69    ) {
70        if ($schemaConfig === null) {
71            $schemaConfig = new SchemaConfig();
72        }
73
74        $this->_schemaConfig = $schemaConfig;
75        $this->_setName($schemaConfig->getName() ?? 'public');
76
77        foreach ($namespaces as $namespace) {
78            $this->createNamespace($namespace);
79        }
80
81        foreach ($tables as $table) {
82            $this->_addTable($table);
83        }
84
85        foreach ($sequences as $sequence) {
86            $this->_addSequence($sequence);
87        }
88    }
89
90    /**
91     * @return bool
92     */
93    public function hasExplicitForeignKeyIndexes()
94    {
95        return $this->_schemaConfig->hasExplicitForeignKeyIndexes();
96    }
97
98    /**
99     * @return void
100     *
101     * @throws SchemaException
102     */
103    protected function _addTable(Table $table)
104    {
105        $namespaceName = $table->getNamespaceName();
106        $tableName     = $table->getFullQualifiedName($this->getName());
107
108        if (isset($this->_tables[$tableName])) {
109            throw SchemaException::tableAlreadyExists($tableName);
110        }
111
112        if (
113            $namespaceName !== null
114            && ! $table->isInDefaultNamespace($this->getName())
115            && ! $this->hasNamespace($namespaceName)
116        ) {
117            $this->createNamespace($namespaceName);
118        }
119
120        $this->_tables[$tableName] = $table;
121        $table->setSchemaConfig($this->_schemaConfig);
122    }
123
124    /**
125     * @return void
126     *
127     * @throws SchemaException
128     */
129    protected function _addSequence(Sequence $sequence)
130    {
131        $namespaceName = $sequence->getNamespaceName();
132        $seqName       = $sequence->getFullQualifiedName($this->getName());
133
134        if (isset($this->_sequences[$seqName])) {
135            throw SchemaException::sequenceAlreadyExists($seqName);
136        }
137
138        if (
139            $namespaceName !== null
140            && ! $sequence->isInDefaultNamespace($this->getName())
141            && ! $this->hasNamespace($namespaceName)
142        ) {
143            $this->createNamespace($namespaceName);
144        }
145
146        $this->_sequences[$seqName] = $sequence;
147    }
148
149    /**
150     * Returns the namespaces of this schema.
151     *
152     * @return string[] A list of namespace names.
153     */
154    public function getNamespaces()
155    {
156        return $this->namespaces;
157    }
158
159    /**
160     * Gets all tables of this schema.
161     *
162     * @return Table[]
163     */
164    public function getTables()
165    {
166        return $this->_tables;
167    }
168
169    /**
170     * @param string $name
171     *
172     * @return Table
173     *
174     * @throws SchemaException
175     */
176    public function getTable($name)
177    {
178        $name = $this->getFullQualifiedAssetName($name);
179        if (! isset($this->_tables[$name])) {
180            throw SchemaException::tableDoesNotExist($name);
181        }
182
183        return $this->_tables[$name];
184    }
185
186    /**
187     * @param string $name
188     *
189     * @return string
190     */
191    private function getFullQualifiedAssetName($name)
192    {
193        $name = $this->getUnquotedAssetName($name);
194
195        if (strpos($name, '.') === false) {
196            $name = $this->getName() . '.' . $name;
197        }
198
199        return strtolower($name);
200    }
201
202    /**
203     * Returns the unquoted representation of a given asset name.
204     *
205     * @param string $assetName Quoted or unquoted representation of an asset name.
206     *
207     * @return string
208     */
209    private function getUnquotedAssetName($assetName)
210    {
211        if ($this->isIdentifierQuoted($assetName)) {
212            return $this->trimQuotes($assetName);
213        }
214
215        return $assetName;
216    }
217
218    /**
219     * Does this schema have a namespace with the given name?
220     *
221     * @param string $name
222     *
223     * @return bool
224     */
225    public function hasNamespace($name)
226    {
227        $name = strtolower($this->getUnquotedAssetName($name));
228
229        return isset($this->namespaces[$name]);
230    }
231
232    /**
233     * Does this schema have a table with the given name?
234     *
235     * @param string $name
236     *
237     * @return bool
238     */
239    public function hasTable($name)
240    {
241        $name = $this->getFullQualifiedAssetName($name);
242
243        return isset($this->_tables[$name]);
244    }
245
246    /**
247     * Gets all table names, prefixed with a schema name, even the default one if present.
248     *
249     * @return string[]
250     */
251    public function getTableNames()
252    {
253        return array_keys($this->_tables);
254    }
255
256    /**
257     * @param string $name
258     *
259     * @return bool
260     */
261    public function hasSequence($name)
262    {
263        $name = $this->getFullQualifiedAssetName($name);
264
265        return isset($this->_sequences[$name]);
266    }
267
268    /**
269     * @param string $name
270     *
271     * @return Sequence
272     *
273     * @throws SchemaException
274     */
275    public function getSequence($name)
276    {
277        $name = $this->getFullQualifiedAssetName($name);
278        if (! $this->hasSequence($name)) {
279            throw SchemaException::sequenceDoesNotExist($name);
280        }
281
282        return $this->_sequences[$name];
283    }
284
285    /**
286     * @return Sequence[]
287     */
288    public function getSequences()
289    {
290        return $this->_sequences;
291    }
292
293    /**
294     * Creates a new namespace.
295     *
296     * @param string $name The name of the namespace to create.
297     *
298     * @return Schema This schema instance.
299     *
300     * @throws SchemaException
301     */
302    public function createNamespace($name)
303    {
304        $unquotedName = strtolower($this->getUnquotedAssetName($name));
305
306        if (isset($this->namespaces[$unquotedName])) {
307            throw SchemaException::namespaceAlreadyExists($unquotedName);
308        }
309
310        $this->namespaces[$unquotedName] = $name;
311
312        return $this;
313    }
314
315    /**
316     * Creates a new table.
317     *
318     * @param string $name
319     *
320     * @return Table
321     *
322     * @throws SchemaException
323     */
324    public function createTable($name)
325    {
326        $table = new Table($name);
327        $this->_addTable($table);
328
329        foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) {
330            $table->addOption($option, $value);
331        }
332
333        return $table;
334    }
335
336    /**
337     * Renames a table.
338     *
339     * @param string $oldName
340     * @param string $newName
341     *
342     * @return Schema
343     *
344     * @throws SchemaException
345     */
346    public function renameTable($oldName, $newName)
347    {
348        $table = $this->getTable($oldName);
349        $table->_setName($newName);
350
351        $this->dropTable($oldName);
352        $this->_addTable($table);
353
354        return $this;
355    }
356
357    /**
358     * Drops a table from the schema.
359     *
360     * @param string $name
361     *
362     * @return Schema
363     *
364     * @throws SchemaException
365     */
366    public function dropTable($name)
367    {
368        $name = $this->getFullQualifiedAssetName($name);
369        $this->getTable($name);
370        unset($this->_tables[$name]);
371
372        return $this;
373    }
374
375    /**
376     * Creates a new sequence.
377     *
378     * @param string $name
379     * @param int    $allocationSize
380     * @param int    $initialValue
381     *
382     * @return Sequence
383     *
384     * @throws SchemaException
385     */
386    public function createSequence($name, $allocationSize = 1, $initialValue = 1)
387    {
388        $seq = new Sequence($name, $allocationSize, $initialValue);
389        $this->_addSequence($seq);
390
391        return $seq;
392    }
393
394    /**
395     * @param string $name
396     *
397     * @return Schema
398     */
399    public function dropSequence($name)
400    {
401        $name = $this->getFullQualifiedAssetName($name);
402        unset($this->_sequences[$name]);
403
404        return $this;
405    }
406
407    /**
408     * Returns an array of necessary SQL queries to create the schema on the given platform.
409     *
410     * @return string[]
411     */
412    public function toSql(AbstractPlatform $platform)
413    {
414        $sqlCollector = new CreateSchemaSqlCollector($platform);
415        $this->visit($sqlCollector);
416
417        return $sqlCollector->getQueries();
418    }
419
420    /**
421     * Return an array of necessary SQL queries to drop the schema on the given platform.
422     *
423     * @return string[]
424     */
425    public function toDropSql(AbstractPlatform $platform)
426    {
427        $dropSqlCollector = new DropSchemaSqlCollector($platform);
428        $this->visit($dropSqlCollector);
429
430        return $dropSqlCollector->getQueries();
431    }
432
433    /**
434     * @return string[]
435     *
436     * @throws SchemaException
437     */
438    public function getMigrateToSql(Schema $toSchema, AbstractPlatform $platform)
439    {
440        $comparator = new Comparator();
441        $schemaDiff = $comparator->compare($this, $toSchema);
442
443        return $schemaDiff->toSql($platform);
444    }
445
446    /**
447     * @return string[]
448     *
449     * @throws SchemaException
450     */
451    public function getMigrateFromSql(Schema $fromSchema, AbstractPlatform $platform)
452    {
453        $comparator = new Comparator();
454        $schemaDiff = $comparator->compare($fromSchema, $this);
455
456        return $schemaDiff->toSql($platform);
457    }
458
459    /**
460     * @return void
461     */
462    public function visit(Visitor $visitor)
463    {
464        $visitor->acceptSchema($this);
465
466        if ($visitor instanceof NamespaceVisitor) {
467            foreach ($this->namespaces as $namespace) {
468                $visitor->acceptNamespace($namespace);
469            }
470        }
471
472        foreach ($this->_tables as $table) {
473            $table->visit($visitor);
474        }
475
476        foreach ($this->_sequences as $sequence) {
477            $sequence->visit($visitor);
478        }
479    }
480
481    /**
482     * Cloning a Schema triggers a deep clone of all related assets.
483     *
484     * @return void
485     */
486    public function __clone()
487    {
488        foreach ($this->_tables as $k => $table) {
489            $this->_tables[$k] = clone $table;
490        }
491
492        foreach ($this->_sequences as $k => $sequence) {
493            $this->_sequences[$k] = clone $sequence;
494        }
495    }
496}
497