summaryrefslogtreecommitdiff
path: root/vendor/doctrine/dbal/src/Schema
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/dbal/src/Schema')
-rw-r--r--vendor/doctrine/dbal/src/Schema/AbstractAsset.php157
-rw-r--r--vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php864
-rw-r--r--vendor/doctrine/dbal/src/Schema/Column.php252
-rw-r--r--vendor/doctrine/dbal/src/Schema/ColumnDiff.php106
-rw-r--r--vendor/doctrine/dbal/src/Schema/Comparator.php417
-rw-r--r--vendor/doctrine/dbal/src/Schema/DB2SchemaManager.php371
-rw-r--r--vendor/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php20
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php21
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php21
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/InvalidTableName.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php19
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php21
-rw-r--r--vendor/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php21
-rw-r--r--vendor/doctrine/dbal/src/Schema/ForeignKeyConstraint.php291
-rw-r--r--vendor/doctrine/dbal/src/Schema/Identifier.php29
-rw-r--r--vendor/doctrine/dbal/src/Schema/Index.php314
-rw-r--r--vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php548
-rw-r--r--vendor/doctrine/dbal/src/Schema/OracleSchemaManager.php475
-rw-r--r--vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php572
-rw-r--r--vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php498
-rw-r--r--vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php620
-rw-r--r--vendor/doctrine/dbal/src/Schema/Schema.php374
-rw-r--r--vendor/doctrine/dbal/src/Schema/SchemaConfig.php61
-rw-r--r--vendor/doctrine/dbal/src/Schema/SchemaDiff.php109
-rw-r--r--vendor/doctrine/dbal/src/Schema/SchemaException.php12
-rw-r--r--vendor/doctrine/dbal/src/Schema/SchemaManagerFactory.php17
-rw-r--r--vendor/doctrine/dbal/src/Schema/Sequence.php98
-rw-r--r--vendor/doctrine/dbal/src/Schema/Table.php753
-rw-r--r--vendor/doctrine/dbal/src/Schema/TableDiff.php164
-rw-r--r--vendor/doctrine/dbal/src/Schema/UniqueConstraint.php152
-rw-r--r--vendor/doctrine/dbal/src/Schema/View.php21
39 files changed, 7569 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Schema/AbstractAsset.php b/vendor/doctrine/dbal/src/Schema/AbstractAsset.php
new file mode 100644
index 0000000..de5b56f
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/AbstractAsset.php
@@ -0,0 +1,157 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8
9use function array_map;
10use function crc32;
11use function dechex;
12use function explode;
13use function implode;
14use function str_contains;
15use function str_replace;
16use function strtolower;
17use function strtoupper;
18use function substr;
19
20/**
21 * The abstract asset allows to reset the name of all assets without publishing this to the public userland.
22 *
23 * This encapsulation hack is necessary to keep a consistent state of the database schema. Say we have a list of tables
24 * array($tableName => Table($tableName)); if you want to rename the table, you have to make sure this does not get
25 * recreated during schema migration.
26 */
27abstract class AbstractAsset
28{
29 protected string $_name = '';
30
31 /**
32 * Namespace of the asset. If none isset the default namespace is assumed.
33 */
34 protected ?string $_namespace = null;
35
36 protected bool $_quoted = false;
37
38 /**
39 * Sets the name of this asset.
40 */
41 protected function _setName(string $name): void
42 {
43 if ($this->isIdentifierQuoted($name)) {
44 $this->_quoted = true;
45 $name = $this->trimQuotes($name);
46 }
47
48 if (str_contains($name, '.')) {
49 $parts = explode('.', $name);
50 $this->_namespace = $parts[0];
51 $name = $parts[1];
52 }
53
54 $this->_name = $name;
55 }
56
57 /**
58 * Is this asset in the default namespace?
59 */
60 public function isInDefaultNamespace(string $defaultNamespaceName): bool
61 {
62 return $this->_namespace === $defaultNamespaceName || $this->_namespace === null;
63 }
64
65 /**
66 * Gets the namespace name of this asset.
67 *
68 * If NULL is returned this means the default namespace is used.
69 */
70 public function getNamespaceName(): ?string
71 {
72 return $this->_namespace;
73 }
74
75 /**
76 * The shortest name is stripped of the default namespace. All other
77 * namespaced elements are returned as full-qualified names.
78 */
79 public function getShortestName(?string $defaultNamespaceName): string
80 {
81 $shortestName = $this->getName();
82 if ($this->_namespace === $defaultNamespaceName) {
83 $shortestName = $this->_name;
84 }
85
86 return strtolower($shortestName);
87 }
88
89 /**
90 * Checks if this asset's name is quoted.
91 */
92 public function isQuoted(): bool
93 {
94 return $this->_quoted;
95 }
96
97 /**
98 * Checks if this identifier is quoted.
99 */
100 protected function isIdentifierQuoted(string $identifier): bool
101 {
102 return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '[');
103 }
104
105 /**
106 * Trim quotes from the identifier.
107 */
108 protected function trimQuotes(string $identifier): string
109 {
110 return str_replace(['`', '"', '[', ']'], '', $identifier);
111 }
112
113 /**
114 * Returns the name of this schema asset.
115 */
116 public function getName(): string
117 {
118 if ($this->_namespace !== null) {
119 return $this->_namespace . '.' . $this->_name;
120 }
121
122 return $this->_name;
123 }
124
125 /**
126 * Gets the quoted representation of this asset but only if it was defined with one. Otherwise
127 * return the plain unquoted value as inserted.
128 */
129 public function getQuotedName(AbstractPlatform $platform): string
130 {
131 $keywords = $platform->getReservedKeywordsList();
132 $parts = explode('.', $this->getName());
133 foreach ($parts as $k => $v) {
134 $parts[$k] = $this->_quoted || $keywords->isKeyword($v) ? $platform->quoteIdentifier($v) : $v;
135 }
136
137 return implode('.', $parts);
138 }
139
140 /**
141 * Generates an identifier from a list of column names obeying a certain string length.
142 *
143 * This is especially important for Oracle, since it does not allow identifiers larger than 30 chars,
144 * however building idents automatically for foreign keys, composite keys or such can easily create
145 * very long names.
146 *
147 * @param array<int, string> $columnNames
148 */
149 protected function _generateIdentifierName(array $columnNames, string $prefix = '', int $maxSize = 30): string
150 {
151 $hash = implode('', array_map(static function ($column): string {
152 return dechex(crc32($column));
153 }, $columnNames));
154
155 return strtoupper(substr($prefix . '_' . $hash, 0, $maxSize));
156 }
157}
diff --git a/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php b/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php
new file mode 100644
index 0000000..9730797
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php
@@ -0,0 +1,864 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Exception;
9use Doctrine\DBAL\Exception\DatabaseRequired;
10use Doctrine\DBAL\Platforms\AbstractPlatform;
11use Doctrine\DBAL\Platforms\Exception\NotSupported;
12use Doctrine\DBAL\Result;
13use Doctrine\DBAL\Schema\Exception\TableDoesNotExist;
14
15use function array_filter;
16use function array_intersect;
17use function array_map;
18use function array_values;
19use function count;
20use function strtolower;
21
22/**
23 * Base class for schema managers. Schema managers are used to inspect and/or
24 * modify the database schema/structure.
25 *
26 * @template-covariant T of AbstractPlatform
27 */
28abstract class AbstractSchemaManager
29{
30 /** @param T $platform */
31 public function __construct(protected Connection $connection, protected AbstractPlatform $platform)
32 {
33 }
34
35 /**
36 * Lists the available databases for this connection.
37 *
38 * @return array<int, string>
39 *
40 * @throws Exception
41 */
42 public function listDatabases(): array
43 {
44 return array_map(function (array $row): string {
45 return $this->_getPortableDatabaseDefinition($row);
46 }, $this->connection->fetchAllAssociative(
47 $this->platform->getListDatabasesSQL(),
48 ));
49 }
50
51 /**
52 * Returns a list of the names of all schemata in the current database.
53 *
54 * @return list<string>
55 *
56 * @throws Exception
57 */
58 public function listSchemaNames(): array
59 {
60 throw NotSupported::new(__METHOD__);
61 }
62
63 /**
64 * Lists the available sequences for this connection.
65 *
66 * @return array<int, Sequence>
67 *
68 * @throws Exception
69 */
70 public function listSequences(): array
71 {
72 return $this->filterAssetNames(
73 array_map(function (array $row): Sequence {
74 return $this->_getPortableSequenceDefinition($row);
75 }, $this->connection->fetchAllAssociative(
76 $this->platform->getListSequencesSQL(
77 $this->getDatabase(__METHOD__),
78 ),
79 )),
80 );
81 }
82
83 /**
84 * Lists the columns for a given table.
85 *
86 * In contrast to other libraries and to the old version of Doctrine,
87 * this column definition does try to contain the 'primary' column for
88 * the reason that it is not portable across different RDBMS. Use
89 * {@see listTableIndexes($tableName)} to retrieve the primary key
90 * of a table. Where a RDBMS specifies more details, these are held
91 * in the platformDetails array.
92 *
93 * @return array<string, Column>
94 *
95 * @throws Exception
96 */
97 public function listTableColumns(string $table): array
98 {
99 $database = $this->getDatabase(__METHOD__);
100
101 return $this->_getPortableTableColumnList(
102 $table,
103 $database,
104 $this->selectTableColumns($database, $this->normalizeName($table))
105 ->fetchAllAssociative(),
106 );
107 }
108
109 /**
110 * Lists the indexes for a given table returning an array of Index instances.
111 *
112 * Keys of the portable indexes list are all lower-cased.
113 *
114 * @return array<string, Index>
115 *
116 * @throws Exception
117 */
118 public function listTableIndexes(string $table): array
119 {
120 $database = $this->getDatabase(__METHOD__);
121 $table = $this->normalizeName($table);
122
123 return $this->_getPortableTableIndexesList(
124 $this->selectIndexColumns(
125 $database,
126 $table,
127 )->fetchAllAssociative(),
128 $table,
129 );
130 }
131
132 /**
133 * Returns true if all the given tables exist.
134 *
135 * @param array<int, string> $names
136 *
137 * @throws Exception
138 */
139 public function tablesExist(array $names): bool
140 {
141 $names = array_map('strtolower', $names);
142
143 return count($names) === count(array_intersect($names, array_map('strtolower', $this->listTableNames())));
144 }
145
146 public function tableExists(string $tableName): bool
147 {
148 return $this->tablesExist([$tableName]);
149 }
150
151 /**
152 * Returns a list of all tables in the current database.
153 *
154 * @return array<int, string>
155 *
156 * @throws Exception
157 */
158 public function listTableNames(): array
159 {
160 return $this->filterAssetNames(
161 array_map(function (array $row): string {
162 return $this->_getPortableTableDefinition($row);
163 }, $this->selectTableNames(
164 $this->getDatabase(__METHOD__),
165 )->fetchAllAssociative()),
166 );
167 }
168
169 /**
170 * Filters asset names if they are configured to return only a subset of all
171 * the found elements.
172 *
173 * @param array<int, mixed> $assetNames
174 *
175 * @return array<int, mixed>
176 */
177 private function filterAssetNames(array $assetNames): array
178 {
179 $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter();
180
181 return array_values(array_filter($assetNames, $filter));
182 }
183
184 /**
185 * Lists the tables for this connection.
186 *
187 * @return list<Table>
188 *
189 * @throws Exception
190 */
191 public function listTables(): array
192 {
193 $database = $this->getDatabase(__METHOD__);
194
195 $tableColumnsByTable = $this->fetchTableColumnsByTable($database);
196 $indexColumnsByTable = $this->fetchIndexColumnsByTable($database);
197 $foreignKeyColumnsByTable = $this->fetchForeignKeyColumnsByTable($database);
198 $tableOptionsByTable = $this->fetchTableOptionsByTable($database);
199
200 $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter();
201 $tables = [];
202
203 foreach ($tableColumnsByTable as $tableName => $tableColumns) {
204 if (! $filter($tableName)) {
205 continue;
206 }
207
208 $tables[] = new Table(
209 $tableName,
210 $this->_getPortableTableColumnList($tableName, $database, $tableColumns),
211 $this->_getPortableTableIndexesList($indexColumnsByTable[$tableName] ?? [], $tableName),
212 [],
213 $this->_getPortableTableForeignKeysList($foreignKeyColumnsByTable[$tableName] ?? []),
214 $tableOptionsByTable[$tableName] ?? [],
215 );
216 }
217
218 return $tables;
219 }
220
221 /**
222 * An extension point for those platforms where case sensitivity of the object name depends on whether it's quoted.
223 *
224 * Such platforms should convert a possibly quoted name into a value of the corresponding case.
225 */
226 protected function normalizeName(string $name): string
227 {
228 $identifier = new Identifier($name);
229
230 return $identifier->getName();
231 }
232
233 /**
234 * Selects names of tables in the specified database.
235 *
236 * @throws Exception
237 */
238 abstract protected function selectTableNames(string $databaseName): Result;
239
240 /**
241 * Selects definitions of table columns in the specified database. If the table name is specified, narrows down
242 * the selection to this table.
243 *
244 * @throws Exception
245 */
246 abstract protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result;
247
248 /**
249 * Selects definitions of index columns in the specified database. If the table name is specified, narrows down
250 * the selection to this table.
251 *
252 * @throws Exception
253 */
254 abstract protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result;
255
256 /**
257 * Selects definitions of foreign key columns in the specified database. If the table name is specified,
258 * narrows down the selection to this table.
259 *
260 * @throws Exception
261 */
262 abstract protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result;
263
264 /**
265 * Fetches definitions of table columns in the specified database and returns them grouped by table name.
266 *
267 * @return array<string,list<array<string,mixed>>>
268 *
269 * @throws Exception
270 */
271 protected function fetchTableColumnsByTable(string $databaseName): array
272 {
273 return $this->fetchAllAssociativeGrouped($this->selectTableColumns($databaseName));
274 }
275
276 /**
277 * Fetches definitions of index columns in the specified database and returns them grouped by table name.
278 *
279 * @return array<string,list<array<string,mixed>>>
280 *
281 * @throws Exception
282 */
283 protected function fetchIndexColumnsByTable(string $databaseName): array
284 {
285 return $this->fetchAllAssociativeGrouped($this->selectIndexColumns($databaseName));
286 }
287
288 /**
289 * Fetches definitions of foreign key columns in the specified database and returns them grouped by table name.
290 *
291 * @return array<string, list<array<string, mixed>>>
292 *
293 * @throws Exception
294 */
295 protected function fetchForeignKeyColumnsByTable(string $databaseName): array
296 {
297 return $this->fetchAllAssociativeGrouped(
298 $this->selectForeignKeyColumns($databaseName),
299 );
300 }
301
302 /**
303 * Fetches table options for the tables in the specified database and returns them grouped by table name.
304 * If the table name is specified, narrows down the selection to this table.
305 *
306 * @return array<string,array<string,mixed>>
307 *
308 * @throws Exception
309 */
310 abstract protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array;
311
312 /**
313 * Introspects the table with the given name.
314 *
315 * @throws Exception
316 */
317 public function introspectTable(string $name): Table
318 {
319 $columns = $this->listTableColumns($name);
320
321 if ($columns === []) {
322 throw TableDoesNotExist::new($name);
323 }
324
325 return new Table(
326 $name,
327 $columns,
328 $this->listTableIndexes($name),
329 [],
330 $this->listTableForeignKeys($name),
331 $this->getTableOptions($name),
332 );
333 }
334
335 /**
336 * Lists the views this connection has.
337 *
338 * @return list<View>
339 *
340 * @throws Exception
341 */
342 public function listViews(): array
343 {
344 return array_map(function (array $row): View {
345 return $this->_getPortableViewDefinition($row);
346 }, $this->connection->fetchAllAssociative(
347 $this->platform->getListViewsSQL(
348 $this->getDatabase(__METHOD__),
349 ),
350 ));
351 }
352
353 /**
354 * Lists the foreign keys for the given table.
355 *
356 * @return array<int|string, ForeignKeyConstraint>
357 *
358 * @throws Exception
359 */
360 public function listTableForeignKeys(string $table): array
361 {
362 $database = $this->getDatabase(__METHOD__);
363
364 return $this->_getPortableTableForeignKeysList(
365 $this->selectForeignKeyColumns(
366 $database,
367 $this->normalizeName($table),
368 )->fetchAllAssociative(),
369 );
370 }
371
372 /**
373 * @return array<string, mixed>
374 *
375 * @throws Exception
376 */
377 private function getTableOptions(string $name): array
378 {
379 $normalizedName = $this->normalizeName($name);
380
381 return $this->fetchTableOptionsByTable(
382 $this->getDatabase(__METHOD__),
383 $normalizedName,
384 )[$normalizedName] ?? [];
385 }
386
387 /* drop*() Methods */
388
389 /**
390 * Drops a database.
391 *
392 * NOTE: You can not drop the database this SchemaManager is currently connected to.
393 *
394 * @throws Exception
395 */
396 public function dropDatabase(string $database): void
397 {
398 $this->connection->executeStatement(
399 $this->platform->getDropDatabaseSQL($database),
400 );
401 }
402
403 /**
404 * Drops a schema.
405 *
406 * @throws Exception
407 */
408 public function dropSchema(string $schemaName): void
409 {
410 $this->connection->executeStatement(
411 $this->platform->getDropSchemaSQL($schemaName),
412 );
413 }
414
415 /**
416 * Drops the given table.
417 *
418 * @throws Exception
419 */
420 public function dropTable(string $name): void
421 {
422 $this->connection->executeStatement(
423 $this->platform->getDropTableSQL($name),
424 );
425 }
426
427 /**
428 * Drops the index from the given table.
429 *
430 * @throws Exception
431 */
432 public function dropIndex(string $index, string $table): void
433 {
434 $this->connection->executeStatement(
435 $this->platform->getDropIndexSQL($index, $table),
436 );
437 }
438
439 /**
440 * Drops a foreign key from a table.
441 *
442 * @throws Exception
443 */
444 public function dropForeignKey(string $name, string $table): void
445 {
446 $this->connection->executeStatement(
447 $this->platform->getDropForeignKeySQL($name, $table),
448 );
449 }
450
451 /**
452 * Drops a sequence with a given name.
453 *
454 * @throws Exception
455 */
456 public function dropSequence(string $name): void
457 {
458 $this->connection->executeStatement(
459 $this->platform->getDropSequenceSQL($name),
460 );
461 }
462
463 /**
464 * Drops the unique constraint from the given table.
465 *
466 * @throws Exception
467 */
468 public function dropUniqueConstraint(string $name, string $tableName): void
469 {
470 $this->connection->executeStatement(
471 $this->platform->getDropUniqueConstraintSQL($name, $tableName),
472 );
473 }
474
475 /**
476 * Drops a view.
477 *
478 * @throws Exception
479 */
480 public function dropView(string $name): void
481 {
482 $this->connection->executeStatement(
483 $this->platform->getDropViewSQL($name),
484 );
485 }
486
487 /* create*() Methods */
488
489 /** @throws Exception */
490 public function createSchemaObjects(Schema $schema): void
491 {
492 $this->executeStatements($schema->toSql($this->platform));
493 }
494
495 /**
496 * Creates a new database.
497 *
498 * @throws Exception
499 */
500 public function createDatabase(string $database): void
501 {
502 $this->connection->executeStatement(
503 $this->platform->getCreateDatabaseSQL($database),
504 );
505 }
506
507 /**
508 * Creates a new table.
509 *
510 * @throws Exception
511 */
512 public function createTable(Table $table): void
513 {
514 $this->executeStatements($this->platform->getCreateTableSQL($table));
515 }
516
517 /**
518 * Creates a new sequence.
519 *
520 * @throws Exception
521 */
522 public function createSequence(Sequence $sequence): void
523 {
524 $this->connection->executeStatement(
525 $this->platform->getCreateSequenceSQL($sequence),
526 );
527 }
528
529 /**
530 * Creates a new index on a table.
531 *
532 * @param string $table The name of the table on which the index is to be created.
533 *
534 * @throws Exception
535 */
536 public function createIndex(Index $index, string $table): void
537 {
538 $this->connection->executeStatement(
539 $this->platform->getCreateIndexSQL($index, $table),
540 );
541 }
542
543 /**
544 * Creates a new foreign key.
545 *
546 * @param ForeignKeyConstraint $foreignKey The ForeignKey instance.
547 * @param string $table The name of the table on which the foreign key is to be created.
548 *
549 * @throws Exception
550 */
551 public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void
552 {
553 $this->connection->executeStatement(
554 $this->platform->getCreateForeignKeySQL($foreignKey, $table),
555 );
556 }
557
558 /**
559 * Creates a unique constraint on a table.
560 *
561 * @throws Exception
562 */
563 public function createUniqueConstraint(UniqueConstraint $uniqueConstraint, string $tableName): void
564 {
565 $this->connection->executeStatement(
566 $this->platform->getCreateUniqueConstraintSQL($uniqueConstraint, $tableName),
567 );
568 }
569
570 /**
571 * Creates a new view.
572 *
573 * @throws Exception
574 */
575 public function createView(View $view): void
576 {
577 $this->connection->executeStatement(
578 $this->platform->getCreateViewSQL(
579 $view->getQuotedName($this->platform),
580 $view->getSql(),
581 ),
582 );
583 }
584
585 /** @throws Exception */
586 public function dropSchemaObjects(Schema $schema): void
587 {
588 $this->executeStatements($schema->toDropSql($this->platform));
589 }
590
591 /**
592 * Alters an existing schema.
593 *
594 * @throws Exception
595 */
596 public function alterSchema(SchemaDiff $schemaDiff): void
597 {
598 $this->executeStatements($this->platform->getAlterSchemaSQL($schemaDiff));
599 }
600
601 /**
602 * Migrates an existing schema to a new schema.
603 *
604 * @throws Exception
605 */
606 public function migrateSchema(Schema $newSchema): void
607 {
608 $schemaDiff = $this->createComparator()
609 ->compareSchemas($this->introspectSchema(), $newSchema);
610
611 $this->alterSchema($schemaDiff);
612 }
613
614 /* alterTable() Methods */
615
616 /**
617 * Alters an existing tables schema.
618 *
619 * @throws Exception
620 */
621 public function alterTable(TableDiff $tableDiff): void
622 {
623 $this->executeStatements($this->platform->getAlterTableSQL($tableDiff));
624 }
625
626 /**
627 * Renames a given table to another name.
628 *
629 * @throws Exception
630 */
631 public function renameTable(string $name, string $newName): void
632 {
633 $this->connection->executeStatement(
634 $this->platform->getRenameTableSQL($name, $newName),
635 );
636 }
637
638 /**
639 * Methods for filtering return values of list*() methods to convert
640 * the native DBMS data definition to a portable Doctrine definition
641 */
642
643 /** @param array<string, string> $database */
644 protected function _getPortableDatabaseDefinition(array $database): string
645 {
646 throw NotSupported::new(__METHOD__);
647 }
648
649 /** @param array<string, mixed> $sequence */
650 protected function _getPortableSequenceDefinition(array $sequence): Sequence
651 {
652 throw NotSupported::new(__METHOD__);
653 }
654
655 /**
656 * Independent of the database the keys of the column list result are lowercased.
657 *
658 * The name of the created column instance however is kept in its case.
659 *
660 * @param array<int, array<string, mixed>> $tableColumns
661 *
662 * @return array<string, Column>
663 *
664 * @throws Exception
665 */
666 protected function _getPortableTableColumnList(string $table, string $database, array $tableColumns): array
667 {
668 $list = [];
669 foreach ($tableColumns as $tableColumn) {
670 $column = $this->_getPortableTableColumnDefinition($tableColumn);
671
672 $name = strtolower($column->getQuotedName($this->platform));
673 $list[$name] = $column;
674 }
675
676 return $list;
677 }
678
679 /**
680 * Gets Table Column Definition.
681 *
682 * @param array<string, mixed> $tableColumn
683 *
684 * @throws Exception
685 */
686 abstract protected function _getPortableTableColumnDefinition(array $tableColumn): Column;
687
688 /**
689 * Aggregates and groups the index results according to the required data result.
690 *
691 * @param array<int, array<string, mixed>> $tableIndexes
692 *
693 * @return array<string, Index>
694 *
695 * @throws Exception
696 */
697 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
698 {
699 $result = [];
700 foreach ($tableIndexes as $tableIndex) {
701 $indexName = $keyName = $tableIndex['key_name'];
702 if ($tableIndex['primary']) {
703 $keyName = 'primary';
704 }
705
706 $keyName = strtolower($keyName);
707
708 if (! isset($result[$keyName])) {
709 $options = [
710 'lengths' => [],
711 ];
712
713 if (isset($tableIndex['where'])) {
714 $options['where'] = $tableIndex['where'];
715 }
716
717 $result[$keyName] = [
718 'name' => $indexName,
719 'columns' => [],
720 'unique' => ! $tableIndex['non_unique'],
721 'primary' => $tableIndex['primary'],
722 'flags' => $tableIndex['flags'] ?? [],
723 'options' => $options,
724 ];
725 }
726
727 $result[$keyName]['columns'][] = $tableIndex['column_name'];
728 $result[$keyName]['options']['lengths'][] = $tableIndex['length'] ?? null;
729 }
730
731 $indexes = [];
732 foreach ($result as $indexKey => $data) {
733 $indexes[$indexKey] = new Index(
734 $data['name'],
735 $data['columns'],
736 $data['unique'],
737 $data['primary'],
738 $data['flags'],
739 $data['options'],
740 );
741 }
742
743 return $indexes;
744 }
745
746 /** @param array<string, string> $table */
747 abstract protected function _getPortableTableDefinition(array $table): string;
748
749 /** @param array<string, mixed> $view */
750 abstract protected function _getPortableViewDefinition(array $view): View;
751
752 /**
753 * @param array<int|string, array<string, mixed>> $tableForeignKeys
754 *
755 * @return array<int, ForeignKeyConstraint>
756 */
757 protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array
758 {
759 $list = [];
760
761 foreach ($tableForeignKeys as $value) {
762 $list[] = $this->_getPortableTableForeignKeyDefinition($value);
763 }
764
765 return $list;
766 }
767
768 /** @param array<string, mixed> $tableForeignKey */
769 abstract protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint;
770
771 /**
772 * @param array<int, string> $sql
773 *
774 * @throws Exception
775 */
776 private function executeStatements(array $sql): void
777 {
778 foreach ($sql as $query) {
779 $this->connection->executeStatement($query);
780 }
781 }
782
783 /**
784 * Returns a {@see Schema} instance representing the current database schema.
785 *
786 * @throws Exception
787 */
788 public function introspectSchema(): Schema
789 {
790 $schemaNames = [];
791
792 if ($this->platform->supportsSchemas()) {
793 $schemaNames = $this->listSchemaNames();
794 }
795
796 $sequences = [];
797
798 if ($this->platform->supportsSequences()) {
799 $sequences = $this->listSequences();
800 }
801
802 $tables = $this->listTables();
803
804 return new Schema($tables, $sequences, $this->createSchemaConfig(), $schemaNames);
805 }
806
807 /**
808 * Creates the configuration for this schema.
809 *
810 * @throws Exception
811 */
812 public function createSchemaConfig(): SchemaConfig
813 {
814 $schemaConfig = new SchemaConfig();
815 $schemaConfig->setMaxIdentifierLength($this->platform->getMaxIdentifierLength());
816
817 $params = $this->connection->getParams();
818 if (! isset($params['defaultTableOptions'])) {
819 $params['defaultTableOptions'] = [];
820 }
821
822 if (! isset($params['defaultTableOptions']['charset']) && isset($params['charset'])) {
823 $params['defaultTableOptions']['charset'] = $params['charset'];
824 }
825
826 $schemaConfig->setDefaultTableOptions($params['defaultTableOptions']);
827
828 return $schemaConfig;
829 }
830
831 /** @throws Exception */
832 private function getDatabase(string $methodName): string
833 {
834 $database = $this->connection->getDatabase();
835
836 if ($database === null) {
837 throw DatabaseRequired::new($methodName);
838 }
839
840 return $database;
841 }
842
843 public function createComparator(): Comparator
844 {
845 return new Comparator($this->platform);
846 }
847
848 /**
849 * @return array<string,list<array<string,mixed>>>
850 *
851 * @throws Exception
852 */
853 private function fetchAllAssociativeGrouped(Result $result): array
854 {
855 $data = [];
856
857 foreach ($result->fetchAllAssociative() as $row) {
858 $tableName = $this->_getPortableTableDefinition($row);
859 $data[$tableName][] = $row;
860 }
861
862 return $data;
863 }
864}
diff --git a/vendor/doctrine/dbal/src/Schema/Column.php b/vendor/doctrine/dbal/src/Schema/Column.php
new file mode 100644
index 0000000..8963cd7
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Column.php
@@ -0,0 +1,252 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Schema\Exception\UnknownColumnOption;
8use Doctrine\DBAL\Types\Type;
9
10use function array_merge;
11use function method_exists;
12
13/**
14 * Object representation of a database column.
15 */
16class Column extends AbstractAsset
17{
18 protected Type $_type;
19
20 protected ?int $_length = null;
21
22 protected ?int $_precision = null;
23
24 protected int $_scale = 0;
25
26 protected bool $_unsigned = false;
27
28 protected bool $_fixed = false;
29
30 protected bool $_notnull = true;
31
32 protected mixed $_default = null;
33
34 protected bool $_autoincrement = false;
35
36 /** @var array<string, mixed> */
37 protected array $_platformOptions = [];
38
39 protected ?string $_columnDefinition = null;
40
41 protected string $_comment = '';
42
43 /**
44 * Creates a new Column.
45 *
46 * @param array<string, mixed> $options
47 */
48 public function __construct(string $name, Type $type, array $options = [])
49 {
50 $this->_setName($name);
51 $this->setType($type);
52 $this->setOptions($options);
53 }
54
55 /** @param array<string, mixed> $options */
56 public function setOptions(array $options): self
57 {
58 foreach ($options as $name => $value) {
59 $method = 'set' . $name;
60
61 if (! method_exists($this, $method)) {
62 throw UnknownColumnOption::new($name);
63 }
64
65 $this->$method($value);
66 }
67
68 return $this;
69 }
70
71 public function setType(Type $type): self
72 {
73 $this->_type = $type;
74
75 return $this;
76 }
77
78 public function setLength(?int $length): self
79 {
80 $this->_length = $length;
81
82 return $this;
83 }
84
85 public function setPrecision(?int $precision): self
86 {
87 $this->_precision = $precision;
88
89 return $this;
90 }
91
92 public function setScale(int $scale): self
93 {
94 $this->_scale = $scale;
95
96 return $this;
97 }
98
99 public function setUnsigned(bool $unsigned): self
100 {
101 $this->_unsigned = $unsigned;
102
103 return $this;
104 }
105
106 public function setFixed(bool $fixed): self
107 {
108 $this->_fixed = $fixed;
109
110 return $this;
111 }
112
113 public function setNotnull(bool $notnull): self
114 {
115 $this->_notnull = $notnull;
116
117 return $this;
118 }
119
120 public function setDefault(mixed $default): self
121 {
122 $this->_default = $default;
123
124 return $this;
125 }
126
127 /** @param array<string, mixed> $platformOptions */
128 public function setPlatformOptions(array $platformOptions): self
129 {
130 $this->_platformOptions = $platformOptions;
131
132 return $this;
133 }
134
135 public function setPlatformOption(string $name, mixed $value): self
136 {
137 $this->_platformOptions[$name] = $value;
138
139 return $this;
140 }
141
142 public function setColumnDefinition(?string $value): self
143 {
144 $this->_columnDefinition = $value;
145
146 return $this;
147 }
148
149 public function getType(): Type
150 {
151 return $this->_type;
152 }
153
154 public function getLength(): ?int
155 {
156 return $this->_length;
157 }
158
159 public function getPrecision(): ?int
160 {
161 return $this->_precision;
162 }
163
164 public function getScale(): int
165 {
166 return $this->_scale;
167 }
168
169 public function getUnsigned(): bool
170 {
171 return $this->_unsigned;
172 }
173
174 public function getFixed(): bool
175 {
176 return $this->_fixed;
177 }
178
179 public function getNotnull(): bool
180 {
181 return $this->_notnull;
182 }
183
184 public function getDefault(): mixed
185 {
186 return $this->_default;
187 }
188
189 /** @return array<string, mixed> */
190 public function getPlatformOptions(): array
191 {
192 return $this->_platformOptions;
193 }
194
195 public function hasPlatformOption(string $name): bool
196 {
197 return isset($this->_platformOptions[$name]);
198 }
199
200 public function getPlatformOption(string $name): mixed
201 {
202 return $this->_platformOptions[$name];
203 }
204
205 public function getColumnDefinition(): ?string
206 {
207 return $this->_columnDefinition;
208 }
209
210 public function getAutoincrement(): bool
211 {
212 return $this->_autoincrement;
213 }
214
215 public function setAutoincrement(bool $flag): self
216 {
217 $this->_autoincrement = $flag;
218
219 return $this;
220 }
221
222 public function setComment(string $comment): self
223 {
224 $this->_comment = $comment;
225
226 return $this;
227 }
228
229 public function getComment(): string
230 {
231 return $this->_comment;
232 }
233
234 /** @return array<string, mixed> */
235 public function toArray(): array
236 {
237 return array_merge([
238 'name' => $this->_name,
239 'type' => $this->_type,
240 'default' => $this->_default,
241 'notnull' => $this->_notnull,
242 'length' => $this->_length,
243 'precision' => $this->_precision,
244 'scale' => $this->_scale,
245 'fixed' => $this->_fixed,
246 'unsigned' => $this->_unsigned,
247 'autoincrement' => $this->_autoincrement,
248 'columnDefinition' => $this->_columnDefinition,
249 'comment' => $this->_comment,
250 ], $this->_platformOptions);
251 }
252}
diff --git a/vendor/doctrine/dbal/src/Schema/ColumnDiff.php b/vendor/doctrine/dbal/src/Schema/ColumnDiff.php
new file mode 100644
index 0000000..3e4950a
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/ColumnDiff.php
@@ -0,0 +1,106 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7/**
8 * Represents the change of a column.
9 */
10class ColumnDiff
11{
12 /** @internal The diff can be only instantiated by a {@see Comparator}. */
13 public function __construct(private readonly Column $oldColumn, private readonly Column $newColumn)
14 {
15 }
16
17 public function getOldColumn(): Column
18 {
19 return $this->oldColumn;
20 }
21
22 public function getNewColumn(): Column
23 {
24 return $this->newColumn;
25 }
26
27 public function hasTypeChanged(): bool
28 {
29 return $this->newColumn->getType()::class !== $this->oldColumn->getType()::class;
30 }
31
32 public function hasLengthChanged(): bool
33 {
34 return $this->hasPropertyChanged(static function (Column $column): ?int {
35 return $column->getLength();
36 });
37 }
38
39 public function hasPrecisionChanged(): bool
40 {
41 return $this->hasPropertyChanged(static function (Column $column): ?int {
42 return $column->getPrecision();
43 });
44 }
45
46 public function hasScaleChanged(): bool
47 {
48 return $this->hasPropertyChanged(static function (Column $column): int {
49 return $column->getScale();
50 });
51 }
52
53 public function hasUnsignedChanged(): bool
54 {
55 return $this->hasPropertyChanged(static function (Column $column): bool {
56 return $column->getUnsigned();
57 });
58 }
59
60 public function hasFixedChanged(): bool
61 {
62 return $this->hasPropertyChanged(static function (Column $column): bool {
63 return $column->getFixed();
64 });
65 }
66
67 public function hasNotNullChanged(): bool
68 {
69 return $this->hasPropertyChanged(static function (Column $column): bool {
70 return $column->getNotnull();
71 });
72 }
73
74 public function hasDefaultChanged(): bool
75 {
76 $oldDefault = $this->oldColumn->getDefault();
77 $newDefault = $this->newColumn->getDefault();
78
79 // Null values need to be checked additionally as they tell whether to create or drop a default value.
80 // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation.
81 if (($newDefault === null) xor ($oldDefault === null)) {
82 return true;
83 }
84
85 return $newDefault != $oldDefault;
86 }
87
88 public function hasAutoIncrementChanged(): bool
89 {
90 return $this->hasPropertyChanged(static function (Column $column): bool {
91 return $column->getAutoincrement();
92 });
93 }
94
95 public function hasCommentChanged(): bool
96 {
97 return $this->hasPropertyChanged(static function (Column $column): string {
98 return $column->getComment();
99 });
100 }
101
102 private function hasPropertyChanged(callable $property): bool
103 {
104 return $property($this->newColumn) !== $property($this->oldColumn);
105 }
106}
diff --git a/vendor/doctrine/dbal/src/Schema/Comparator.php b/vendor/doctrine/dbal/src/Schema/Comparator.php
new file mode 100644
index 0000000..4c60c07
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Comparator.php
@@ -0,0 +1,417 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8
9use function array_map;
10use function assert;
11use function count;
12use function strtolower;
13
14/**
15 * Compares two Schemas and return an instance of SchemaDiff.
16 */
17class Comparator
18{
19 /** @internal The comparator can be only instantiated by a schema manager. */
20 public function __construct(private readonly AbstractPlatform $platform)
21 {
22 }
23
24 /**
25 * Returns the differences between the schemas.
26 */
27 public function compareSchemas(Schema $oldSchema, Schema $newSchema): SchemaDiff
28 {
29 $createdSchemas = [];
30 $droppedSchemas = [];
31 $createdTables = [];
32 $alteredTables = [];
33 $droppedTables = [];
34 $createdSequences = [];
35 $alteredSequences = [];
36 $droppedSequences = [];
37
38 foreach ($newSchema->getNamespaces() as $newNamespace) {
39 if ($oldSchema->hasNamespace($newNamespace)) {
40 continue;
41 }
42
43 $createdSchemas[] = $newNamespace;
44 }
45
46 foreach ($oldSchema->getNamespaces() as $oldNamespace) {
47 if ($newSchema->hasNamespace($oldNamespace)) {
48 continue;
49 }
50
51 $droppedSchemas[] = $oldNamespace;
52 }
53
54 foreach ($newSchema->getTables() as $newTable) {
55 $newTableName = $newTable->getShortestName($newSchema->getName());
56 if (! $oldSchema->hasTable($newTableName)) {
57 $createdTables[] = $newSchema->getTable($newTableName);
58 } else {
59 $tableDiff = $this->compareTables(
60 $oldSchema->getTable($newTableName),
61 $newSchema->getTable($newTableName),
62 );
63
64 if (! $tableDiff->isEmpty()) {
65 $alteredTables[] = $tableDiff;
66 }
67 }
68 }
69
70 // Check if there are tables removed
71 foreach ($oldSchema->getTables() as $oldTable) {
72 $oldTableName = $oldTable->getShortestName($oldSchema->getName());
73
74 $oldTable = $oldSchema->getTable($oldTableName);
75 if ($newSchema->hasTable($oldTableName)) {
76 continue;
77 }
78
79 $droppedTables[] = $oldTable;
80 }
81
82 foreach ($newSchema->getSequences() as $newSequence) {
83 $newSequenceName = $newSequence->getShortestName($newSchema->getName());
84 if (! $oldSchema->hasSequence($newSequenceName)) {
85 if (! $this->isAutoIncrementSequenceInSchema($oldSchema, $newSequence)) {
86 $createdSequences[] = $newSequence;
87 }
88 } else {
89 if ($this->diffSequence($newSequence, $oldSchema->getSequence($newSequenceName))) {
90 $alteredSequences[] = $newSchema->getSequence($newSequenceName);
91 }
92 }
93 }
94
95 foreach ($oldSchema->getSequences() as $oldSequence) {
96 if ($this->isAutoIncrementSequenceInSchema($newSchema, $oldSequence)) {
97 continue;
98 }
99
100 $oldSequenceName = $oldSequence->getShortestName($oldSchema->getName());
101
102 if ($newSchema->hasSequence($oldSequenceName)) {
103 continue;
104 }
105
106 $droppedSequences[] = $oldSequence;
107 }
108
109 return new SchemaDiff(
110 $createdSchemas,
111 $droppedSchemas,
112 $createdTables,
113 $alteredTables,
114 $droppedTables,
115 $createdSequences,
116 $alteredSequences,
117 $droppedSequences,
118 );
119 }
120
121 private function isAutoIncrementSequenceInSchema(Schema $schema, Sequence $sequence): bool
122 {
123 foreach ($schema->getTables() as $table) {
124 if ($sequence->isAutoIncrementsFor($table)) {
125 return true;
126 }
127 }
128
129 return false;
130 }
131
132 public function diffSequence(Sequence $sequence1, Sequence $sequence2): bool
133 {
134 if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) {
135 return true;
136 }
137
138 return $sequence1->getInitialValue() !== $sequence2->getInitialValue();
139 }
140
141 /**
142 * Compares the tables and returns the difference between them.
143 */
144 public function compareTables(Table $oldTable, Table $newTable): TableDiff
145 {
146 $addedColumns = [];
147 $modifiedColumns = [];
148 $droppedColumns = [];
149 $addedIndexes = [];
150 $modifiedIndexes = [];
151 $droppedIndexes = [];
152 $addedForeignKeys = [];
153 $modifiedForeignKeys = [];
154 $droppedForeignKeys = [];
155
156 $oldColumns = $oldTable->getColumns();
157 $newColumns = $newTable->getColumns();
158
159 // See if all the columns in the old table exist in the new table
160 foreach ($newColumns as $newColumn) {
161 $newColumnName = strtolower($newColumn->getName());
162
163 if ($oldTable->hasColumn($newColumnName)) {
164 continue;
165 }
166
167 $addedColumns[$newColumnName] = $newColumn;
168 }
169
170 // See if there are any removed columns in the new table
171 foreach ($oldColumns as $oldColumn) {
172 $oldColumnName = strtolower($oldColumn->getName());
173
174 // See if column is removed in the new table.
175 if (! $newTable->hasColumn($oldColumnName)) {
176 $droppedColumns[$oldColumnName] = $oldColumn;
177
178 continue;
179 }
180
181 $newColumn = $newTable->getColumn($oldColumnName);
182
183 if ($this->columnsEqual($oldColumn, $newColumn)) {
184 continue;
185 }
186
187 $modifiedColumns[] = new ColumnDiff($oldColumn, $newColumn);
188 }
189
190 $renamedColumns = $this->detectRenamedColumns($addedColumns, $droppedColumns);
191
192 $oldIndexes = $oldTable->getIndexes();
193 $newIndexes = $newTable->getIndexes();
194
195 // See if all the indexes from the old table exist in the new one
196 foreach ($newIndexes as $newIndexName => $newIndex) {
197 if (($newIndex->isPrimary() && $oldTable->getPrimaryKey() !== null) || $oldTable->hasIndex($newIndexName)) {
198 continue;
199 }
200
201 $addedIndexes[$newIndexName] = $newIndex;
202 }
203
204 // See if there are any removed indexes in the new table
205 foreach ($oldIndexes as $oldIndexName => $oldIndex) {
206 // See if the index is removed in the new table.
207 if (
208 ($oldIndex->isPrimary() && $newTable->getPrimaryKey() === null) ||
209 ! $oldIndex->isPrimary() && ! $newTable->hasIndex($oldIndexName)
210 ) {
211 $droppedIndexes[$oldIndexName] = $oldIndex;
212
213 continue;
214 }
215
216 // See if index has changed in the new table.
217 $newIndex = $oldIndex->isPrimary() ? $newTable->getPrimaryKey() : $newTable->getIndex($oldIndexName);
218 assert($newIndex instanceof Index);
219
220 if (! $this->diffIndex($oldIndex, $newIndex)) {
221 continue;
222 }
223
224 $modifiedIndexes[] = $newIndex;
225 }
226
227 $renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes);
228
229 $oldForeignKeys = $oldTable->getForeignKeys();
230 $newForeignKeys = $newTable->getForeignKeys();
231
232 foreach ($oldForeignKeys as $oldKey => $oldForeignKey) {
233 foreach ($newForeignKeys as $newKey => $newForeignKey) {
234 if ($this->diffForeignKey($oldForeignKey, $newForeignKey) === false) {
235 unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
236 } else {
237 if (strtolower($oldForeignKey->getName()) === strtolower($newForeignKey->getName())) {
238 $modifiedForeignKeys[] = $newForeignKey;
239
240 unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]);
241 }
242 }
243 }
244 }
245
246 foreach ($oldForeignKeys as $oldForeignKey) {
247 $droppedForeignKeys[] = $oldForeignKey;
248 }
249
250 foreach ($newForeignKeys as $newForeignKey) {
251 $addedForeignKeys[] = $newForeignKey;
252 }
253
254 return new TableDiff(
255 $oldTable,
256 $addedColumns,
257 $modifiedColumns,
258 $droppedColumns,
259 $renamedColumns,
260 $addedIndexes,
261 $modifiedIndexes,
262 $droppedIndexes,
263 $renamedIndexes,
264 $addedForeignKeys,
265 $modifiedForeignKeys,
266 $droppedForeignKeys,
267 );
268 }
269
270 /**
271 * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
272 * however ambiguities between different possibilities should not lead to renaming at all.
273 *
274 * @param array<string,Column> $addedColumns
275 * @param array<string,Column> $removedColumns
276 *
277 * @return array<string,Column>
278 */
279 private function detectRenamedColumns(array &$addedColumns, array &$removedColumns): array
280 {
281 $candidatesByName = [];
282
283 foreach ($addedColumns as $addedColumnName => $addedColumn) {
284 foreach ($removedColumns as $removedColumn) {
285 if (! $this->columnsEqual($addedColumn, $removedColumn)) {
286 continue;
287 }
288
289 $candidatesByName[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName];
290 }
291 }
292
293 $renamedColumns = [];
294
295 foreach ($candidatesByName as $candidates) {
296 if (count($candidates) !== 1) {
297 continue;
298 }
299
300 [$removedColumn, $addedColumn] = $candidates[0];
301 $removedColumnName = $removedColumn->getName();
302 $addedColumnName = strtolower($addedColumn->getName());
303
304 if (isset($renamedColumns[$removedColumnName])) {
305 continue;
306 }
307
308 $renamedColumns[$removedColumnName] = $addedColumn;
309 unset(
310 $addedColumns[$addedColumnName],
311 $removedColumns[strtolower($removedColumnName)],
312 );
313 }
314
315 return $renamedColumns;
316 }
317
318 /**
319 * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
320 * however ambiguities between different possibilities should not lead to renaming at all.
321 *
322 * @param array<string,Index> $addedIndexes
323 * @param array<string,Index> $removedIndexes
324 *
325 * @return array<string,Index>
326 */
327 private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array
328 {
329 $candidatesByName = [];
330
331 // Gather possible rename candidates by comparing each added and removed index based on semantics.
332 foreach ($addedIndexes as $addedIndexName => $addedIndex) {
333 foreach ($removedIndexes as $removedIndex) {
334 if ($this->diffIndex($addedIndex, $removedIndex)) {
335 continue;
336 }
337
338 $candidatesByName[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName];
339 }
340 }
341
342 $renamedIndexes = [];
343
344 foreach ($candidatesByName as $candidates) {
345 // If the current rename candidate contains exactly one semantically equal index,
346 // we can safely rename it.
347 // Otherwise, it is unclear if a rename action is really intended,
348 // therefore we let those ambiguous indexes be added/dropped.
349 if (count($candidates) !== 1) {
350 continue;
351 }
352
353 [$removedIndex, $addedIndex] = $candidates[0];
354
355 $removedIndexName = strtolower($removedIndex->getName());
356 $addedIndexName = strtolower($addedIndex->getName());
357
358 if (isset($renamedIndexes[$removedIndexName])) {
359 continue;
360 }
361
362 $renamedIndexes[$removedIndexName] = $addedIndex;
363 unset(
364 $addedIndexes[$addedIndexName],
365 $removedIndexes[$removedIndexName],
366 );
367 }
368
369 return $renamedIndexes;
370 }
371
372 protected function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2): bool
373 {
374 if (
375 array_map('strtolower', $key1->getUnquotedLocalColumns())
376 !== array_map('strtolower', $key2->getUnquotedLocalColumns())
377 ) {
378 return true;
379 }
380
381 if (
382 array_map('strtolower', $key1->getUnquotedForeignColumns())
383 !== array_map('strtolower', $key2->getUnquotedForeignColumns())
384 ) {
385 return true;
386 }
387
388 if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) {
389 return true;
390 }
391
392 if ($key1->onUpdate() !== $key2->onUpdate()) {
393 return true;
394 }
395
396 return $key1->onDelete() !== $key2->onDelete();
397 }
398
399 /**
400 * Compares the definitions of the given columns
401 */
402 protected function columnsEqual(Column $column1, Column $column2): bool
403 {
404 return $this->platform->columnsEqual($column1, $column2);
405 }
406
407 /**
408 * Finds the difference between the indexes $index1 and $index2.
409 *
410 * Compares $index1 with $index2 and returns true if there are any
411 * differences or false in case there are no differences.
412 */
413 protected function diffIndex(Index $index1, Index $index2): bool
414 {
415 return ! ($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1));
416 }
417}
diff --git a/vendor/doctrine/dbal/src/Schema/DB2SchemaManager.php b/vendor/doctrine/dbal/src/Schema/DB2SchemaManager.php
new file mode 100644
index 0000000..f2ed089
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/DB2SchemaManager.php
@@ -0,0 +1,371 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\DB2Platform;
9use Doctrine\DBAL\Result;
10use Doctrine\DBAL\Types\Type;
11use Doctrine\DBAL\Types\Types;
12
13use function array_change_key_case;
14use function implode;
15use function preg_match;
16use function str_replace;
17use function strpos;
18use function strtolower;
19use function strtoupper;
20use function substr;
21
22use const CASE_LOWER;
23
24/**
25 * IBM Db2 Schema Manager.
26 *
27 * @extends AbstractSchemaManager<DB2Platform>
28 */
29class DB2SchemaManager extends AbstractSchemaManager
30{
31 /**
32 * {@inheritDoc}
33 *
34 * @throws Exception
35 */
36 protected function _getPortableTableColumnDefinition(array $tableColumn): Column
37 {
38 $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
39
40 $length = $precision = $default = null;
41 $scale = 0;
42 $fixed = false;
43
44 if ($tableColumn['default'] !== null && $tableColumn['default'] !== 'NULL') {
45 $default = $tableColumn['default'];
46
47 if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) {
48 $default = str_replace("''", "'", $matches[1]);
49 }
50 }
51
52 $type = $this->platform->getDoctrineTypeMapping($tableColumn['typename']);
53
54 switch (strtolower($tableColumn['typename'])) {
55 case 'varchar':
56 if ($tableColumn['codepage'] === 0) {
57 $type = Types::BINARY;
58 }
59
60 $length = $tableColumn['length'];
61 break;
62
63 case 'character':
64 if ($tableColumn['codepage'] === 0) {
65 $type = Types::BINARY;
66 }
67
68 $length = $tableColumn['length'];
69 $fixed = true;
70 break;
71
72 case 'clob':
73 $length = $tableColumn['length'];
74 break;
75
76 case 'decimal':
77 case 'double':
78 case 'real':
79 $scale = $tableColumn['scale'];
80 $precision = $tableColumn['length'];
81 break;
82 }
83
84 $options = [
85 'length' => $length,
86 'unsigned' => false,
87 'fixed' => $fixed,
88 'default' => $default,
89 'autoincrement' => (bool) $tableColumn['autoincrement'],
90 'notnull' => $tableColumn['nulls'] === 'N',
91 'platformOptions' => [],
92 ];
93
94 if (isset($tableColumn['comment'])) {
95 $options['comment'] = $tableColumn['comment'];
96 }
97
98 if ($scale !== null && $precision !== null) {
99 $options['scale'] = $scale;
100 $options['precision'] = $precision;
101 }
102
103 return new Column($tableColumn['colname'], Type::getType($type), $options);
104 }
105
106 /**
107 * {@inheritDoc}
108 */
109 protected function _getPortableTableDefinition(array $table): string
110 {
111 $table = array_change_key_case($table, CASE_LOWER);
112
113 return $table['name'];
114 }
115
116 /**
117 * {@inheritDoc}
118 */
119 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
120 {
121 foreach ($tableIndexes as &$tableIndexRow) {
122 $tableIndexRow = array_change_key_case($tableIndexRow, CASE_LOWER);
123 $tableIndexRow['primary'] = (bool) $tableIndexRow['primary'];
124 }
125
126 return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
127 }
128
129 /**
130 * {@inheritDoc}
131 */
132 protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
133 {
134 return new ForeignKeyConstraint(
135 $tableForeignKey['local_columns'],
136 $tableForeignKey['foreign_table'],
137 $tableForeignKey['foreign_columns'],
138 $tableForeignKey['name'],
139 $tableForeignKey['options'],
140 );
141 }
142
143 /**
144 * {@inheritDoc}
145 */
146 protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array
147 {
148 $foreignKeys = [];
149
150 foreach ($tableForeignKeys as $tableForeignKey) {
151 $tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER);
152
153 if (! isset($foreignKeys[$tableForeignKey['index_name']])) {
154 $foreignKeys[$tableForeignKey['index_name']] = [
155 'local_columns' => [$tableForeignKey['local_column']],
156 'foreign_table' => $tableForeignKey['foreign_table'],
157 'foreign_columns' => [$tableForeignKey['foreign_column']],
158 'name' => $tableForeignKey['index_name'],
159 'options' => [
160 'onUpdate' => $tableForeignKey['on_update'],
161 'onDelete' => $tableForeignKey['on_delete'],
162 ],
163 ];
164 } else {
165 $foreignKeys[$tableForeignKey['index_name']]['local_columns'][] = $tableForeignKey['local_column'];
166 $foreignKeys[$tableForeignKey['index_name']]['foreign_columns'][] = $tableForeignKey['foreign_column'];
167 }
168 }
169
170 return parent::_getPortableTableForeignKeysList($foreignKeys);
171 }
172
173 /**
174 * {@inheritDoc}
175 */
176 protected function _getPortableViewDefinition(array $view): View
177 {
178 $view = array_change_key_case($view, CASE_LOWER);
179
180 $sql = '';
181 $pos = strpos($view['text'], ' AS ');
182
183 if ($pos !== false) {
184 $sql = substr($view['text'], $pos + 4);
185 }
186
187 return new View($view['name'], $sql);
188 }
189
190 protected function normalizeName(string $name): string
191 {
192 $identifier = new Identifier($name);
193
194 return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name);
195 }
196
197 protected function selectTableNames(string $databaseName): Result
198 {
199 $sql = <<<'SQL'
200SELECT NAME
201FROM SYSIBM.SYSTABLES
202WHERE TYPE = 'T'
203 AND CREATOR = ?
204SQL;
205
206 return $this->connection->executeQuery($sql, [$databaseName]);
207 }
208
209 protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
210 {
211 $sql = 'SELECT';
212
213 if ($tableName === null) {
214 $sql .= ' C.TABNAME AS NAME,';
215 }
216
217 $sql .= <<<'SQL'
218 C.COLNAME,
219 C.TYPENAME,
220 C.CODEPAGE,
221 C.NULLS,
222 C.LENGTH,
223 C.SCALE,
224 C.REMARKS AS COMMENT,
225 CASE
226 WHEN C.GENERATED = 'D' THEN 1
227 ELSE 0
228 END AS AUTOINCREMENT,
229 C.DEFAULT
230FROM SYSCAT.COLUMNS C
231 JOIN SYSCAT.TABLES AS T
232 ON T.TABSCHEMA = C.TABSCHEMA
233 AND T.TABNAME = C.TABNAME
234SQL;
235
236 $conditions = ['C.TABSCHEMA = ?', "T.TYPE = 'T'"];
237 $params = [$databaseName];
238
239 if ($tableName !== null) {
240 $conditions[] = 'C.TABNAME = ?';
241 $params[] = $tableName;
242 }
243
244 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.TABNAME, C.COLNO';
245
246 return $this->connection->executeQuery($sql, $params);
247 }
248
249 protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
250 {
251 $sql = 'SELECT';
252
253 if ($tableName === null) {
254 $sql .= ' IDX.TABNAME AS NAME,';
255 }
256
257 $sql .= <<<'SQL'
258 IDX.INDNAME AS KEY_NAME,
259 IDXCOL.COLNAME AS COLUMN_NAME,
260 CASE
261 WHEN IDX.UNIQUERULE = 'P' THEN 1
262 ELSE 0
263 END AS PRIMARY,
264 CASE
265 WHEN IDX.UNIQUERULE = 'D' THEN 1
266 ELSE 0
267 END AS NON_UNIQUE
268 FROM SYSCAT.INDEXES AS IDX
269 JOIN SYSCAT.TABLES AS T
270 ON IDX.TABSCHEMA = T.TABSCHEMA AND IDX.TABNAME = T.TABNAME
271 JOIN SYSCAT.INDEXCOLUSE AS IDXCOL
272 ON IDX.INDSCHEMA = IDXCOL.INDSCHEMA AND IDX.INDNAME = IDXCOL.INDNAME
273SQL;
274
275 $conditions = ['IDX.TABSCHEMA = ?', "T.TYPE = 'T'"];
276 $params = [$databaseName];
277
278 if ($tableName !== null) {
279 $conditions[] = 'IDX.TABNAME = ?';
280 $params[] = $tableName;
281 }
282
283 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IDX.INDNAME, IDXCOL.COLSEQ';
284
285 return $this->connection->executeQuery($sql, $params);
286 }
287
288 protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
289 {
290 $sql = 'SELECT';
291
292 if ($tableName === null) {
293 $sql .= ' R.TABNAME AS NAME,';
294 }
295
296 $sql .= <<<'SQL'
297 FKCOL.COLNAME AS LOCAL_COLUMN,
298 R.REFTABNAME AS FOREIGN_TABLE,
299 PKCOL.COLNAME AS FOREIGN_COLUMN,
300 R.CONSTNAME AS INDEX_NAME,
301 CASE
302 WHEN R.UPDATERULE = 'R' THEN 'RESTRICT'
303 END AS ON_UPDATE,
304 CASE
305 WHEN R.DELETERULE = 'C' THEN 'CASCADE'
306 WHEN R.DELETERULE = 'N' THEN 'SET NULL'
307 WHEN R.DELETERULE = 'R' THEN 'RESTRICT'
308 END AS ON_DELETE
309 FROM SYSCAT.REFERENCES AS R
310 JOIN SYSCAT.TABLES AS T
311 ON T.TABSCHEMA = R.TABSCHEMA
312 AND T.TABNAME = R.TABNAME
313 JOIN SYSCAT.KEYCOLUSE AS FKCOL
314 ON FKCOL.CONSTNAME = R.CONSTNAME
315 AND FKCOL.TABSCHEMA = R.TABSCHEMA
316 AND FKCOL.TABNAME = R.TABNAME
317 JOIN SYSCAT.KEYCOLUSE AS PKCOL
318 ON PKCOL.CONSTNAME = R.REFKEYNAME
319 AND PKCOL.TABSCHEMA = R.REFTABSCHEMA
320 AND PKCOL.TABNAME = R.REFTABNAME
321 AND PKCOL.COLSEQ = FKCOL.COLSEQ
322SQL;
323
324 $conditions = ['R.TABSCHEMA = ?', "T.TYPE = 'T'"];
325 $params = [$databaseName];
326
327 if ($tableName !== null) {
328 $conditions[] = 'R.TABNAME = ?';
329 $params[] = $tableName;
330 }
331
332 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY R.CONSTNAME, FKCOL.COLSEQ';
333
334 return $this->connection->executeQuery($sql, $params);
335 }
336
337 /**
338 * {@inheritDoc}
339 */
340 protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
341 {
342 $sql = 'SELECT NAME, REMARKS';
343
344 $conditions = [];
345 $params = [];
346
347 if ($tableName !== null) {
348 $conditions[] = 'NAME = ?';
349 $params[] = $tableName;
350 }
351
352 $sql .= ' FROM SYSIBM.SYSTABLES';
353
354 if ($conditions !== []) {
355 $sql .= ' WHERE ' . implode(' AND ', $conditions);
356 }
357
358 /** @var array<string,array<string,mixed>> $metadata */
359 $metadata = $this->connection->executeQuery($sql, $params)
360 ->fetchAllAssociativeIndexed();
361
362 $tableOptions = [];
363 foreach ($metadata as $table => $data) {
364 $data = array_change_key_case($data, CASE_LOWER);
365
366 $tableOptions[$table] = ['comment' => $data['remarks']];
367 }
368
369 return $tableOptions;
370 }
371}
diff --git a/vendor/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php b/vendor/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php
new file mode 100644
index 0000000..efba87f
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php
@@ -0,0 +1,20 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Exception;
9
10/**
11 * A schema manager factory that returns the default schema manager for the given platform.
12 */
13final class DefaultSchemaManagerFactory implements SchemaManagerFactory
14{
15 /** @throws Exception If the platform does not support creating schema managers yet. */
16 public function createSchemaManager(Connection $connection): AbstractSchemaManager
17 {
18 return $connection->getDatabasePlatform()->createSchemaManager($connection);
19 }
20}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php b/vendor/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php
new file mode 100644
index 0000000..53daac9
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class ColumnAlreadyExists extends LogicException implements SchemaException
14{
15 public static function new(string $tableName, string $columnName): self
16 {
17 return new self(sprintf('The column "%s" on table "%s" already exists.', $columnName, $tableName));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php b/vendor/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php
new file mode 100644
index 0000000..cb1cedf
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class ColumnDoesNotExist extends LogicException implements SchemaException
14{
15 public static function new(string $columnName, string $table): self
16 {
17 return new self(sprintf('There is no column with name "%s" on table "%s".', $columnName, $table));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php b/vendor/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php
new file mode 100644
index 0000000..3e4c6c6
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class ForeignKeyDoesNotExist extends LogicException implements SchemaException
14{
15 public static function new(string $foreignKeyName, string $table): self
16 {
17 return new self(
18 sprintf('There exists no foreign key with the name "%s" on table "%s".', $foreignKeyName, $table),
19 );
20 }
21}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php b/vendor/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php
new file mode 100644
index 0000000..e8592be
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class IndexAlreadyExists extends LogicException implements SchemaException
14{
15 public static function new(string $indexName, string $table): self
16 {
17 return new self(
18 sprintf('An index with name "%s" was already defined on table "%s".', $indexName, $table),
19 );
20 }
21}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php b/vendor/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php
new file mode 100644
index 0000000..ee25fd8
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class IndexDoesNotExist extends LogicException implements SchemaException
14{
15 public static function new(string $indexName, string $table): self
16 {
17 return new self(sprintf('Index "%s" does not exist on table "%s".', $indexName, $table));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php b/vendor/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php
new file mode 100644
index 0000000..d295eb8
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use InvalidArgumentException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class IndexNameInvalid extends InvalidArgumentException implements SchemaException
14{
15 public static function new(string $indexName): self
16 {
17 return new self(sprintf('Invalid index name "%s" given, has to be [a-zA-Z0-9_].', $indexName));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/InvalidTableName.php b/vendor/doctrine/dbal/src/Schema/Exception/InvalidTableName.php
new file mode 100644
index 0000000..3b5d89d
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/InvalidTableName.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use InvalidArgumentException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class InvalidTableName extends InvalidArgumentException implements SchemaException
14{
15 public static function new(string $tableName): self
16 {
17 return new self(sprintf('Invalid table name specified "%s".', $tableName));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php b/vendor/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php
new file mode 100644
index 0000000..4109af5
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class NamespaceAlreadyExists extends LogicException implements SchemaException
14{
15 public static function new(string $namespaceName): self
16 {
17 return new self(sprintf('The namespace with name "%s" already exists.', $namespaceName));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php b/vendor/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php
new file mode 100644
index 0000000..d374f27
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class SequenceAlreadyExists extends LogicException implements SchemaException
14{
15 public static function new(string $sequenceName): self
16 {
17 return new self(sprintf('The sequence "%s" already exists.', $sequenceName));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php b/vendor/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php
new file mode 100644
index 0000000..fa98cee
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class SequenceDoesNotExist extends LogicException implements SchemaException
14{
15 public static function new(string $sequenceName): self
16 {
17 return new self(sprintf('There exists no sequence with the name "%s".', $sequenceName));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php b/vendor/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php
new file mode 100644
index 0000000..b978dbc
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class TableAlreadyExists extends LogicException implements SchemaException
14{
15 public static function new(string $tableName): self
16 {
17 return new self(sprintf('The table with name "%s" already exists.', $tableName));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php b/vendor/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php
new file mode 100644
index 0000000..8c66a11
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php
@@ -0,0 +1,19 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class TableDoesNotExist extends LogicException implements SchemaException
14{
15 public static function new(string $tableName): self
16 {
17 return new self(sprintf('There is no table with name "%s" in the schema.', $tableName));
18 }
19}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php b/vendor/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php
new file mode 100644
index 0000000..3ae5aaa
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use LogicException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class UniqueConstraintDoesNotExist extends LogicException implements SchemaException
14{
15 public static function new(string $constraintName, string $table): self
16 {
17 return new self(
18 sprintf('There exists no unique constraint with the name "%s" on table "%s".', $constraintName, $table),
19 );
20 }
21}
diff --git a/vendor/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php b/vendor/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php
new file mode 100644
index 0000000..a97ee76
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema\Exception;
6
7use Doctrine\DBAL\Schema\SchemaException;
8use InvalidArgumentException;
9
10use function sprintf;
11
12/** @psalm-immutable */
13final class UnknownColumnOption extends InvalidArgumentException implements SchemaException
14{
15 public static function new(string $name): self
16 {
17 return new self(
18 sprintf('The "%s" column option is not supported.', $name),
19 );
20 }
21}
diff --git a/vendor/doctrine/dbal/src/Schema/ForeignKeyConstraint.php b/vendor/doctrine/dbal/src/Schema/ForeignKeyConstraint.php
new file mode 100644
index 0000000..bb5ef7f
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/ForeignKeyConstraint.php
@@ -0,0 +1,291 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8
9use function array_keys;
10use function array_map;
11use function strrpos;
12use function strtolower;
13use function strtoupper;
14use function substr;
15
16/**
17 * An abstraction class for a foreign key constraint.
18 */
19class ForeignKeyConstraint extends AbstractAsset
20{
21 /**
22 * Asset identifier instances of the referencing table column names the foreign key constraint is associated with.
23 *
24 * @var array<string, Identifier>
25 */
26 protected array $_localColumnNames;
27
28 /**
29 * Table or asset identifier instance of the referenced table name the foreign key constraint is associated with.
30 */
31 protected Identifier $_foreignTableName;
32
33 /**
34 * Asset identifier instances of the referenced table column names the foreign key constraint is associated with.
35 *
36 * @var array<string, Identifier>
37 */
38 protected array $_foreignColumnNames;
39
40 /**
41 * Initializes the foreign key constraint.
42 *
43 * @param array<int, string> $localColumnNames Names of the referencing table columns.
44 * @param string $foreignTableName Referenced table.
45 * @param array<int, string> $foreignColumnNames Names of the referenced table columns.
46 * @param string $name Name of the foreign key constraint.
47 * @param array<string, mixed> $options Options associated with the foreign key constraint.
48 */
49 public function __construct(
50 array $localColumnNames,
51 string $foreignTableName,
52 array $foreignColumnNames,
53 string $name = '',
54 protected array $options = [],
55 ) {
56 $this->_setName($name);
57
58 $this->_localColumnNames = $this->createIdentifierMap($localColumnNames);
59 $this->_foreignTableName = new Identifier($foreignTableName);
60
61 $this->_foreignColumnNames = $this->createIdentifierMap($foreignColumnNames);
62 }
63
64 /**
65 * @param array<int, string> $names
66 *
67 * @return array<string, Identifier>
68 */
69 private function createIdentifierMap(array $names): array
70 {
71 $identifiers = [];
72
73 foreach ($names as $name) {
74 $identifiers[$name] = new Identifier($name);
75 }
76
77 return $identifiers;
78 }
79
80 /**
81 * Returns the names of the referencing table columns
82 * the foreign key constraint is associated with.
83 *
84 * @return array<int, string>
85 */
86 public function getLocalColumns(): array
87 {
88 return array_keys($this->_localColumnNames);
89 }
90
91 /**
92 * Returns the quoted representation of the referencing table column names
93 * the foreign key constraint is associated with.
94 *
95 * But only if they were defined with one or the referencing table column name
96 * is a keyword reserved by the platform.
97 * Otherwise the plain unquoted value as inserted is returned.
98 *
99 * @param AbstractPlatform $platform The platform to use for quotation.
100 *
101 * @return array<int, string>
102 */
103 public function getQuotedLocalColumns(AbstractPlatform $platform): array
104 {
105 $columns = [];
106
107 foreach ($this->_localColumnNames as $column) {
108 $columns[] = $column->getQuotedName($platform);
109 }
110
111 return $columns;
112 }
113
114 /**
115 * Returns unquoted representation of local table column names for comparison with other FK
116 *
117 * @return array<int, string>
118 */
119 public function getUnquotedLocalColumns(): array
120 {
121 return array_map($this->trimQuotes(...), $this->getLocalColumns());
122 }
123
124 /**
125 * Returns unquoted representation of foreign table column names for comparison with other FK
126 *
127 * @return array<int, string>
128 */
129 public function getUnquotedForeignColumns(): array
130 {
131 return array_map($this->trimQuotes(...), $this->getForeignColumns());
132 }
133
134 /**
135 * Returns the name of the referenced table
136 * the foreign key constraint is associated with.
137 */
138 public function getForeignTableName(): string
139 {
140 return $this->_foreignTableName->getName();
141 }
142
143 /**
144 * Returns the non-schema qualified foreign table name.
145 */
146 public function getUnqualifiedForeignTableName(): string
147 {
148 $name = $this->_foreignTableName->getName();
149 $position = strrpos($name, '.');
150
151 if ($position !== false) {
152 $name = substr($name, $position + 1);
153 }
154
155 return strtolower($name);
156 }
157
158 /**
159 * Returns the quoted representation of the referenced table name
160 * the foreign key constraint is associated with.
161 *
162 * But only if it was defined with one or the referenced table name
163 * is a keyword reserved by the platform.
164 * Otherwise the plain unquoted value as inserted is returned.
165 *
166 * @param AbstractPlatform $platform The platform to use for quotation.
167 */
168 public function getQuotedForeignTableName(AbstractPlatform $platform): string
169 {
170 return $this->_foreignTableName->getQuotedName($platform);
171 }
172
173 /**
174 * Returns the names of the referenced table columns
175 * the foreign key constraint is associated with.
176 *
177 * @return array<int, string>
178 */
179 public function getForeignColumns(): array
180 {
181 return array_keys($this->_foreignColumnNames);
182 }
183
184 /**
185 * Returns the quoted representation of the referenced table column names
186 * the foreign key constraint is associated with.
187 *
188 * But only if they were defined with one or the referenced table column name
189 * is a keyword reserved by the platform.
190 * Otherwise the plain unquoted value as inserted is returned.
191 *
192 * @param AbstractPlatform $platform The platform to use for quotation.
193 *
194 * @return array<int, string>
195 */
196 public function getQuotedForeignColumns(AbstractPlatform $platform): array
197 {
198 $columns = [];
199
200 foreach ($this->_foreignColumnNames as $column) {
201 $columns[] = $column->getQuotedName($platform);
202 }
203
204 return $columns;
205 }
206
207 /**
208 * Returns whether or not a given option
209 * is associated with the foreign key constraint.
210 */
211 public function hasOption(string $name): bool
212 {
213 return isset($this->options[$name]);
214 }
215
216 /**
217 * Returns an option associated with the foreign key constraint.
218 */
219 public function getOption(string $name): mixed
220 {
221 return $this->options[$name];
222 }
223
224 /**
225 * Returns the options associated with the foreign key constraint.
226 *
227 * @return array<string, mixed>
228 */
229 public function getOptions(): array
230 {
231 return $this->options;
232 }
233
234 /**
235 * Returns the referential action for UPDATE operations
236 * on the referenced table the foreign key constraint is associated with.
237 */
238 public function onUpdate(): ?string
239 {
240 return $this->onEvent('onUpdate');
241 }
242
243 /**
244 * Returns the referential action for DELETE operations
245 * on the referenced table the foreign key constraint is associated with.
246 */
247 public function onDelete(): ?string
248 {
249 return $this->onEvent('onDelete');
250 }
251
252 /**
253 * Returns the referential action for a given database operation
254 * on the referenced table the foreign key constraint is associated with.
255 *
256 * @param string $event Name of the database operation/event to return the referential action for.
257 */
258 private function onEvent(string $event): ?string
259 {
260 if (isset($this->options[$event])) {
261 $onEvent = strtoupper($this->options[$event]);
262
263 if ($onEvent !== 'NO ACTION' && $onEvent !== 'RESTRICT') {
264 return $onEvent;
265 }
266 }
267
268 return null;
269 }
270
271 /**
272 * Checks whether this foreign key constraint intersects the given index columns.
273 *
274 * Returns `true` if at least one of this foreign key's local columns
275 * matches one of the given index's columns, `false` otherwise.
276 *
277 * @param Index $index The index to be checked against.
278 */
279 public function intersectsIndexColumns(Index $index): bool
280 {
281 foreach ($index->getColumns() as $indexColumn) {
282 foreach ($this->_localColumnNames as $localColumn) {
283 if (strtolower($indexColumn) === strtolower($localColumn->getName())) {
284 return true;
285 }
286 }
287 }
288
289 return false;
290 }
291}
diff --git a/vendor/doctrine/dbal/src/Schema/Identifier.php b/vendor/doctrine/dbal/src/Schema/Identifier.php
new file mode 100644
index 0000000..c3c84a7
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Identifier.php
@@ -0,0 +1,29 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7/**
8 * An abstraction class for an asset identifier.
9 *
10 * Wraps identifier names like column names in indexes / foreign keys
11 * in an abstract class for proper quotation capabilities.
12 */
13class Identifier extends AbstractAsset
14{
15 /**
16 * @param string $identifier Identifier name to wrap.
17 * @param bool $quote Whether to force quoting the given identifier.
18 */
19 public function __construct(string $identifier, bool $quote = false)
20 {
21 $this->_setName($identifier);
22
23 if (! $quote || $this->_quoted) {
24 return;
25 }
26
27 $this->_setName('"' . $this->getName() . '"');
28 }
29}
diff --git a/vendor/doctrine/dbal/src/Schema/Index.php b/vendor/doctrine/dbal/src/Schema/Index.php
new file mode 100644
index 0000000..1755654
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Index.php
@@ -0,0 +1,314 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8
9use function array_filter;
10use function array_keys;
11use function array_map;
12use function array_search;
13use function array_shift;
14use function count;
15use function strtolower;
16
17class Index extends AbstractAsset
18{
19 /**
20 * Asset identifier instances of the column names the index is associated with.
21 *
22 * @var array<string, Identifier>
23 */
24 protected array $_columns = [];
25
26 protected bool $_isUnique = false;
27
28 protected bool $_isPrimary = false;
29
30 /**
31 * Platform specific flags for indexes.
32 *
33 * @var array<string, true>
34 */
35 protected array $_flags = [];
36
37 /**
38 * @param array<int, string> $columns
39 * @param array<int, string> $flags
40 * @param array<string, mixed> $options
41 */
42 public function __construct(
43 ?string $name,
44 array $columns,
45 bool $isUnique = false,
46 bool $isPrimary = false,
47 array $flags = [],
48 private readonly array $options = [],
49 ) {
50 $isUnique = $isUnique || $isPrimary;
51
52 if ($name !== null) {
53 $this->_setName($name);
54 }
55
56 $this->_isUnique = $isUnique;
57 $this->_isPrimary = $isPrimary;
58
59 foreach ($columns as $column) {
60 $this->_addColumn($column);
61 }
62
63 foreach ($flags as $flag) {
64 $this->addFlag($flag);
65 }
66 }
67
68 protected function _addColumn(string $column): void
69 {
70 $this->_columns[$column] = new Identifier($column);
71 }
72
73 /**
74 * Returns the names of the referencing table columns the constraint is associated with.
75 *
76 * @return list<string>
77 */
78 public function getColumns(): array
79 {
80 return array_keys($this->_columns);
81 }
82
83 /**
84 * Returns the quoted representation of the column names the constraint is associated with.
85 *
86 * But only if they were defined with one or a column name
87 * is a keyword reserved by the platform.
88 * Otherwise, the plain unquoted value as inserted is returned.
89 *
90 * @param AbstractPlatform $platform The platform to use for quotation.
91 *
92 * @return list<string>
93 */
94 public function getQuotedColumns(AbstractPlatform $platform): array
95 {
96 $subParts = $platform->supportsColumnLengthIndexes() && $this->hasOption('lengths')
97 ? $this->getOption('lengths') : [];
98
99 $columns = [];
100
101 foreach ($this->_columns as $column) {
102 $length = array_shift($subParts);
103
104 $quotedColumn = $column->getQuotedName($platform);
105
106 if ($length !== null) {
107 $quotedColumn .= '(' . $length . ')';
108 }
109
110 $columns[] = $quotedColumn;
111 }
112
113 return $columns;
114 }
115
116 /** @return array<int, string> */
117 public function getUnquotedColumns(): array
118 {
119 return array_map($this->trimQuotes(...), $this->getColumns());
120 }
121
122 /**
123 * Is the index neither unique nor primary key?
124 */
125 public function isSimpleIndex(): bool
126 {
127 return ! $this->_isPrimary && ! $this->_isUnique;
128 }
129
130 public function isUnique(): bool
131 {
132 return $this->_isUnique;
133 }
134
135 public function isPrimary(): bool
136 {
137 return $this->_isPrimary;
138 }
139
140 public function hasColumnAtPosition(string $name, int $pos = 0): bool
141 {
142 $name = $this->trimQuotes(strtolower($name));
143 $indexColumns = array_map('strtolower', $this->getUnquotedColumns());
144
145 return array_search($name, $indexColumns, true) === $pos;
146 }
147
148 /**
149 * Checks if this index exactly spans the given column names in the correct order.
150 *
151 * @param array<int, string> $columnNames
152 */
153 public function spansColumns(array $columnNames): bool
154 {
155 $columns = $this->getColumns();
156 $numberOfColumns = count($columns);
157 $sameColumns = true;
158
159 for ($i = 0; $i < $numberOfColumns; $i++) {
160 if (
161 isset($columnNames[$i])
162 && $this->trimQuotes(strtolower($columns[$i])) === $this->trimQuotes(strtolower($columnNames[$i]))
163 ) {
164 continue;
165 }
166
167 $sameColumns = false;
168 }
169
170 return $sameColumns;
171 }
172
173 /**
174 * Checks if the other index already fulfills all the indexing and constraint needs of the current one.
175 */
176 public function isFulfilledBy(Index $other): bool
177 {
178 // allow the other index to be equally large only. It being larger is an option
179 // but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo)
180 if (count($other->getColumns()) !== count($this->getColumns())) {
181 return false;
182 }
183
184 // Check if columns are the same, and even in the same order
185 $sameColumns = $this->spansColumns($other->getColumns());
186
187 if ($sameColumns) {
188 if (! $this->samePartialIndex($other)) {
189 return false;
190 }
191
192 if (! $this->hasSameColumnLengths($other)) {
193 return false;
194 }
195
196 if (! $this->isUnique() && ! $this->isPrimary()) {
197 // this is a special case: If the current key is neither primary or unique, any unique or
198 // primary key will always have the same effect for the index and there cannot be any constraint
199 // overlaps. This means a primary or unique index can always fulfill the requirements of just an
200 // index that has no constraints.
201 return true;
202 }
203
204 if ($other->isPrimary() !== $this->isPrimary()) {
205 return false;
206 }
207
208 return $other->isUnique() === $this->isUnique();
209 }
210
211 return false;
212 }
213
214 /**
215 * Detects if the other index is a non-unique, non primary index that can be overwritten by this one.
216 */
217 public function overrules(Index $other): bool
218 {
219 if ($other->isPrimary()) {
220 return false;
221 }
222
223 if ($this->isSimpleIndex() && $other->isUnique()) {
224 return false;
225 }
226
227 return $this->spansColumns($other->getColumns())
228 && ($this->isPrimary() || $this->isUnique())
229 && $this->samePartialIndex($other);
230 }
231
232 /**
233 * Returns platform specific flags for indexes.
234 *
235 * @return array<int, string>
236 */
237 public function getFlags(): array
238 {
239 return array_keys($this->_flags);
240 }
241
242 /**
243 * Adds Flag for an index that translates to platform specific handling.
244 *
245 * @example $index->addFlag('CLUSTERED')
246 */
247 public function addFlag(string $flag): self
248 {
249 $this->_flags[strtolower($flag)] = true;
250
251 return $this;
252 }
253
254 /**
255 * Does this index have a specific flag?
256 */
257 public function hasFlag(string $flag): bool
258 {
259 return isset($this->_flags[strtolower($flag)]);
260 }
261
262 /**
263 * Removes a flag.
264 */
265 public function removeFlag(string $flag): void
266 {
267 unset($this->_flags[strtolower($flag)]);
268 }
269
270 public function hasOption(string $name): bool
271 {
272 return isset($this->options[strtolower($name)]);
273 }
274
275 public function getOption(string $name): mixed
276 {
277 return $this->options[strtolower($name)];
278 }
279
280 /** @return array<string, mixed> */
281 public function getOptions(): array
282 {
283 return $this->options;
284 }
285
286 /**
287 * Return whether the two indexes have the same partial index
288 */
289 private function samePartialIndex(Index $other): bool
290 {
291 if (
292 $this->hasOption('where')
293 && $other->hasOption('where')
294 && $this->getOption('where') === $other->getOption('where')
295 ) {
296 return true;
297 }
298
299 return ! $this->hasOption('where') && ! $other->hasOption('where');
300 }
301
302 /**
303 * Returns whether the index has the same column lengths as the other
304 */
305 private function hasSameColumnLengths(self $other): bool
306 {
307 $filter = static function (?int $length): bool {
308 return $length !== null;
309 };
310
311 return array_filter($this->options['lengths'] ?? [], $filter)
312 === array_filter($other->options['lengths'] ?? [], $filter);
313 }
314}
diff --git a/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php b/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php
new file mode 100644
index 0000000..249be13
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php
@@ -0,0 +1,548 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
9use Doctrine\DBAL\Platforms\MariaDBPlatform;
10use Doctrine\DBAL\Platforms\MySQL;
11use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider\CachingCharsetMetadataProvider;
12use Doctrine\DBAL\Platforms\MySQL\CharsetMetadataProvider\ConnectionCharsetMetadataProvider;
13use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\CachingCollationMetadataProvider;
14use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\ConnectionCollationMetadataProvider;
15use Doctrine\DBAL\Platforms\MySQL\DefaultTableOptions;
16use Doctrine\DBAL\Result;
17use Doctrine\DBAL\Types\Type;
18
19use function array_change_key_case;
20use function assert;
21use function explode;
22use function implode;
23use function is_string;
24use function preg_match;
25use function str_contains;
26use function strtok;
27use function strtolower;
28use function strtr;
29
30use const CASE_LOWER;
31
32/**
33 * Schema manager for the MySQL RDBMS.
34 *
35 * @extends AbstractSchemaManager<AbstractMySQLPlatform>
36 */
37class MySQLSchemaManager extends AbstractSchemaManager
38{
39 /** @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences */
40 private const MARIADB_ESCAPE_SEQUENCES = [
41 '\\0' => "\0",
42 "\\'" => "'",
43 '\\"' => '"',
44 '\\b' => "\b",
45 '\\n' => "\n",
46 '\\r' => "\r",
47 '\\t' => "\t",
48 '\\Z' => "\x1a",
49 '\\\\' => '\\',
50 '\\%' => '%',
51 '\\_' => '_',
52
53 // Internally, MariaDB escapes single quotes using the standard syntax
54 "''" => "'",
55 ];
56
57 private ?DefaultTableOptions $defaultTableOptions = null;
58
59 /**
60 * {@inheritDoc}
61 */
62 protected function _getPortableTableDefinition(array $table): string
63 {
64 return $table['TABLE_NAME'];
65 }
66
67 /**
68 * {@inheritDoc}
69 */
70 protected function _getPortableViewDefinition(array $view): View
71 {
72 return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']);
73 }
74
75 /**
76 * {@inheritDoc}
77 */
78 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
79 {
80 foreach ($tableIndexes as $k => $v) {
81 $v = array_change_key_case($v, CASE_LOWER);
82 if ($v['key_name'] === 'PRIMARY') {
83 $v['primary'] = true;
84 } else {
85 $v['primary'] = false;
86 }
87
88 if (str_contains($v['index_type'], 'FULLTEXT')) {
89 $v['flags'] = ['FULLTEXT'];
90 } elseif (str_contains($v['index_type'], 'SPATIAL')) {
91 $v['flags'] = ['SPATIAL'];
92 }
93
94 // Ignore prohibited prefix `length` for spatial index
95 if (! str_contains($v['index_type'], 'SPATIAL')) {
96 $v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null;
97 }
98
99 $tableIndexes[$k] = $v;
100 }
101
102 return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
103 }
104
105 /**
106 * {@inheritDoc}
107 */
108 protected function _getPortableDatabaseDefinition(array $database): string
109 {
110 return $database['Database'];
111 }
112
113 /**
114 * {@inheritDoc}
115 */
116 protected function _getPortableTableColumnDefinition(array $tableColumn): Column
117 {
118 $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
119
120 $dbType = strtolower($tableColumn['type']);
121 $dbType = strtok($dbType, '(), ');
122 assert(is_string($dbType));
123
124 $length = $tableColumn['length'] ?? strtok('(), ');
125
126 $fixed = false;
127
128 if (! isset($tableColumn['name'])) {
129 $tableColumn['name'] = '';
130 }
131
132 $scale = 0;
133 $precision = null;
134
135 $type = $this->platform->getDoctrineTypeMapping($dbType);
136
137 switch ($dbType) {
138 case 'char':
139 case 'binary':
140 $fixed = true;
141 break;
142
143 case 'float':
144 case 'double':
145 case 'real':
146 case 'numeric':
147 case 'decimal':
148 if (
149 preg_match(
150 '([A-Za-z]+\(([0-9]+),([0-9]+)\))',
151 $tableColumn['type'],
152 $match,
153 ) === 1
154 ) {
155 $precision = (int) $match[1];
156 $scale = (int) $match[2];
157 $length = null;
158 }
159
160 break;
161
162 case 'tinytext':
163 $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT;
164 break;
165
166 case 'text':
167 $length = AbstractMySQLPlatform::LENGTH_LIMIT_TEXT;
168 break;
169
170 case 'mediumtext':
171 $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT;
172 break;
173
174 case 'tinyblob':
175 $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB;
176 break;
177
178 case 'blob':
179 $length = AbstractMySQLPlatform::LENGTH_LIMIT_BLOB;
180 break;
181
182 case 'mediumblob':
183 $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB;
184 break;
185
186 case 'tinyint':
187 case 'smallint':
188 case 'mediumint':
189 case 'int':
190 case 'integer':
191 case 'bigint':
192 case 'year':
193 $length = null;
194 break;
195 }
196
197 if ($this->platform instanceof MariaDBPlatform) {
198 $columnDefault = $this->getMariaDBColumnDefault($this->platform, $tableColumn['default']);
199 } else {
200 $columnDefault = $tableColumn['default'];
201 }
202
203 $options = [
204 'length' => $length !== null ? (int) $length : null,
205 'unsigned' => str_contains($tableColumn['type'], 'unsigned'),
206 'fixed' => $fixed,
207 'default' => $columnDefault,
208 'notnull' => $tableColumn['null'] !== 'YES',
209 'scale' => $scale,
210 'precision' => $precision,
211 'autoincrement' => str_contains($tableColumn['extra'], 'auto_increment'),
212 ];
213
214 if (isset($tableColumn['comment'])) {
215 $options['comment'] = $tableColumn['comment'];
216 }
217
218 $column = new Column($tableColumn['field'], Type::getType($type), $options);
219
220 if (isset($tableColumn['characterset'])) {
221 $column->setPlatformOption('charset', $tableColumn['characterset']);
222 }
223
224 if (isset($tableColumn['collation'])) {
225 $column->setPlatformOption('collation', $tableColumn['collation']);
226 }
227
228 return $column;
229 }
230
231 /**
232 * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
233 *
234 * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted
235 * to distinguish them from expressions (see MDEV-10134).
236 * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema
237 * as current_timestamp(), currdate(), currtime()
238 * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have
239 * null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053)
240 * - \' is always stored as '' in information_schema (normalized)
241 *
242 * @link https://mariadb.com/kb/en/library/information-schema-columns-table/
243 * @link https://jira.mariadb.org/browse/MDEV-13132
244 *
245 * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7
246 */
247 private function getMariaDBColumnDefault(MariaDBPlatform $platform, ?string $columnDefault): ?string
248 {
249 if ($columnDefault === 'NULL' || $columnDefault === null) {
250 return null;
251 }
252
253 if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches) === 1) {
254 return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
255 }
256
257 return match ($columnDefault) {
258 'current_timestamp()' => $platform->getCurrentTimestampSQL(),
259 'curdate()' => $platform->getCurrentDateSQL(),
260 'curtime()' => $platform->getCurrentTimeSQL(),
261 default => $columnDefault,
262 };
263 }
264
265 /**
266 * {@inheritDoc}
267 */
268 protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array
269 {
270 $list = [];
271 foreach ($tableForeignKeys as $value) {
272 $value = array_change_key_case($value, CASE_LOWER);
273 if (! isset($list[$value['constraint_name']])) {
274 if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') {
275 $value['delete_rule'] = null;
276 }
277
278 if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') {
279 $value['update_rule'] = null;
280 }
281
282 $list[$value['constraint_name']] = [
283 'name' => $value['constraint_name'],
284 'local' => [],
285 'foreign' => [],
286 'foreignTable' => $value['referenced_table_name'],
287 'onDelete' => $value['delete_rule'],
288 'onUpdate' => $value['update_rule'],
289 ];
290 }
291
292 $list[$value['constraint_name']]['local'][] = $value['column_name'];
293 $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name'];
294 }
295
296 return parent::_getPortableTableForeignKeysList($list);
297 }
298
299 /**
300 * {@inheritDoc}
301 */
302 protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
303 {
304 return new ForeignKeyConstraint(
305 $tableForeignKey['local'],
306 $tableForeignKey['foreignTable'],
307 $tableForeignKey['foreign'],
308 $tableForeignKey['name'],
309 [
310 'onDelete' => $tableForeignKey['onDelete'],
311 'onUpdate' => $tableForeignKey['onUpdate'],
312 ],
313 );
314 }
315
316 /** @throws Exception */
317 public function createComparator(): Comparator
318 {
319 return new MySQL\Comparator(
320 $this->platform,
321 new CachingCharsetMetadataProvider(
322 new ConnectionCharsetMetadataProvider($this->connection),
323 ),
324 new CachingCollationMetadataProvider(
325 new ConnectionCollationMetadataProvider($this->connection),
326 ),
327 $this->getDefaultTableOptions(),
328 );
329 }
330
331 protected function selectTableNames(string $databaseName): Result
332 {
333 $sql = <<<'SQL'
334SELECT TABLE_NAME
335FROM information_schema.TABLES
336WHERE TABLE_SCHEMA = ?
337 AND TABLE_TYPE = 'BASE TABLE'
338ORDER BY TABLE_NAME
339SQL;
340
341 return $this->connection->executeQuery($sql, [$databaseName]);
342 }
343
344 protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
345 {
346 $columnTypeSQL = $this->platform->getColumnTypeSQLSnippet('c', $databaseName);
347
348 $sql = 'SELECT';
349
350 if ($tableName === null) {
351 $sql .= ' c.TABLE_NAME,';
352 }
353
354 $sql .= <<<SQL
355 c.COLUMN_NAME AS field,
356 $columnTypeSQL AS type,
357 c.IS_NULLABLE AS `null`,
358 c.COLUMN_KEY AS `key`,
359 c.COLUMN_DEFAULT AS `default`,
360 c.EXTRA,
361 c.COLUMN_COMMENT AS comment,
362 c.CHARACTER_SET_NAME AS characterset,
363 c.COLLATION_NAME AS collation
364FROM information_schema.COLUMNS c
365 INNER JOIN information_schema.TABLES t
366 ON t.TABLE_NAME = c.TABLE_NAME
367SQL;
368
369 // The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition
370 // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
371 // caused by https://bugs.mysql.com/bug.php?id=81347
372 $conditions = ['c.TABLE_SCHEMA = ?', 't.TABLE_SCHEMA = ?', "t.TABLE_TYPE = 'BASE TABLE'"];
373 $params = [$databaseName, $databaseName];
374
375 if ($tableName !== null) {
376 $conditions[] = 't.TABLE_NAME = ?';
377 $params[] = $tableName;
378 }
379
380 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY ORDINAL_POSITION';
381
382 return $this->connection->executeQuery($sql, $params);
383 }
384
385 protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
386 {
387 $sql = 'SELECT';
388
389 if ($tableName === null) {
390 $sql .= ' TABLE_NAME,';
391 }
392
393 $sql .= <<<'SQL'
394 NON_UNIQUE AS Non_Unique,
395 INDEX_NAME AS Key_name,
396 COLUMN_NAME AS Column_Name,
397 SUB_PART AS Sub_Part,
398 INDEX_TYPE AS Index_Type
399FROM information_schema.STATISTICS
400SQL;
401
402 $conditions = ['TABLE_SCHEMA = ?'];
403 $params = [$databaseName];
404
405 if ($tableName !== null) {
406 $conditions[] = 'TABLE_NAME = ?';
407 $params[] = $tableName;
408 }
409
410 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY SEQ_IN_INDEX';
411
412 return $this->connection->executeQuery($sql, $params);
413 }
414
415 protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
416 {
417 $sql = 'SELECT DISTINCT';
418
419 if ($tableName === null) {
420 $sql .= ' k.TABLE_NAME,';
421 }
422
423 $sql .= <<<'SQL'
424 k.CONSTRAINT_NAME,
425 k.COLUMN_NAME,
426 k.REFERENCED_TABLE_NAME,
427 k.REFERENCED_COLUMN_NAME,
428 k.ORDINAL_POSITION,
429 c.UPDATE_RULE,
430 c.DELETE_RULE
431FROM information_schema.key_column_usage k
432INNER JOIN information_schema.referential_constraints c
433ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME
434AND c.TABLE_NAME = k.TABLE_NAME
435SQL;
436
437 $conditions = ['k.TABLE_SCHEMA = ?'];
438 $params = [$databaseName];
439
440 if ($tableName !== null) {
441 $conditions[] = 'k.TABLE_NAME = ?';
442 $params[] = $tableName;
443 }
444
445 // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition
446 // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
447 // caused by https://bugs.mysql.com/bug.php?id=81347
448 $conditions[] = 'c.CONSTRAINT_SCHEMA = ?';
449 $params[] = $databaseName;
450
451 $conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL';
452
453 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY k.ORDINAL_POSITION';
454
455 return $this->connection->executeQuery($sql, $params);
456 }
457
458 /**
459 * {@inheritDoc}
460 */
461 protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
462 {
463 // MariaDB-10.10.1 added FULL_COLLATION_NAME to the information_schema.COLLATION_CHARACTER_SET_APPLICABILITY.
464 // A base collation like uca1400_ai_ci can refer to multiple character sets. The value in the
465 // information_schema.TABLES.TABLE_COLLATION corresponds to the full collation name.
466 // The MariaDB executable comment syntax with version, /*M!101001, is exclusively executed on
467 // MariaDB-10.10.1+ servers for backwards compatibility, and compatiblity to MySQL servers.
468 $sql = <<<'SQL'
469 SELECT t.TABLE_NAME,
470 t.ENGINE,
471 t.AUTO_INCREMENT,
472 t.TABLE_COMMENT,
473 t.CREATE_OPTIONS,
474 t.TABLE_COLLATION,
475 ccsa.CHARACTER_SET_NAME
476 FROM information_schema.TABLES t
477 INNER JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY ccsa
478 ON /*M!101001 ccsa.FULL_COLLATION_NAME = t.TABLE_COLLATION OR */
479 ccsa.COLLATION_NAME = t.TABLE_COLLATION
480SQL;
481
482 $conditions = ['t.TABLE_SCHEMA = ?'];
483 $params = [$databaseName];
484
485 if ($tableName !== null) {
486 $conditions[] = 't.TABLE_NAME = ?';
487 $params[] = $tableName;
488 }
489
490 $conditions[] = "t.TABLE_TYPE = 'BASE TABLE'";
491
492 $sql .= ' WHERE ' . implode(' AND ', $conditions);
493
494 /** @var array<string,array<string,mixed>> $metadata */
495 $metadata = $this->connection->executeQuery($sql, $params)
496 ->fetchAllAssociativeIndexed();
497
498 $tableOptions = [];
499 foreach ($metadata as $table => $data) {
500 $data = array_change_key_case($data, CASE_LOWER);
501
502 $tableOptions[$table] = [
503 'engine' => $data['engine'],
504 'collation' => $data['table_collation'],
505 'charset' => $data['character_set_name'],
506 'autoincrement' => $data['auto_increment'],
507 'comment' => $data['table_comment'],
508 'create_options' => $this->parseCreateOptions($data['create_options']),
509 ];
510 }
511
512 return $tableOptions;
513 }
514
515 /** @return array<string, string>|array<string, true> */
516 private function parseCreateOptions(?string $string): array
517 {
518 $options = [];
519
520 if ($string === null || $string === '') {
521 return $options;
522 }
523
524 foreach (explode(' ', $string) as $pair) {
525 $parts = explode('=', $pair, 2);
526
527 $options[$parts[0]] = $parts[1] ?? true;
528 }
529
530 return $options;
531 }
532
533 /** @throws Exception */
534 private function getDefaultTableOptions(): DefaultTableOptions
535 {
536 if ($this->defaultTableOptions === null) {
537 $row = $this->connection->fetchNumeric(
538 'SELECT @@character_set_database, @@collation_database',
539 );
540
541 assert($row !== false);
542
543 $this->defaultTableOptions = new DefaultTableOptions(...$row);
544 }
545
546 return $this->defaultTableOptions;
547 }
548}
diff --git a/vendor/doctrine/dbal/src/Schema/OracleSchemaManager.php b/vendor/doctrine/dbal/src/Schema/OracleSchemaManager.php
new file mode 100644
index 0000000..f973eaa
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/OracleSchemaManager.php
@@ -0,0 +1,475 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
9use Doctrine\DBAL\Platforms\OraclePlatform;
10use Doctrine\DBAL\Result;
11use Doctrine\DBAL\Types\Type;
12
13use function array_change_key_case;
14use function array_key_exists;
15use function array_values;
16use function assert;
17use function implode;
18use function is_string;
19use function preg_match;
20use function str_contains;
21use function str_replace;
22use function str_starts_with;
23use function strtolower;
24use function strtoupper;
25use function trim;
26
27use const CASE_LOWER;
28
29/**
30 * Oracle Schema Manager.
31 *
32 * @extends AbstractSchemaManager<OraclePlatform>
33 */
34class OracleSchemaManager extends AbstractSchemaManager
35{
36 /**
37 * {@inheritDoc}
38 */
39 protected function _getPortableViewDefinition(array $view): View
40 {
41 $view = array_change_key_case($view, CASE_LOWER);
42
43 return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']);
44 }
45
46 /**
47 * {@inheritDoc}
48 */
49 protected function _getPortableTableDefinition(array $table): string
50 {
51 $table = array_change_key_case($table, CASE_LOWER);
52
53 return $this->getQuotedIdentifierName($table['table_name']);
54 }
55
56 /**
57 * {@inheritDoc}
58 *
59 * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
60 */
61 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
62 {
63 $indexBuffer = [];
64 foreach ($tableIndexes as $tableIndex) {
65 $tableIndex = array_change_key_case($tableIndex, CASE_LOWER);
66
67 $keyName = strtolower($tableIndex['name']);
68 $buffer = [];
69
70 if ($tableIndex['is_primary'] === 'P') {
71 $keyName = 'primary';
72 $buffer['primary'] = true;
73 $buffer['non_unique'] = false;
74 } else {
75 $buffer['primary'] = false;
76 $buffer['non_unique'] = ! $tableIndex['is_unique'];
77 }
78
79 $buffer['key_name'] = $keyName;
80 $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']);
81 $indexBuffer[] = $buffer;
82 }
83
84 return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
85 }
86
87 /**
88 * {@inheritDoc}
89 */
90 protected function _getPortableTableColumnDefinition(array $tableColumn): Column
91 {
92 $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
93
94 $dbType = strtolower($tableColumn['data_type']);
95 if (str_starts_with($dbType, 'timestamp(')) {
96 if (str_contains($dbType, 'with time zone')) {
97 $dbType = 'timestamptz';
98 } else {
99 $dbType = 'timestamp';
100 }
101 }
102
103 $length = $precision = null;
104 $scale = 0;
105 $fixed = false;
106
107 if (! isset($tableColumn['column_name'])) {
108 $tableColumn['column_name'] = '';
109 }
110
111 assert(array_key_exists('data_default', $tableColumn));
112
113 // Default values returned from database sometimes have trailing spaces.
114 if (is_string($tableColumn['data_default'])) {
115 $tableColumn['data_default'] = trim($tableColumn['data_default']);
116 }
117
118 if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') {
119 $tableColumn['data_default'] = null;
120 }
121
122 if ($tableColumn['data_default'] !== null) {
123 // Default values returned from database are represented as literal expressions
124 if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches) === 1) {
125 $tableColumn['data_default'] = str_replace("''", "'", $matches[1]);
126 }
127 }
128
129 if ($tableColumn['data_precision'] !== null) {
130 $precision = (int) $tableColumn['data_precision'];
131 }
132
133 if ($tableColumn['data_scale'] !== null) {
134 $scale = (int) $tableColumn['data_scale'];
135 }
136
137 $type = $this->platform->getDoctrineTypeMapping($dbType);
138
139 switch ($dbType) {
140 case 'number':
141 if ($precision === 20 && $scale === 0) {
142 $type = 'bigint';
143 } elseif ($precision === 5 && $scale === 0) {
144 $type = 'smallint';
145 } elseif ($precision === 1 && $scale === 0) {
146 $type = 'boolean';
147 } elseif ($scale > 0) {
148 $type = 'decimal';
149 }
150
151 break;
152
153 case 'varchar':
154 case 'varchar2':
155 case 'nvarchar2':
156 $length = (int) $tableColumn['char_length'];
157 break;
158
159 case 'raw':
160 $length = (int) $tableColumn['data_length'];
161 $fixed = true;
162 break;
163
164 case 'char':
165 case 'nchar':
166 $length = (int) $tableColumn['char_length'];
167 $fixed = true;
168 break;
169 }
170
171 $options = [
172 'notnull' => $tableColumn['nullable'] === 'N',
173 'fixed' => $fixed,
174 'default' => $tableColumn['data_default'],
175 'length' => $length,
176 'precision' => $precision,
177 'scale' => $scale,
178 ];
179
180 if (isset($tableColumn['comments'])) {
181 $options['comment'] = $tableColumn['comments'];
182 }
183
184 return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options);
185 }
186
187 /**
188 * {@inheritDoc}
189 */
190 protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array
191 {
192 $list = [];
193 foreach ($tableForeignKeys as $value) {
194 $value = array_change_key_case($value, CASE_LOWER);
195 if (! isset($list[$value['constraint_name']])) {
196 if ($value['delete_rule'] === 'NO ACTION') {
197 $value['delete_rule'] = null;
198 }
199
200 $list[$value['constraint_name']] = [
201 'name' => $this->getQuotedIdentifierName($value['constraint_name']),
202 'local' => [],
203 'foreign' => [],
204 'foreignTable' => $value['references_table'],
205 'onDelete' => $value['delete_rule'],
206 ];
207 }
208
209 $localColumn = $this->getQuotedIdentifierName($value['local_column']);
210 $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']);
211
212 $list[$value['constraint_name']]['local'][$value['position']] = $localColumn;
213 $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn;
214 }
215
216 return parent::_getPortableTableForeignKeysList($list);
217 }
218
219 /**
220 * {@inheritDoc}
221 */
222 protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
223 {
224 return new ForeignKeyConstraint(
225 array_values($tableForeignKey['local']),
226 $this->getQuotedIdentifierName($tableForeignKey['foreignTable']),
227 array_values($tableForeignKey['foreign']),
228 $this->getQuotedIdentifierName($tableForeignKey['name']),
229 ['onDelete' => $tableForeignKey['onDelete']],
230 );
231 }
232
233 /**
234 * {@inheritDoc}
235 */
236 protected function _getPortableSequenceDefinition(array $sequence): Sequence
237 {
238 $sequence = array_change_key_case($sequence, CASE_LOWER);
239
240 return new Sequence(
241 $this->getQuotedIdentifierName($sequence['sequence_name']),
242 (int) $sequence['increment_by'],
243 (int) $sequence['min_value'],
244 );
245 }
246
247 /**
248 * {@inheritDoc}
249 */
250 protected function _getPortableDatabaseDefinition(array $database): string
251 {
252 $database = array_change_key_case($database, CASE_LOWER);
253
254 return $database['username'];
255 }
256
257 public function createDatabase(string $database): void
258 {
259 $statement = $this->platform->getCreateDatabaseSQL($database);
260
261 $params = $this->connection->getParams();
262
263 if (isset($params['password'])) {
264 $statement .= ' IDENTIFIED BY ' . $params['password'];
265 }
266
267 $this->connection->executeStatement($statement);
268
269 $statement = 'GRANT DBA TO ' . $database;
270 $this->connection->executeStatement($statement);
271 }
272
273 /** @throws Exception */
274 protected function dropAutoincrement(string $table): bool
275 {
276 $sql = $this->platform->getDropAutoincrementSql($table);
277 foreach ($sql as $query) {
278 $this->connection->executeStatement($query);
279 }
280
281 return true;
282 }
283
284 public function dropTable(string $name): void
285 {
286 try {
287 $this->dropAutoincrement($name);
288 } catch (DatabaseObjectNotFoundException) {
289 }
290
291 parent::dropTable($name);
292 }
293
294 /**
295 * Returns the quoted representation of the given identifier name.
296 *
297 * Quotes non-uppercase identifiers explicitly to preserve case
298 * and thus make references to the particular identifier work.
299 */
300 private function getQuotedIdentifierName(string $identifier): string
301 {
302 if (preg_match('/[a-z]/', $identifier) === 1) {
303 return $this->platform->quoteIdentifier($identifier);
304 }
305
306 return $identifier;
307 }
308
309 protected function selectTableNames(string $databaseName): Result
310 {
311 $sql = <<<'SQL'
312SELECT TABLE_NAME
313FROM ALL_TABLES
314WHERE OWNER = :OWNER
315ORDER BY TABLE_NAME
316SQL;
317
318 return $this->connection->executeQuery($sql, ['OWNER' => $databaseName]);
319 }
320
321 protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
322 {
323 $sql = 'SELECT';
324
325 if ($tableName === null) {
326 $sql .= ' C.TABLE_NAME,';
327 }
328
329 $sql .= <<<'SQL'
330 C.COLUMN_NAME,
331 C.DATA_TYPE,
332 C.DATA_DEFAULT,
333 C.DATA_PRECISION,
334 C.DATA_SCALE,
335 C.CHAR_LENGTH,
336 C.DATA_LENGTH,
337 C.NULLABLE,
338 D.COMMENTS
339 FROM ALL_TAB_COLUMNS C
340 INNER JOIN ALL_TABLES T
341 ON T.OWNER = C.OWNER
342 AND T.TABLE_NAME = C.TABLE_NAME
343 LEFT JOIN ALL_COL_COMMENTS D
344 ON D.OWNER = C.OWNER
345 AND D.TABLE_NAME = C.TABLE_NAME
346 AND D.COLUMN_NAME = C.COLUMN_NAME
347SQL;
348
349 $conditions = ['C.OWNER = :OWNER'];
350 $params = ['OWNER' => $databaseName];
351
352 if ($tableName !== null) {
353 $conditions[] = 'C.TABLE_NAME = :TABLE_NAME';
354 $params['TABLE_NAME'] = $tableName;
355 }
356
357 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.COLUMN_ID';
358
359 return $this->connection->executeQuery($sql, $params);
360 }
361
362 protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
363 {
364 $sql = 'SELECT';
365
366 if ($tableName === null) {
367 $sql .= ' IND_COL.TABLE_NAME,';
368 }
369
370 $sql .= <<<'SQL'
371 IND_COL.INDEX_NAME AS NAME,
372 IND.INDEX_TYPE AS TYPE,
373 DECODE(IND.UNIQUENESS, 'NONUNIQUE', 0, 'UNIQUE', 1) AS IS_UNIQUE,
374 IND_COL.COLUMN_NAME,
375 IND_COL.COLUMN_POSITION AS COLUMN_POS,
376 CON.CONSTRAINT_TYPE AS IS_PRIMARY
377 FROM ALL_IND_COLUMNS IND_COL
378 LEFT JOIN ALL_INDEXES IND
379 ON IND.OWNER = IND_COL.INDEX_OWNER
380 AND IND.INDEX_NAME = IND_COL.INDEX_NAME
381 LEFT JOIN ALL_CONSTRAINTS CON
382 ON CON.OWNER = IND_COL.INDEX_OWNER
383 AND CON.INDEX_NAME = IND_COL.INDEX_NAME
384SQL;
385
386 $conditions = ['IND_COL.INDEX_OWNER = :OWNER'];
387 $params = ['OWNER' => $databaseName];
388
389 if ($tableName !== null) {
390 $conditions[] = 'IND_COL.TABLE_NAME = :TABLE_NAME';
391 $params['TABLE_NAME'] = $tableName;
392 }
393
394 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IND_COL.TABLE_NAME, IND_COL.INDEX_NAME'
395 . ', IND_COL.COLUMN_POSITION';
396
397 return $this->connection->executeQuery($sql, $params);
398 }
399
400 protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
401 {
402 $sql = 'SELECT';
403
404 if ($tableName === null) {
405 $sql .= ' COLS.TABLE_NAME,';
406 }
407
408 $sql .= <<<'SQL'
409 ALC.CONSTRAINT_NAME,
410 ALC.DELETE_RULE,
411 COLS.COLUMN_NAME LOCAL_COLUMN,
412 COLS.POSITION,
413 R_COLS.TABLE_NAME REFERENCES_TABLE,
414 R_COLS.COLUMN_NAME FOREIGN_COLUMN
415 FROM ALL_CONS_COLUMNS COLS
416 LEFT JOIN ALL_CONSTRAINTS ALC ON ALC.OWNER = COLS.OWNER AND ALC.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME
417 LEFT JOIN ALL_CONS_COLUMNS R_COLS ON R_COLS.OWNER = ALC.R_OWNER AND
418 R_COLS.CONSTRAINT_NAME = ALC.R_CONSTRAINT_NAME AND
419 R_COLS.POSITION = COLS.POSITION
420SQL;
421
422 $conditions = ["ALC.CONSTRAINT_TYPE = 'R'", 'COLS.OWNER = :OWNER'];
423 $params = ['OWNER' => $databaseName];
424
425 if ($tableName !== null) {
426 $conditions[] = 'COLS.TABLE_NAME = :TABLE_NAME';
427 $params['TABLE_NAME'] = $tableName;
428 }
429
430 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY COLS.TABLE_NAME, COLS.CONSTRAINT_NAME'
431 . ', COLS.POSITION';
432
433 return $this->connection->executeQuery($sql, $params);
434 }
435
436 /**
437 * {@inheritDoc}
438 */
439 protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
440 {
441 $sql = 'SELECT TABLE_NAME, COMMENTS';
442
443 $conditions = ['OWNER = :OWNER'];
444 $params = ['OWNER' => $databaseName];
445
446 if ($tableName !== null) {
447 $conditions[] = 'TABLE_NAME = :TABLE_NAME';
448 $params['TABLE_NAME'] = $tableName;
449 }
450
451 $sql .= ' FROM ALL_TAB_COMMENTS WHERE ' . implode(' AND ', $conditions);
452
453 /** @var array<string,array<string,mixed>> $metadata */
454 $metadata = $this->connection->executeQuery($sql, $params)
455 ->fetchAllAssociativeIndexed();
456
457 $tableOptions = [];
458 foreach ($metadata as $table => $data) {
459 $data = array_change_key_case($data, CASE_LOWER);
460
461 $tableOptions[$table] = [
462 'comment' => $data['comments'],
463 ];
464 }
465
466 return $tableOptions;
467 }
468
469 protected function normalizeName(string $name): string
470 {
471 $identifier = new Identifier($name);
472
473 return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name);
474 }
475}
diff --git a/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php b/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php
new file mode 100644
index 0000000..9af16c9
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php
@@ -0,0 +1,572 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
9use Doctrine\DBAL\Result;
10use Doctrine\DBAL\Types\JsonType;
11use Doctrine\DBAL\Types\Type;
12
13use function array_change_key_case;
14use function array_key_exists;
15use function array_map;
16use function array_merge;
17use function assert;
18use function explode;
19use function implode;
20use function in_array;
21use function is_string;
22use function preg_match;
23use function sprintf;
24use function str_contains;
25use function str_replace;
26use function strtolower;
27use function trim;
28
29use const CASE_LOWER;
30
31/**
32 * PostgreSQL Schema Manager.
33 *
34 * @extends AbstractSchemaManager<PostgreSQLPlatform>
35 */
36class PostgreSQLSchemaManager extends AbstractSchemaManager
37{
38 private ?string $currentSchema = null;
39
40 /**
41 * {@inheritDoc}
42 */
43 public function listSchemaNames(): array
44 {
45 return $this->connection->fetchFirstColumn(
46 <<<'SQL'
47SELECT schema_name
48FROM information_schema.schemata
49WHERE schema_name NOT LIKE 'pg\_%'
50AND schema_name != 'information_schema'
51SQL,
52 );
53 }
54
55 public function createSchemaConfig(): SchemaConfig
56 {
57 $config = parent::createSchemaConfig();
58
59 $config->setName($this->getCurrentSchema());
60
61 return $config;
62 }
63
64 /**
65 * Returns the name of the current schema.
66 *
67 * @throws Exception
68 */
69 protected function getCurrentSchema(): ?string
70 {
71 return $this->currentSchema ??= $this->determineCurrentSchema();
72 }
73
74 /**
75 * Determines the name of the current schema.
76 *
77 * @throws Exception
78 */
79 protected function determineCurrentSchema(): string
80 {
81 $currentSchema = $this->connection->fetchOne('SELECT current_schema()');
82 assert(is_string($currentSchema));
83
84 return $currentSchema;
85 }
86
87 /**
88 * {@inheritDoc}
89 */
90 protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
91 {
92 $onUpdate = null;
93 $onDelete = null;
94
95 if (
96 preg_match(
97 '(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
98 $tableForeignKey['condef'],
99 $match,
100 ) === 1
101 ) {
102 $onUpdate = $match[1];
103 }
104
105 if (
106 preg_match(
107 '(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))',
108 $tableForeignKey['condef'],
109 $match,
110 ) === 1
111 ) {
112 $onDelete = $match[1];
113 }
114
115 $result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values);
116 assert($result === 1);
117
118 // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
119 // the idea to trim them here.
120 $localColumns = array_map('trim', explode(',', $values[1]));
121 $foreignColumns = array_map('trim', explode(',', $values[3]));
122 $foreignTable = $values[2];
123
124 return new ForeignKeyConstraint(
125 $localColumns,
126 $foreignTable,
127 $foreignColumns,
128 $tableForeignKey['conname'],
129 ['onUpdate' => $onUpdate, 'onDelete' => $onDelete],
130 );
131 }
132
133 /**
134 * {@inheritDoc}
135 */
136 protected function _getPortableViewDefinition(array $view): View
137 {
138 return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']);
139 }
140
141 /**
142 * {@inheritDoc}
143 */
144 protected function _getPortableTableDefinition(array $table): string
145 {
146 $currentSchema = $this->getCurrentSchema();
147
148 if ($table['schema_name'] === $currentSchema) {
149 return $table['table_name'];
150 }
151
152 return $table['schema_name'] . '.' . $table['table_name'];
153 }
154
155 /**
156 * {@inheritDoc}
157 *
158 * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
159 */
160 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
161 {
162 $buffer = [];
163 foreach ($tableIndexes as $row) {
164 $colNumbers = array_map('intval', explode(' ', $row['indkey']));
165 $columnNameSql = sprintf(
166 'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC',
167 $row['indrelid'],
168 implode(' ,', $colNumbers),
169 );
170
171 $indexColumns = $this->connection->fetchAllAssociative($columnNameSql);
172
173 // required for getting the order of the columns right.
174 foreach ($colNumbers as $colNum) {
175 foreach ($indexColumns as $colRow) {
176 if ($colNum !== $colRow['attnum']) {
177 continue;
178 }
179
180 $buffer[] = [
181 'key_name' => $row['relname'],
182 'column_name' => trim($colRow['attname']),
183 'non_unique' => ! $row['indisunique'],
184 'primary' => $row['indisprimary'],
185 'where' => $row['where'],
186 ];
187 }
188 }
189 }
190
191 return parent::_getPortableTableIndexesList($buffer, $tableName);
192 }
193
194 /**
195 * {@inheritDoc}
196 */
197 protected function _getPortableDatabaseDefinition(array $database): string
198 {
199 return $database['datname'];
200 }
201
202 /**
203 * {@inheritDoc}
204 */
205 protected function _getPortableSequenceDefinition(array $sequence): Sequence
206 {
207 if ($sequence['schemaname'] !== 'public') {
208 $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname'];
209 } else {
210 $sequenceName = $sequence['relname'];
211 }
212
213 return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']);
214 }
215
216 /**
217 * {@inheritDoc}
218 */
219 protected function _getPortableTableColumnDefinition(array $tableColumn): Column
220 {
221 $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
222
223 $length = null;
224
225 if (
226 in_array(strtolower($tableColumn['type']), ['varchar', 'bpchar'], true)
227 && preg_match('/\((\d*)\)/', $tableColumn['complete_type'], $matches) === 1
228 ) {
229 $length = (int) $matches[1];
230 }
231
232 $autoincrement = $tableColumn['attidentity'] === 'd';
233
234 $matches = [];
235
236 assert(array_key_exists('default', $tableColumn));
237 assert(array_key_exists('complete_type', $tableColumn));
238
239 if ($tableColumn['default'] !== null) {
240 if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) {
241 $tableColumn['default'] = $matches[1];
242 } elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) {
243 $tableColumn['default'] = null;
244 }
245 }
246
247 if ($length === -1 && isset($tableColumn['atttypmod'])) {
248 $length = $tableColumn['atttypmod'] - 4;
249 }
250
251 if ((int) $length <= 0) {
252 $length = null;
253 }
254
255 $fixed = false;
256
257 if (! isset($tableColumn['name'])) {
258 $tableColumn['name'] = '';
259 }
260
261 $precision = null;
262 $scale = 0;
263 $jsonb = null;
264
265 $dbType = strtolower($tableColumn['type']);
266 if (
267 $tableColumn['domain_type'] !== null
268 && $tableColumn['domain_type'] !== ''
269 && ! $this->platform->hasDoctrineTypeMappingFor($tableColumn['type'])
270 ) {
271 $dbType = strtolower($tableColumn['domain_type']);
272 $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
273 }
274
275 $type = $this->platform->getDoctrineTypeMapping($dbType);
276
277 switch ($dbType) {
278 case 'smallint':
279 case 'int2':
280 case 'int':
281 case 'int4':
282 case 'integer':
283 case 'bigint':
284 case 'int8':
285 $length = null;
286 break;
287
288 case 'bool':
289 case 'boolean':
290 if ($tableColumn['default'] === 'true') {
291 $tableColumn['default'] = true;
292 }
293
294 if ($tableColumn['default'] === 'false') {
295 $tableColumn['default'] = false;
296 }
297
298 $length = null;
299 break;
300
301 case 'json':
302 case 'text':
303 case '_varchar':
304 case 'varchar':
305 $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']);
306 break;
307
308 case 'char':
309 case 'bpchar':
310 $fixed = true;
311 break;
312
313 case 'float':
314 case 'float4':
315 case 'float8':
316 case 'double':
317 case 'double precision':
318 case 'real':
319 case 'decimal':
320 case 'money':
321 case 'numeric':
322 if (
323 preg_match(
324 '([A-Za-z]+\(([0-9]+),([0-9]+)\))',
325 $tableColumn['complete_type'],
326 $match,
327 ) === 1
328 ) {
329 $precision = (int) $match[1];
330 $scale = (int) $match[2];
331 $length = null;
332 }
333
334 break;
335
336 case 'year':
337 $length = null;
338 break;
339
340 // PostgreSQL 9.4+ only
341 case 'jsonb':
342 $jsonb = true;
343 break;
344 }
345
346 if (
347 is_string($tableColumn['default']) && preg_match(
348 "('([^']+)'::)",
349 $tableColumn['default'],
350 $match,
351 ) === 1
352 ) {
353 $tableColumn['default'] = $match[1];
354 }
355
356 $options = [
357 'length' => $length,
358 'notnull' => (bool) $tableColumn['isnotnull'],
359 'default' => $tableColumn['default'],
360 'precision' => $precision,
361 'scale' => $scale,
362 'fixed' => $fixed,
363 'autoincrement' => $autoincrement,
364 ];
365
366 if (isset($tableColumn['comment'])) {
367 $options['comment'] = $tableColumn['comment'];
368 }
369
370 $column = new Column($tableColumn['field'], Type::getType($type), $options);
371
372 if (! empty($tableColumn['collation'])) {
373 $column->setPlatformOption('collation', $tableColumn['collation']);
374 }
375
376 if ($column->getType() instanceof JsonType) {
377 $column->setPlatformOption('jsonb', $jsonb);
378 }
379
380 return $column;
381 }
382
383 /**
384 * Parses a default value expression as given by PostgreSQL
385 */
386 private function parseDefaultExpression(?string $default): ?string
387 {
388 if ($default === null) {
389 return $default;
390 }
391
392 return str_replace("''", "'", $default);
393 }
394
395 protected function selectTableNames(string $databaseName): Result
396 {
397 $sql = <<<'SQL'
398SELECT quote_ident(table_name) AS table_name,
399 table_schema AS schema_name
400FROM information_schema.tables
401WHERE table_catalog = ?
402 AND table_schema NOT LIKE 'pg\_%'
403 AND table_schema != 'information_schema'
404 AND table_name != 'geometry_columns'
405 AND table_name != 'spatial_ref_sys'
406 AND table_type = 'BASE TABLE'
407SQL;
408
409 return $this->connection->executeQuery($sql, [$databaseName]);
410 }
411
412 protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
413 {
414 $sql = 'SELECT';
415
416 if ($tableName === null) {
417 $sql .= ' c.relname AS table_name, n.nspname AS schema_name,';
418 }
419
420 $sql .= <<<'SQL'
421 a.attnum,
422 quote_ident(a.attname) AS field,
423 t.typname AS type,
424 format_type(a.atttypid, a.atttypmod) AS complete_type,
425 (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation,
426 (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type,
427 (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM
428 pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type,
429 a.attnotnull AS isnotnull,
430 a.attidentity,
431 (SELECT 't'
432 FROM pg_index
433 WHERE c.oid = pg_index.indrelid
434 AND pg_index.indkey[0] = a.attnum
435 AND pg_index.indisprimary = 't'
436 ) AS pri,
437 (SELECT pg_get_expr(adbin, adrelid)
438 FROM pg_attrdef
439 WHERE c.oid = pg_attrdef.adrelid
440 AND pg_attrdef.adnum=a.attnum
441 ) AS default,
442 (SELECT pg_description.description
443 FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid
444 ) AS comment
445 FROM pg_attribute a
446 INNER JOIN pg_class c
447 ON c.oid = a.attrelid
448 INNER JOIN pg_type t
449 ON t.oid = a.atttypid
450 INNER JOIN pg_namespace n
451 ON n.oid = c.relnamespace
452 LEFT JOIN pg_depend d
453 ON d.objid = c.oid
454 AND d.deptype = 'e'
455 AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class')
456SQL;
457
458 $conditions = array_merge([
459 'a.attnum > 0',
460 "c.relkind = 'r'",
461 'd.refobjid IS NULL',
462 ], $this->buildQueryConditions($tableName));
463
464 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY a.attnum';
465
466 return $this->connection->executeQuery($sql);
467 }
468
469 protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
470 {
471 $sql = 'SELECT';
472
473 if ($tableName === null) {
474 $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
475 }
476
477 $sql .= <<<'SQL'
478 quote_ident(ic.relname) AS relname,
479 i.indisunique,
480 i.indisprimary,
481 i.indkey,
482 i.indrelid,
483 pg_get_expr(indpred, indrelid) AS "where"
484 FROM pg_index i
485 JOIN pg_class AS tc ON tc.oid = i.indrelid
486 JOIN pg_namespace tn ON tn.oid = tc.relnamespace
487 JOIN pg_class AS ic ON ic.oid = i.indexrelid
488 WHERE ic.oid IN (
489 SELECT indexrelid
490 FROM pg_index i, pg_class c, pg_namespace n
491SQL;
492
493 $conditions = array_merge([
494 'c.oid = i.indrelid',
495 'c.relnamespace = n.oid',
496 ], $this->buildQueryConditions($tableName));
497
498 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ')';
499
500 return $this->connection->executeQuery($sql);
501 }
502
503 protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
504 {
505 $sql = 'SELECT';
506
507 if ($tableName === null) {
508 $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,';
509 }
510
511 $sql .= <<<'SQL'
512 quote_ident(r.conname) as conname,
513 pg_get_constraintdef(r.oid, true) as condef
514 FROM pg_constraint r
515 JOIN pg_class AS tc ON tc.oid = r.conrelid
516 JOIN pg_namespace tn ON tn.oid = tc.relnamespace
517 WHERE r.conrelid IN
518 (
519 SELECT c.oid
520 FROM pg_class c, pg_namespace n
521SQL;
522
523 $conditions = array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName));
524
525 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f'";
526
527 return $this->connection->executeQuery($sql);
528 }
529
530 /**
531 * {@inheritDoc}
532 */
533 protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
534 {
535 $sql = <<<'SQL'
536SELECT c.relname,
537 CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged,
538 obj_description(c.oid, 'pg_class') AS comment
539FROM pg_class c
540 INNER JOIN pg_namespace n
541 ON n.oid = c.relnamespace
542SQL;
543
544 $conditions = array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName));
545
546 $sql .= ' WHERE ' . implode(' AND ', $conditions);
547
548 return $this->connection->fetchAllAssociativeIndexed($sql);
549 }
550
551 /** @return list<string> */
552 private function buildQueryConditions(?string $tableName): array
553 {
554 $conditions = [];
555
556 if ($tableName !== null) {
557 if (str_contains($tableName, '.')) {
558 [$schemaName, $tableName] = explode('.', $tableName);
559 $conditions[] = 'n.nspname = ' . $this->platform->quoteStringLiteral($schemaName);
560 } else {
561 $conditions[] = 'n.nspname = ANY(current_schemas(false))';
562 }
563
564 $identifier = new Identifier($tableName);
565 $conditions[] = 'c.relname = ' . $this->platform->quoteStringLiteral($identifier->getName());
566 }
567
568 $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')";
569
570 return $conditions;
571 }
572}
diff --git a/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php b/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php
new file mode 100644
index 0000000..e0a74ce
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php
@@ -0,0 +1,498 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\SQLServer;
9use Doctrine\DBAL\Platforms\SQLServerPlatform;
10use Doctrine\DBAL\Result;
11use Doctrine\DBAL\Types\Type;
12
13use function array_change_key_case;
14use function assert;
15use function explode;
16use function implode;
17use function is_string;
18use function preg_match;
19use function sprintf;
20use function str_contains;
21use function str_replace;
22use function strtok;
23
24use const CASE_LOWER;
25
26/**
27 * SQL Server Schema Manager.
28 *
29 * @extends AbstractSchemaManager<SQLServerPlatform>
30 */
31class SQLServerSchemaManager extends AbstractSchemaManager
32{
33 private ?string $databaseCollation = null;
34
35 /**
36 * {@inheritDoc}
37 */
38 public function listSchemaNames(): array
39 {
40 return $this->connection->fetchFirstColumn(
41 <<<'SQL'
42SELECT name
43FROM sys.schemas
44WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')
45SQL,
46 );
47 }
48
49 /**
50 * {@inheritDoc}
51 */
52 protected function _getPortableSequenceDefinition(array $sequence): Sequence
53 {
54 return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']);
55 }
56
57 /**
58 * {@inheritDoc}
59 */
60 protected function _getPortableTableColumnDefinition(array $tableColumn): Column
61 {
62 $dbType = strtok($tableColumn['type'], '(), ');
63 assert(is_string($dbType));
64
65 $length = (int) $tableColumn['length'];
66
67 $precision = null;
68
69 $scale = 0;
70 $fixed = false;
71
72 if (! isset($tableColumn['name'])) {
73 $tableColumn['name'] = '';
74 }
75
76 if ($tableColumn['scale'] !== null) {
77 $scale = (int) $tableColumn['scale'];
78 }
79
80 if ($tableColumn['precision'] !== null) {
81 $precision = (int) $tableColumn['precision'];
82 }
83
84 switch ($dbType) {
85 case 'nchar':
86 case 'ntext':
87 // Unicode data requires 2 bytes per character
88 $length /= 2;
89 break;
90
91 case 'nvarchar':
92 if ($length === -1) {
93 break;
94 }
95
96 // Unicode data requires 2 bytes per character
97 $length /= 2;
98 break;
99
100 case 'varchar':
101 // TEXT type is returned as VARCHAR(MAX) with a length of -1
102 if ($length === -1) {
103 $dbType = 'text';
104 }
105
106 break;
107
108 case 'varbinary':
109 if ($length === -1) {
110 $dbType = 'blob';
111 }
112
113 break;
114 }
115
116 if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') {
117 $fixed = true;
118 }
119
120 $type = $this->platform->getDoctrineTypeMapping($dbType);
121
122 $options = [
123 'fixed' => $fixed,
124 'notnull' => (bool) $tableColumn['notnull'],
125 'scale' => $scale,
126 'precision' => $precision,
127 'autoincrement' => (bool) $tableColumn['autoincrement'],
128 ];
129
130 if (isset($tableColumn['comment'])) {
131 $options['comment'] = $tableColumn['comment'];
132 }
133
134 if ($length !== 0 && ($type === 'text' || $type === 'string' || $type === 'binary')) {
135 $options['length'] = $length;
136 }
137
138 $column = new Column($tableColumn['name'], Type::getType($type), $options);
139
140 if ($tableColumn['default'] !== null) {
141 $default = $this->parseDefaultExpression($tableColumn['default']);
142
143 $column->setDefault($default);
144 $column->setPlatformOption(
145 SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME,
146 $tableColumn['df_name'],
147 );
148 }
149
150 if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') {
151 $column->setPlatformOption('collation', $tableColumn['collation']);
152 }
153
154 return $column;
155 }
156
157 private function parseDefaultExpression(string $value): ?string
158 {
159 while (preg_match('/^\((.*)\)$/s', $value, $matches)) {
160 $value = $matches[1];
161 }
162
163 if ($value === 'NULL') {
164 return null;
165 }
166
167 if (preg_match('/^\'(.*)\'$/s', $value, $matches) === 1) {
168 $value = str_replace("''", "'", $matches[1]);
169 }
170
171 if ($value === 'getdate()') {
172 return $this->platform->getCurrentTimestampSQL();
173 }
174
175 return $value;
176 }
177
178 /**
179 * {@inheritDoc}
180 */
181 protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array
182 {
183 $foreignKeys = [];
184
185 foreach ($tableForeignKeys as $tableForeignKey) {
186 $name = $tableForeignKey['ForeignKey'];
187
188 if (! isset($foreignKeys[$name])) {
189 $foreignKeys[$name] = [
190 'local_columns' => [$tableForeignKey['ColumnName']],
191 'foreign_table' => $tableForeignKey['ReferenceTableName'],
192 'foreign_columns' => [$tableForeignKey['ReferenceColumnName']],
193 'name' => $name,
194 'options' => [
195 'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']),
196 'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']),
197 ],
198 ];
199 } else {
200 $foreignKeys[$name]['local_columns'][] = $tableForeignKey['ColumnName'];
201 $foreignKeys[$name]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName'];
202 }
203 }
204
205 return parent::_getPortableTableForeignKeysList($foreignKeys);
206 }
207
208 /**
209 * {@inheritDoc}
210 */
211 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
212 {
213 foreach ($tableIndexes as &$tableIndex) {
214 $tableIndex['non_unique'] = (bool) $tableIndex['non_unique'];
215 $tableIndex['primary'] = (bool) $tableIndex['primary'];
216 $tableIndex['flags'] = $tableIndex['flags'] ? [$tableIndex['flags']] : null;
217 }
218
219 return parent::_getPortableTableIndexesList($tableIndexes, $tableName);
220 }
221
222 /**
223 * {@inheritDoc}
224 */
225 protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
226 {
227 return new ForeignKeyConstraint(
228 $tableForeignKey['local_columns'],
229 $tableForeignKey['foreign_table'],
230 $tableForeignKey['foreign_columns'],
231 $tableForeignKey['name'],
232 $tableForeignKey['options'],
233 );
234 }
235
236 /**
237 * {@inheritDoc}
238 */
239 protected function _getPortableTableDefinition(array $table): string
240 {
241 if ($table['schema_name'] !== 'dbo') {
242 return $table['schema_name'] . '.' . $table['table_name'];
243 }
244
245 return $table['table_name'];
246 }
247
248 /**
249 * {@inheritDoc}
250 */
251 protected function _getPortableDatabaseDefinition(array $database): string
252 {
253 return $database['name'];
254 }
255
256 /**
257 * {@inheritDoc}
258 */
259 protected function _getPortableViewDefinition(array $view): View
260 {
261 return new View($view['name'], $view['definition']);
262 }
263
264 /** @throws Exception */
265 public function createComparator(): Comparator
266 {
267 return new SQLServer\Comparator($this->platform, $this->getDatabaseCollation());
268 }
269
270 /** @throws Exception */
271 private function getDatabaseCollation(): string
272 {
273 if ($this->databaseCollation === null) {
274 $databaseCollation = $this->connection->fetchOne(
275 'SELECT collation_name FROM sys.databases WHERE name = '
276 . $this->platform->getCurrentDatabaseExpression(),
277 );
278
279 // a database is always selected, even if omitted in the connection parameters
280 assert(is_string($databaseCollation));
281
282 $this->databaseCollation = $databaseCollation;
283 }
284
285 return $this->databaseCollation;
286 }
287
288 protected function selectTableNames(string $databaseName): Result
289 {
290 // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
291 $sql = <<<'SQL'
292SELECT name AS table_name,
293 SCHEMA_NAME(schema_id) AS schema_name
294FROM sys.objects
295WHERE type = 'U'
296 AND name != 'sysdiagrams'
297ORDER BY name
298SQL;
299
300 return $this->connection->executeQuery($sql);
301 }
302
303 protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
304 {
305 $sql = 'SELECT';
306
307 if ($tableName === null) {
308 $sql .= ' obj.name AS table_name, scm.name AS schema_name,';
309 }
310
311 $sql .= <<<'SQL'
312 col.name,
313 type.name AS type,
314 col.max_length AS length,
315 ~col.is_nullable AS notnull,
316 def.definition AS [default],
317 def.name AS df_name,
318 col.scale,
319 col.precision,
320 col.is_identity AS autoincrement,
321 col.collation_name AS collation,
322 -- CAST avoids driver error for sql_variant type
323 CAST(prop.value AS NVARCHAR(MAX)) AS comment
324 FROM sys.columns AS col
325 JOIN sys.types AS type
326 ON col.user_type_id = type.user_type_id
327 JOIN sys.objects AS obj
328 ON col.object_id = obj.object_id
329 JOIN sys.schemas AS scm
330 ON obj.schema_id = scm.schema_id
331 LEFT JOIN sys.default_constraints def
332 ON col.default_object_id = def.object_id
333 AND col.object_id = def.parent_object_id
334 LEFT JOIN sys.extended_properties AS prop
335 ON obj.object_id = prop.major_id
336 AND col.column_id = prop.minor_id
337 AND prop.name = 'MS_Description'
338SQL;
339
340 // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams
341 $conditions = ["obj.type = 'U'", "obj.name != 'sysdiagrams'"];
342 $params = [];
343
344 if ($tableName !== null) {
345 $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'obj.name');
346 }
347
348 $sql .= ' WHERE ' . implode(' AND ', $conditions);
349
350 return $this->connection->executeQuery($sql, $params);
351 }
352
353 protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
354 {
355 $sql = 'SELECT';
356
357 if ($tableName === null) {
358 $sql .= ' tbl.name AS table_name, scm.name AS schema_name,';
359 }
360
361 $sql .= <<<'SQL'
362 idx.name AS key_name,
363 col.name AS column_name,
364 ~idx.is_unique AS non_unique,
365 idx.is_primary_key AS [primary],
366 CASE idx.type
367 WHEN '1' THEN 'clustered'
368 WHEN '2' THEN 'nonclustered'
369 ELSE NULL
370 END AS flags
371 FROM sys.tables AS tbl
372 JOIN sys.schemas AS scm
373 ON tbl.schema_id = scm.schema_id
374 JOIN sys.indexes AS idx
375 ON tbl.object_id = idx.object_id
376 JOIN sys.index_columns AS idxcol
377 ON idx.object_id = idxcol.object_id
378 AND idx.index_id = idxcol.index_id
379 JOIN sys.columns AS col
380 ON idxcol.object_id = col.object_id
381 AND idxcol.column_id = col.column_id
382SQL;
383
384 $conditions = [];
385 $params = [];
386
387 if ($tableName !== null) {
388 $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name');
389 $sql .= ' WHERE ' . implode(' AND ', $conditions);
390 }
391
392 $sql .= ' ORDER BY idx.index_id, idxcol.key_ordinal';
393
394 return $this->connection->executeQuery($sql, $params);
395 }
396
397 protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
398 {
399 $sql = 'SELECT';
400
401 if ($tableName === null) {
402 $sql .= ' OBJECT_NAME (f.parent_object_id) AS table_name, SCHEMA_NAME(f.schema_id) AS schema_name,';
403 }
404
405 $sql .= <<<'SQL'
406 f.name AS ForeignKey,
407 SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName,
408 OBJECT_NAME (f.parent_object_id) AS TableName,
409 COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName,
410 SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName,
411 OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName,
412 COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName,
413 f.delete_referential_action_desc,
414 f.update_referential_action_desc
415 FROM sys.foreign_keys AS f
416 INNER JOIN sys.foreign_key_columns AS fc
417 INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id
418 ON f.OBJECT_ID = fc.constraint_object_id
419SQL;
420
421 $conditions = [];
422 $params = [];
423
424 if ($tableName !== null) {
425 $conditions[] = $this->getTableWhereClause(
426 $tableName,
427 'SCHEMA_NAME(f.schema_id)',
428 'OBJECT_NAME(f.parent_object_id)',
429 );
430
431 $sql .= ' WHERE ' . implode(' AND ', $conditions);
432 }
433
434 $sql .= ' ORDER BY fc.constraint_column_id';
435
436 return $this->connection->executeQuery($sql, $params);
437 }
438
439 /**
440 * {@inheritDoc}
441 */
442 protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
443 {
444 $sql = <<<'SQL'
445 SELECT
446 tbl.name,
447 p.value AS [table_comment]
448 FROM
449 sys.tables AS tbl
450 INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1
451SQL;
452
453 $conditions = ["SCHEMA_NAME(tbl.schema_id) = N'dbo'", "p.name = N'MS_Description'"];
454 $params = [];
455
456 if ($tableName !== null) {
457 $conditions[] = "tbl.name = N'" . $tableName . "'";
458 }
459
460 $sql .= ' WHERE ' . implode(' AND ', $conditions);
461
462 /** @var array<string,array<string,mixed>> $metadata */
463 $metadata = $this->connection->executeQuery($sql, $params)
464 ->fetchAllAssociativeIndexed();
465
466 $tableOptions = [];
467 foreach ($metadata as $table => $data) {
468 $data = array_change_key_case($data, CASE_LOWER);
469
470 $tableOptions[$table] = [
471 'comment' => $data['table_comment'],
472 ];
473 }
474
475 return $tableOptions;
476 }
477
478 /**
479 * Returns the where clause to filter schema and table name in a query.
480 *
481 * @param string $table The full qualified name of the table.
482 * @param string $schemaColumn The name of the column to compare the schema to in the where clause.
483 * @param string $tableColumn The name of the column to compare the table to in the where clause.
484 */
485 private function getTableWhereClause(string $table, string $schemaColumn, string $tableColumn): string
486 {
487 if (str_contains($table, '.')) {
488 [$schema, $table] = explode('.', $table);
489 $schema = $this->platform->quoteStringLiteral($schema);
490 $table = $this->platform->quoteStringLiteral($table);
491 } else {
492 $schema = 'SCHEMA_NAME()';
493 $table = $this->platform->quoteStringLiteral($table);
494 }
495
496 return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema);
497 }
498}
diff --git a/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php b/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php
new file mode 100644
index 0000000..c001c25
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php
@@ -0,0 +1,620 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\SQLite;
9use Doctrine\DBAL\Platforms\SQLitePlatform;
10use Doctrine\DBAL\Result;
11use Doctrine\DBAL\Types\StringType;
12use Doctrine\DBAL\Types\TextType;
13use Doctrine\DBAL\Types\Type;
14
15use function array_change_key_case;
16use function array_merge;
17use function assert;
18use function count;
19use function implode;
20use function is_string;
21use function preg_match;
22use function preg_match_all;
23use function preg_quote;
24use function preg_replace;
25use function rtrim;
26use function str_contains;
27use function str_replace;
28use function str_starts_with;
29use function strcasecmp;
30use function strtolower;
31use function trim;
32use function usort;
33
34use const CASE_LOWER;
35
36/**
37 * SQLite SchemaManager.
38 *
39 * @extends AbstractSchemaManager<SQLitePlatform>
40 */
41class SQLiteSchemaManager extends AbstractSchemaManager
42{
43 /**
44 * {@inheritDoc}
45 */
46 protected function fetchForeignKeyColumnsByTable(string $databaseName): array
47 {
48 $columnsByTable = parent::fetchForeignKeyColumnsByTable($databaseName);
49
50 if (count($columnsByTable) > 0) {
51 foreach ($columnsByTable as $table => $columns) {
52 $columnsByTable[$table] = $this->addDetailsToTableForeignKeyColumns($table, $columns);
53 }
54 }
55
56 return $columnsByTable;
57 }
58
59 public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void
60 {
61 $table = $this->introspectTable($table);
62
63 $this->alterTable(new TableDiff($table, [], [], [], [], [], [], [], [], [$foreignKey], [], []));
64 }
65
66 public function dropForeignKey(string $name, string $table): void
67 {
68 $table = $this->introspectTable($table);
69
70 $foreignKey = $table->getForeignKey($name);
71
72 $this->alterTable(new TableDiff($table, [], [], [], [], [], [], [], [], [], [], [$foreignKey]));
73 }
74
75 /**
76 * {@inheritDoc}
77 */
78 public function listTableForeignKeys(string $table): array
79 {
80 $table = $this->normalizeName($table);
81
82 $columns = $this->selectForeignKeyColumns('main', $table)
83 ->fetchAllAssociative();
84
85 if (count($columns) > 0) {
86 $columns = $this->addDetailsToTableForeignKeyColumns($table, $columns);
87 }
88
89 return $this->_getPortableTableForeignKeysList($columns);
90 }
91
92 /**
93 * {@inheritDoc}
94 */
95 protected function _getPortableTableDefinition(array $table): string
96 {
97 return $table['table_name'];
98 }
99
100 /**
101 * {@inheritDoc}
102 *
103 * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
104 */
105 protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array
106 {
107 $indexBuffer = [];
108
109 // fetch primary
110 $indexArray = $this->connection->fetchAllAssociative('SELECT * FROM PRAGMA_TABLE_INFO (?)', [$tableName]);
111
112 usort(
113 $indexArray,
114 /**
115 * @param array<string,mixed> $a
116 * @param array<string,mixed> $b
117 */
118 static function (array $a, array $b): int {
119 if ($a['pk'] === $b['pk']) {
120 return $a['cid'] - $b['cid'];
121 }
122
123 return $a['pk'] - $b['pk'];
124 },
125 );
126
127 foreach ($indexArray as $indexColumnRow) {
128 if ($indexColumnRow['pk'] === 0 || $indexColumnRow['pk'] === '0') {
129 continue;
130 }
131
132 $indexBuffer[] = [
133 'key_name' => 'primary',
134 'primary' => true,
135 'non_unique' => false,
136 'column_name' => $indexColumnRow['name'],
137 ];
138 }
139
140 // fetch regular indexes
141 foreach ($tableIndexes as $tableIndex) {
142 // Ignore indexes with reserved names, e.g. autoindexes
143 if (str_starts_with($tableIndex['name'], 'sqlite_')) {
144 continue;
145 }
146
147 $keyName = $tableIndex['name'];
148 $idx = [];
149 $idx['key_name'] = $keyName;
150 $idx['primary'] = false;
151 $idx['non_unique'] = ! $tableIndex['unique'];
152
153 $indexArray = $this->connection->fetchAllAssociative('SELECT * FROM PRAGMA_INDEX_INFO (?)', [$keyName]);
154
155 foreach ($indexArray as $indexColumnRow) {
156 $idx['column_name'] = $indexColumnRow['name'];
157 $indexBuffer[] = $idx;
158 }
159 }
160
161 return parent::_getPortableTableIndexesList($indexBuffer, $tableName);
162 }
163
164 /**
165 * {@inheritDoc}
166 */
167 protected function _getPortableTableColumnList(string $table, string $database, array $tableColumns): array
168 {
169 $list = parent::_getPortableTableColumnList($table, $database, $tableColumns);
170
171 // find column with autoincrement
172 $autoincrementColumn = null;
173 $autoincrementCount = 0;
174
175 foreach ($tableColumns as $tableColumn) {
176 if ($tableColumn['pk'] === 0 || $tableColumn['pk'] === '0') {
177 continue;
178 }
179
180 $autoincrementCount++;
181 if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') {
182 continue;
183 }
184
185 $autoincrementColumn = $tableColumn['name'];
186 }
187
188 if ($autoincrementCount === 1 && $autoincrementColumn !== null) {
189 foreach ($list as $column) {
190 if ($autoincrementColumn !== $column->getName()) {
191 continue;
192 }
193
194 $column->setAutoincrement(true);
195 }
196 }
197
198 // inspect column collation and comments
199 $createSql = $this->getCreateTableSQL($table);
200
201 foreach ($list as $columnName => $column) {
202 $type = $column->getType();
203
204 if ($type instanceof StringType || $type instanceof TextType) {
205 $column->setPlatformOption(
206 'collation',
207 $this->parseColumnCollationFromSQL($columnName, $createSql) ?? 'BINARY',
208 );
209 }
210
211 $comment = $this->parseColumnCommentFromSQL($columnName, $createSql);
212
213 $column->setComment($comment);
214 }
215
216 return $list;
217 }
218
219 /**
220 * {@inheritDoc}
221 */
222 protected function _getPortableTableColumnDefinition(array $tableColumn): Column
223 {
224 preg_match('/^([^()]*)\\s*(\\(((\\d+)(,\\s*(\\d+))?)\\))?/', $tableColumn['type'], $matches);
225
226 $dbType = trim(strtolower($matches[1]));
227
228 $length = $precision = $unsigned = null;
229 $fixed = $unsigned = false;
230 $scale = 0;
231
232 if (count($matches) >= 6) {
233 $precision = (int) $matches[4];
234 $scale = (int) $matches[6];
235 } elseif (count($matches) >= 4) {
236 $length = (int) $matches[4];
237 }
238
239 if (str_contains($dbType, ' unsigned')) {
240 $dbType = str_replace(' unsigned', '', $dbType);
241 $unsigned = true;
242 }
243
244 $type = $this->platform->getDoctrineTypeMapping($dbType);
245 $default = $tableColumn['dflt_value'];
246 if ($default === 'NULL') {
247 $default = null;
248 }
249
250 if ($default !== null) {
251 // SQLite returns the default value as a literal expression, so we need to parse it
252 if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) {
253 $default = str_replace("''", "'", $matches[1]);
254 }
255 }
256
257 $notnull = (bool) $tableColumn['notnull'];
258
259 if (! isset($tableColumn['name'])) {
260 $tableColumn['name'] = '';
261 }
262
263 if ($dbType === 'char') {
264 $fixed = true;
265 }
266
267 $options = [
268 'length' => $length,
269 'unsigned' => $unsigned,
270 'fixed' => $fixed,
271 'notnull' => $notnull,
272 'default' => $default,
273 'precision' => $precision,
274 'scale' => $scale,
275 ];
276
277 return new Column($tableColumn['name'], Type::getType($type), $options);
278 }
279
280 /**
281 * {@inheritDoc}
282 */
283 protected function _getPortableViewDefinition(array $view): View
284 {
285 return new View($view['name'], $view['sql']);
286 }
287
288 /**
289 * {@inheritDoc}
290 */
291 protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array
292 {
293 $list = [];
294 foreach ($tableForeignKeys as $value) {
295 $value = array_change_key_case($value, CASE_LOWER);
296 $id = $value['id'];
297 if (! isset($list[$id])) {
298 if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') {
299 $value['on_delete'] = null;
300 }
301
302 if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') {
303 $value['on_update'] = null;
304 }
305
306 $list[$id] = [
307 'name' => $value['constraint_name'],
308 'local' => [],
309 'foreign' => [],
310 'foreignTable' => $value['table'],
311 'onDelete' => $value['on_delete'],
312 'onUpdate' => $value['on_update'],
313 'deferrable' => $value['deferrable'],
314 'deferred' => $value['deferred'],
315 ];
316 }
317
318 $list[$id]['local'][] = $value['from'];
319
320 if ($value['to'] === null) {
321 // Inferring a shorthand form for the foreign key constraint, where the "to" field is empty.
322 // @see https://www.sqlite.org/foreignkeys.html#fk_indexes.
323 $foreignTableIndexes = $this->_getPortableTableIndexesList([], $value['table']);
324
325 if (! isset($foreignTableIndexes['primary'])) {
326 continue;
327 }
328
329 $list[$id]['foreign'] = [...$list[$id]['foreign'], ...$foreignTableIndexes['primary']->getColumns()];
330
331 continue;
332 }
333
334 $list[$id]['foreign'][] = $value['to'];
335 }
336
337 return parent::_getPortableTableForeignKeysList($list);
338 }
339
340 /**
341 * {@inheritDoc}
342 */
343 protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint
344 {
345 return new ForeignKeyConstraint(
346 $tableForeignKey['local'],
347 $tableForeignKey['foreignTable'],
348 $tableForeignKey['foreign'],
349 $tableForeignKey['name'],
350 [
351 'onDelete' => $tableForeignKey['onDelete'],
352 'onUpdate' => $tableForeignKey['onUpdate'],
353 'deferrable' => $tableForeignKey['deferrable'],
354 'deferred' => $tableForeignKey['deferred'],
355 ],
356 );
357 }
358
359 private function parseColumnCollationFromSQL(string $column, string $sql): ?string
360 {
361 $pattern = '{(?:\W' . preg_quote($column) . '\W|\W'
362 . preg_quote($this->platform->quoteSingleIdentifier($column))
363 . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is';
364
365 if (preg_match($pattern, $sql, $match) !== 1) {
366 return null;
367 }
368
369 return $match[1];
370 }
371
372 private function parseTableCommentFromSQL(string $table, string $sql): ?string
373 {
374 $pattern = '/\s* # Allow whitespace characters at start of line
375CREATE\sTABLE # Match "CREATE TABLE"
376(?:\W"' . preg_quote($this->platform->quoteSingleIdentifier($table), '/') . '"\W|\W' . preg_quote($table, '/')
377 . '\W) # Match table name (quoted and unquoted)
378( # Start capture
379 (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s)
380)/ix';
381
382 if (preg_match($pattern, $sql, $match) !== 1) {
383 return null;
384 }
385
386 $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
387
388 return $comment === '' ? null : $comment;
389 }
390
391 private function parseColumnCommentFromSQL(string $column, string $sql): string
392 {
393 $pattern = '{[\s(,](?:\W' . preg_quote($this->platform->quoteSingleIdentifier($column))
394 . '\W|\W' . preg_quote($column) . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i';
395
396 if (preg_match($pattern, $sql, $match) !== 1) {
397 return '';
398 }
399
400 $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n"));
401 assert(is_string($comment));
402
403 return $comment;
404 }
405
406 /** @throws Exception */
407 private function getCreateTableSQL(string $table): string
408 {
409 $sql = $this->connection->fetchOne(
410 <<<'SQL'
411SELECT sql
412 FROM (
413 SELECT *
414 FROM sqlite_master
415 UNION ALL
416 SELECT *
417 FROM sqlite_temp_master
418 )
419WHERE type = 'table'
420AND name = ?
421SQL
422 ,
423 [$table],
424 );
425
426 if ($sql !== false) {
427 return $sql;
428 }
429
430 return '';
431 }
432
433 /**
434 * @param list<array<string,mixed>> $columns
435 *
436 * @return list<array<string,mixed>>
437 *
438 * @throws Exception
439 */
440 private function addDetailsToTableForeignKeyColumns(string $table, array $columns): array
441 {
442 $foreignKeyDetails = $this->getForeignKeyDetails($table);
443 $foreignKeyCount = count($foreignKeyDetails);
444
445 foreach ($columns as $i => $column) {
446 // SQLite identifies foreign keys in reverse order of appearance in SQL
447 $columns[$i] = array_merge($column, $foreignKeyDetails[$foreignKeyCount - $column['id'] - 1]);
448 }
449
450 return $columns;
451 }
452
453 /**
454 * @return list<array<string, mixed>>
455 *
456 * @throws Exception
457 */
458 private function getForeignKeyDetails(string $table): array
459 {
460 $createSql = $this->getCreateTableSQL($table);
461
462 if (
463 preg_match_all(
464 '#
465 (?:CONSTRAINT\s+(\S+)\s+)?
466 (?:FOREIGN\s+KEY[^)]+\)\s*)?
467 REFERENCES\s+\S+\s*(?:\([^)]+\))?
468 (?:
469 [^,]*?
470 (NOT\s+DEFERRABLE|DEFERRABLE)
471 (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?
472 )?#isx',
473 $createSql,
474 $match,
475 ) === 0
476 ) {
477 return [];
478 }
479
480 $names = $match[1];
481 $deferrable = $match[2];
482 $deferred = $match[3];
483 $details = [];
484
485 for ($i = 0, $count = count($match[0]); $i < $count; $i++) {
486 $details[] = [
487 'constraint_name' => $names[$i] ?? '',
488 'deferrable' => isset($deferrable[$i]) && strcasecmp($deferrable[$i], 'deferrable') === 0,
489 'deferred' => isset($deferred[$i]) && strcasecmp($deferred[$i], 'deferred') === 0,
490 ];
491 }
492
493 return $details;
494 }
495
496 public function createComparator(): Comparator
497 {
498 return new SQLite\Comparator($this->platform);
499 }
500
501 protected function selectTableNames(string $databaseName): Result
502 {
503 $sql = <<<'SQL'
504SELECT name AS table_name
505FROM sqlite_master
506WHERE type = 'table'
507 AND name != 'sqlite_sequence'
508 AND name != 'geometry_columns'
509 AND name != 'spatial_ref_sys'
510UNION ALL
511SELECT name
512FROM sqlite_temp_master
513WHERE type = 'table'
514ORDER BY name
515SQL;
516
517 return $this->connection->executeQuery($sql);
518 }
519
520 protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result
521 {
522 $sql = <<<'SQL'
523 SELECT t.name AS table_name,
524 c.*
525 FROM sqlite_master t
526 JOIN pragma_table_info(t.name) c
527SQL;
528
529 $conditions = [
530 "t.type = 'table'",
531 "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
532 ];
533 $params = [];
534
535 if ($tableName !== null) {
536 $conditions[] = 't.name = ?';
537 $params[] = str_replace('.', '__', $tableName);
538 }
539
540 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, c.cid';
541
542 return $this->connection->executeQuery($sql, $params);
543 }
544
545 protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result
546 {
547 $sql = <<<'SQL'
548 SELECT t.name AS table_name,
549 i.*
550 FROM sqlite_master t
551 JOIN pragma_index_list(t.name) i
552SQL;
553
554 $conditions = [
555 "t.type = 'table'",
556 "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
557 ];
558 $params = [];
559
560 if ($tableName !== null) {
561 $conditions[] = 't.name = ?';
562 $params[] = str_replace('.', '__', $tableName);
563 }
564
565 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, i.seq';
566
567 return $this->connection->executeQuery($sql, $params);
568 }
569
570 protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result
571 {
572 $sql = <<<'SQL'
573 SELECT t.name AS table_name,
574 p.*
575 FROM sqlite_master t
576 JOIN pragma_foreign_key_list(t.name) p
577 ON p."seq" != '-1'
578SQL;
579
580 $conditions = [
581 "t.type = 'table'",
582 "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')",
583 ];
584 $params = [];
585
586 if ($tableName !== null) {
587 $conditions[] = 't.name = ?';
588 $params[] = str_replace('.', '__', $tableName);
589 }
590
591 $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, p.id DESC, p.seq';
592
593 return $this->connection->executeQuery($sql, $params);
594 }
595
596 /**
597 * {@inheritDoc}
598 */
599 protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array
600 {
601 if ($tableName === null) {
602 $tables = $this->listTableNames();
603 } else {
604 $tables = [$tableName];
605 }
606
607 $tableOptions = [];
608 foreach ($tables as $table) {
609 $comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table));
610
611 if ($comment === null) {
612 continue;
613 }
614
615 $tableOptions[$table]['comment'] = $comment;
616 }
617
618 return $tableOptions;
619 }
620}
diff --git a/vendor/doctrine/dbal/src/Schema/Schema.php b/vendor/doctrine/dbal/src/Schema/Schema.php
new file mode 100644
index 0000000..25fe4a3
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Schema.php
@@ -0,0 +1,374 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\AbstractPlatform;
9use Doctrine\DBAL\Schema\Exception\NamespaceAlreadyExists;
10use Doctrine\DBAL\Schema\Exception\SequenceAlreadyExists;
11use Doctrine\DBAL\Schema\Exception\SequenceDoesNotExist;
12use Doctrine\DBAL\Schema\Exception\TableAlreadyExists;
13use Doctrine\DBAL\Schema\Exception\TableDoesNotExist;
14use Doctrine\DBAL\SQL\Builder\CreateSchemaObjectsSQLBuilder;
15use Doctrine\DBAL\SQL\Builder\DropSchemaObjectsSQLBuilder;
16
17use function array_values;
18use function str_contains;
19use function strtolower;
20
21/**
22 * Object representation of a database schema.
23 *
24 * Different vendors have very inconsistent naming with regard to the concept
25 * of a "schema". Doctrine understands a schema as the entity that conceptually
26 * wraps a set of database objects such as tables, sequences, indexes and
27 * foreign keys that belong to each other into a namespace. A Doctrine Schema
28 * has nothing to do with the "SCHEMA" defined as in PostgreSQL, it is more
29 * related to the concept of "DATABASE" that exists in MySQL and PostgreSQL.
30 *
31 * Every asset in the doctrine schema has a name. A name consists of either a
32 * namespace.local name pair or just a local unqualified name.
33 *
34 * The abstraction layer that covers a PostgreSQL schema is the namespace of an
35 * database object (asset). A schema can have a name, which will be used as
36 * default namespace for the unqualified database objects that are created in
37 * the schema.
38 *
39 * In the case of MySQL where cross-database queries are allowed this leads to
40 * databases being "misinterpreted" as namespaces. This is intentional, however
41 * the CREATE/DROP SQL visitors will just filter this queries and do not
42 * execute them. Only the queries for the currently connected database are
43 * executed.
44 */
45class Schema extends AbstractAsset
46{
47 /**
48 * The namespaces in this schema.
49 *
50 * @var array<string, string>
51 */
52 private array $namespaces = [];
53
54 /** @var array<string, Table> */
55 protected array $_tables = [];
56
57 /** @var array<string, Sequence> */
58 protected array $_sequences = [];
59
60 protected SchemaConfig $_schemaConfig;
61
62 /**
63 * @param array<Table> $tables
64 * @param array<Sequence> $sequences
65 * @param array<string> $namespaces
66 */
67 public function __construct(
68 array $tables = [],
69 array $sequences = [],
70 ?SchemaConfig $schemaConfig = null,
71 array $namespaces = [],
72 ) {
73 $schemaConfig ??= new SchemaConfig();
74
75 $this->_schemaConfig = $schemaConfig;
76
77 $name = $schemaConfig->getName();
78
79 if ($name !== null) {
80 $this->_setName($name);
81 }
82
83 foreach ($namespaces as $namespace) {
84 $this->createNamespace($namespace);
85 }
86
87 foreach ($tables as $table) {
88 $this->_addTable($table);
89 }
90
91 foreach ($sequences as $sequence) {
92 $this->_addSequence($sequence);
93 }
94 }
95
96 protected function _addTable(Table $table): void
97 {
98 $namespaceName = $table->getNamespaceName();
99 $tableName = $this->normalizeName($table);
100
101 if (isset($this->_tables[$tableName])) {
102 throw TableAlreadyExists::new($tableName);
103 }
104
105 if (
106 $namespaceName !== null
107 && ! $table->isInDefaultNamespace($this->getName())
108 && ! $this->hasNamespace($namespaceName)
109 ) {
110 $this->createNamespace($namespaceName);
111 }
112
113 $this->_tables[$tableName] = $table;
114 $table->setSchemaConfig($this->_schemaConfig);
115 }
116
117 protected function _addSequence(Sequence $sequence): void
118 {
119 $namespaceName = $sequence->getNamespaceName();
120 $seqName = $this->normalizeName($sequence);
121
122 if (isset($this->_sequences[$seqName])) {
123 throw SequenceAlreadyExists::new($seqName);
124 }
125
126 if (
127 $namespaceName !== null
128 && ! $sequence->isInDefaultNamespace($this->getName())
129 && ! $this->hasNamespace($namespaceName)
130 ) {
131 $this->createNamespace($namespaceName);
132 }
133
134 $this->_sequences[$seqName] = $sequence;
135 }
136
137 /**
138 * Returns the namespaces of this schema.
139 *
140 * @return list<string> A list of namespace names.
141 */
142 public function getNamespaces(): array
143 {
144 return array_values($this->namespaces);
145 }
146
147 /**
148 * Gets all tables of this schema.
149 *
150 * @return list<Table>
151 */
152 public function getTables(): array
153 {
154 return array_values($this->_tables);
155 }
156
157 public function getTable(string $name): Table
158 {
159 $name = $this->getFullQualifiedAssetName($name);
160 if (! isset($this->_tables[$name])) {
161 throw TableDoesNotExist::new($name);
162 }
163
164 return $this->_tables[$name];
165 }
166
167 private function getFullQualifiedAssetName(string $name): string
168 {
169 $name = $this->getUnquotedAssetName($name);
170
171 if (! str_contains($name, '.')) {
172 $name = $this->getName() . '.' . $name;
173 }
174
175 return strtolower($name);
176 }
177
178 /**
179 * The normalized name is qualified and lower-cased. Lower-casing is
180 * actually wrong, but we have to do it to keep our sanity. If you are
181 * using database objects that only differentiate in the casing (FOO vs
182 * Foo) then you will NOT be able to use Doctrine Schema abstraction.
183 *
184 * Every non-namespaced element is prefixed with this schema name.
185 */
186 private function normalizeName(AbstractAsset $asset): string
187 {
188 $name = $asset->getName();
189
190 if ($asset->getNamespaceName() === null) {
191 $name = $this->getName() . '.' . $name;
192 }
193
194 return strtolower($name);
195 }
196
197 /**
198 * Returns the unquoted representation of a given asset name.
199 */
200 private function getUnquotedAssetName(string $assetName): string
201 {
202 if ($this->isIdentifierQuoted($assetName)) {
203 return $this->trimQuotes($assetName);
204 }
205
206 return $assetName;
207 }
208
209 /**
210 * Does this schema have a namespace with the given name?
211 */
212 public function hasNamespace(string $name): bool
213 {
214 $name = strtolower($this->getUnquotedAssetName($name));
215
216 return isset($this->namespaces[$name]);
217 }
218
219 /**
220 * Does this schema have a table with the given name?
221 */
222 public function hasTable(string $name): bool
223 {
224 $name = $this->getFullQualifiedAssetName($name);
225
226 return isset($this->_tables[$name]);
227 }
228
229 public function hasSequence(string $name): bool
230 {
231 $name = $this->getFullQualifiedAssetName($name);
232
233 return isset($this->_sequences[$name]);
234 }
235
236 public function getSequence(string $name): Sequence
237 {
238 $name = $this->getFullQualifiedAssetName($name);
239 if (! $this->hasSequence($name)) {
240 throw SequenceDoesNotExist::new($name);
241 }
242
243 return $this->_sequences[$name];
244 }
245
246 /** @return list<Sequence> */
247 public function getSequences(): array
248 {
249 return array_values($this->_sequences);
250 }
251
252 /**
253 * Creates a new namespace.
254 *
255 * @return $this
256 */
257 public function createNamespace(string $name): self
258 {
259 $unquotedName = strtolower($this->getUnquotedAssetName($name));
260
261 if (isset($this->namespaces[$unquotedName])) {
262 throw NamespaceAlreadyExists::new($unquotedName);
263 }
264
265 $this->namespaces[$unquotedName] = $name;
266
267 return $this;
268 }
269
270 /**
271 * Creates a new table.
272 */
273 public function createTable(string $name): Table
274 {
275 $table = new Table($name);
276 $this->_addTable($table);
277
278 foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) {
279 $table->addOption($option, $value);
280 }
281
282 return $table;
283 }
284
285 /**
286 * Renames a table.
287 *
288 * @return $this
289 */
290 public function renameTable(string $oldName, string $newName): self
291 {
292 $table = $this->getTable($oldName);
293 $table->_setName($newName);
294
295 $this->dropTable($oldName);
296 $this->_addTable($table);
297
298 return $this;
299 }
300
301 /**
302 * Drops a table from the schema.
303 *
304 * @return $this
305 */
306 public function dropTable(string $name): self
307 {
308 $name = $this->getFullQualifiedAssetName($name);
309 $this->getTable($name);
310 unset($this->_tables[$name]);
311
312 return $this;
313 }
314
315 /**
316 * Creates a new sequence.
317 */
318 public function createSequence(string $name, int $allocationSize = 1, int $initialValue = 1): Sequence
319 {
320 $seq = new Sequence($name, $allocationSize, $initialValue);
321 $this->_addSequence($seq);
322
323 return $seq;
324 }
325
326 /** @return $this */
327 public function dropSequence(string $name): self
328 {
329 $name = $this->getFullQualifiedAssetName($name);
330 unset($this->_sequences[$name]);
331
332 return $this;
333 }
334
335 /**
336 * Returns an array of necessary SQL queries to create the schema on the given platform.
337 *
338 * @return list<string>
339 *
340 * @throws Exception
341 */
342 public function toSql(AbstractPlatform $platform): array
343 {
344 $builder = new CreateSchemaObjectsSQLBuilder($platform);
345
346 return $builder->buildSQL($this);
347 }
348
349 /**
350 * Return an array of necessary SQL queries to drop the schema on the given platform.
351 *
352 * @return list<string>
353 */
354 public function toDropSql(AbstractPlatform $platform): array
355 {
356 $builder = new DropSchemaObjectsSQLBuilder($platform);
357
358 return $builder->buildSQL($this);
359 }
360
361 /**
362 * Cloning a Schema triggers a deep clone of all related assets.
363 */
364 public function __clone()
365 {
366 foreach ($this->_tables as $k => $table) {
367 $this->_tables[$k] = clone $table;
368 }
369
370 foreach ($this->_sequences as $k => $sequence) {
371 $this->_sequences[$k] = clone $sequence;
372 }
373 }
374}
diff --git a/vendor/doctrine/dbal/src/Schema/SchemaConfig.php b/vendor/doctrine/dbal/src/Schema/SchemaConfig.php
new file mode 100644
index 0000000..86cc84f
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/SchemaConfig.php
@@ -0,0 +1,61 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7/**
8 * Configuration for a Schema.
9 */
10class SchemaConfig
11{
12 protected int $maxIdentifierLength = 63;
13
14 protected ?string $name = null;
15
16 /** @var array<string, mixed> */
17 protected array $defaultTableOptions = [];
18
19 public function setMaxIdentifierLength(int $length): void
20 {
21 $this->maxIdentifierLength = $length;
22 }
23
24 public function getMaxIdentifierLength(): int
25 {
26 return $this->maxIdentifierLength;
27 }
28
29 /**
30 * Gets the default namespace of schema objects.
31 */
32 public function getName(): ?string
33 {
34 return $this->name;
35 }
36
37 /**
38 * Sets the default namespace name of schema objects.
39 */
40 public function setName(?string $name): void
41 {
42 $this->name = $name;
43 }
44
45 /**
46 * Gets the default options that are passed to Table instances created with
47 * Schema#createTable().
48 *
49 * @return array<string, mixed>
50 */
51 public function getDefaultTableOptions(): array
52 {
53 return $this->defaultTableOptions;
54 }
55
56 /** @param array<string, mixed> $defaultTableOptions */
57 public function setDefaultTableOptions(array $defaultTableOptions): void
58 {
59 $this->defaultTableOptions = $defaultTableOptions;
60 }
61}
diff --git a/vendor/doctrine/dbal/src/Schema/SchemaDiff.php b/vendor/doctrine/dbal/src/Schema/SchemaDiff.php
new file mode 100644
index 0000000..28c014d
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/SchemaDiff.php
@@ -0,0 +1,109 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use function array_filter;
8use function count;
9
10/**
11 * Differences between two schemas.
12 */
13class SchemaDiff
14{
15 /** @var array<TableDiff> */
16 private readonly array $alteredTables;
17
18 /**
19 * Constructs an SchemaDiff object.
20 *
21 * @internal The diff can be only instantiated by a {@see Comparator}.
22 *
23 * @param array<string> $createdSchemas
24 * @param array<string> $droppedSchemas
25 * @param array<Table> $createdTables
26 * @param array<TableDiff> $alteredTables
27 * @param array<Table> $droppedTables
28 * @param array<Sequence> $createdSequences
29 * @param array<Sequence> $alteredSequences
30 * @param array<Sequence> $droppedSequences
31 */
32 public function __construct(
33 private readonly array $createdSchemas,
34 private readonly array $droppedSchemas,
35 private readonly array $createdTables,
36 array $alteredTables,
37 private readonly array $droppedTables,
38 private readonly array $createdSequences,
39 private readonly array $alteredSequences,
40 private readonly array $droppedSequences,
41 ) {
42 $this->alteredTables = array_filter($alteredTables, static function (TableDiff $diff): bool {
43 return ! $diff->isEmpty();
44 });
45 }
46
47 /** @return array<string> */
48 public function getCreatedSchemas(): array
49 {
50 return $this->createdSchemas;
51 }
52
53 /** @return array<string> */
54 public function getDroppedSchemas(): array
55 {
56 return $this->droppedSchemas;
57 }
58
59 /** @return array<Table> */
60 public function getCreatedTables(): array
61 {
62 return $this->createdTables;
63 }
64
65 /** @return array<TableDiff> */
66 public function getAlteredTables(): array
67 {
68 return $this->alteredTables;
69 }
70
71 /** @return array<Table> */
72 public function getDroppedTables(): array
73 {
74 return $this->droppedTables;
75 }
76
77 /** @return array<Sequence> */
78 public function getCreatedSequences(): array
79 {
80 return $this->createdSequences;
81 }
82
83 /** @return array<Sequence> */
84 public function getAlteredSequences(): array
85 {
86 return $this->alteredSequences;
87 }
88
89 /** @return array<Sequence> */
90 public function getDroppedSequences(): array
91 {
92 return $this->droppedSequences;
93 }
94
95 /**
96 * Returns whether the diff is empty (contains no changes).
97 */
98 public function isEmpty(): bool
99 {
100 return count($this->createdSchemas) === 0
101 && count($this->droppedSchemas) === 0
102 && count($this->createdTables) === 0
103 && count($this->alteredTables) === 0
104 && count($this->droppedTables) === 0
105 && count($this->createdSequences) === 0
106 && count($this->alteredSequences) === 0
107 && count($this->droppedSequences) === 0;
108 }
109}
diff --git a/vendor/doctrine/dbal/src/Schema/SchemaException.php b/vendor/doctrine/dbal/src/Schema/SchemaException.php
new file mode 100644
index 0000000..43dd2ad
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/SchemaException.php
@@ -0,0 +1,12 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8
9/** @psalm-immutable */
10interface SchemaException extends Exception
11{
12}
diff --git a/vendor/doctrine/dbal/src/Schema/SchemaManagerFactory.php b/vendor/doctrine/dbal/src/Schema/SchemaManagerFactory.php
new file mode 100644
index 0000000..37c32e0
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/SchemaManagerFactory.php
@@ -0,0 +1,17 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Connection;
8
9/**
10 * Creates a schema manager for the given connection.
11 *
12 * This interface is an extension point for applications that need to override schema managers.
13 */
14interface SchemaManagerFactory
15{
16 public function createSchemaManager(Connection $connection): AbstractSchemaManager;
17}
diff --git a/vendor/doctrine/dbal/src/Schema/Sequence.php b/vendor/doctrine/dbal/src/Schema/Sequence.php
new file mode 100644
index 0000000..32a5e67
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Sequence.php
@@ -0,0 +1,98 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use function count;
8use function sprintf;
9
10/**
11 * Sequence structure.
12 */
13class Sequence extends AbstractAsset
14{
15 protected int $allocationSize = 1;
16
17 protected int $initialValue = 1;
18
19 public function __construct(
20 string $name,
21 int $allocationSize = 1,
22 int $initialValue = 1,
23 protected ?int $cache = null,
24 ) {
25 $this->_setName($name);
26 $this->setAllocationSize($allocationSize);
27 $this->setInitialValue($initialValue);
28 }
29
30 public function getAllocationSize(): int
31 {
32 return $this->allocationSize;
33 }
34
35 public function getInitialValue(): int
36 {
37 return $this->initialValue;
38 }
39
40 public function getCache(): ?int
41 {
42 return $this->cache;
43 }
44
45 public function setAllocationSize(int $allocationSize): self
46 {
47 $this->allocationSize = $allocationSize;
48
49 return $this;
50 }
51
52 public function setInitialValue(int $initialValue): self
53 {
54 $this->initialValue = $initialValue;
55
56 return $this;
57 }
58
59 public function setCache(int $cache): self
60 {
61 $this->cache = $cache;
62
63 return $this;
64 }
65
66 /**
67 * Checks if this sequence is an autoincrement sequence for a given table.
68 *
69 * This is used inside the comparator to not report sequences as missing,
70 * when the "from" schema implicitly creates the sequences.
71 */
72 public function isAutoIncrementsFor(Table $table): bool
73 {
74 $primaryKey = $table->getPrimaryKey();
75
76 if ($primaryKey === null) {
77 return false;
78 }
79
80 $pkColumns = $primaryKey->getColumns();
81
82 if (count($pkColumns) !== 1) {
83 return false;
84 }
85
86 $column = $table->getColumn($pkColumns[0]);
87
88 if (! $column->getAutoincrement()) {
89 return false;
90 }
91
92 $sequenceName = $this->getShortestName($table->getNamespaceName());
93 $tableName = $table->getShortestName($table->getNamespaceName());
94 $tableSequenceName = sprintf('%s_%s_seq', $tableName, $column->getShortestName($table->getNamespaceName()));
95
96 return $tableSequenceName === $sequenceName;
97 }
98}
diff --git a/vendor/doctrine/dbal/src/Schema/Table.php b/vendor/doctrine/dbal/src/Schema/Table.php
new file mode 100644
index 0000000..cc7f04d
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/Table.php
@@ -0,0 +1,753 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Schema\Exception\ColumnAlreadyExists;
8use Doctrine\DBAL\Schema\Exception\ColumnDoesNotExist;
9use Doctrine\DBAL\Schema\Exception\ForeignKeyDoesNotExist;
10use Doctrine\DBAL\Schema\Exception\IndexAlreadyExists;
11use Doctrine\DBAL\Schema\Exception\IndexDoesNotExist;
12use Doctrine\DBAL\Schema\Exception\IndexNameInvalid;
13use Doctrine\DBAL\Schema\Exception\InvalidTableName;
14use Doctrine\DBAL\Schema\Exception\UniqueConstraintDoesNotExist;
15use Doctrine\DBAL\Types\Type;
16
17use function array_merge;
18use function array_values;
19use function in_array;
20use function is_string;
21use function preg_match;
22use function strtolower;
23
24/**
25 * Object Representation of a table.
26 */
27class Table extends AbstractAsset
28{
29 /** @var Column[] */
30 protected array $_columns = [];
31
32 /** @var Index[] */
33 private array $implicitIndexes = [];
34
35 /** @var Index[] */
36 protected array $_indexes = [];
37
38 protected ?string $_primaryKeyName = null;
39
40 /** @var UniqueConstraint[] */
41 protected array $uniqueConstraints = [];
42
43 /** @var ForeignKeyConstraint[] */
44 protected array $_fkConstraints = [];
45
46 /** @var mixed[] */
47 protected array $_options = [
48 'create_options' => [],
49 ];
50
51 protected ?SchemaConfig $_schemaConfig = null;
52
53 /**
54 * @param array<Column> $columns
55 * @param array<Index> $indexes
56 * @param array<UniqueConstraint> $uniqueConstraints
57 * @param array<ForeignKeyConstraint> $fkConstraints
58 * @param array<string, mixed> $options
59 */
60 public function __construct(
61 string $name,
62 array $columns = [],
63 array $indexes = [],
64 array $uniqueConstraints = [],
65 array $fkConstraints = [],
66 array $options = [],
67 ) {
68 if ($name === '') {
69 throw InvalidTableName::new($name);
70 }
71
72 $this->_setName($name);
73
74 foreach ($columns as $column) {
75 $this->_addColumn($column);
76 }
77
78 foreach ($indexes as $idx) {
79 $this->_addIndex($idx);
80 }
81
82 foreach ($uniqueConstraints as $uniqueConstraint) {
83 $this->_addUniqueConstraint($uniqueConstraint);
84 }
85
86 foreach ($fkConstraints as $fkConstraint) {
87 $this->_addForeignKeyConstraint($fkConstraint);
88 }
89
90 $this->_options = array_merge($this->_options, $options);
91 }
92
93 public function setSchemaConfig(SchemaConfig $schemaConfig): void
94 {
95 $this->_schemaConfig = $schemaConfig;
96 }
97
98 /**
99 * Sets the Primary Key.
100 *
101 * @param array<int, string> $columnNames
102 */
103 public function setPrimaryKey(array $columnNames, ?string $indexName = null): self
104 {
105 if ($indexName === null) {
106 $indexName = 'primary';
107 }
108
109 $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true));
110
111 foreach ($columnNames as $columnName) {
112 $column = $this->getColumn($columnName);
113 $column->setNotnull(true);
114 }
115
116 return $this;
117 }
118
119 /**
120 * @param array<int, string> $columnNames
121 * @param array<int, string> $flags
122 * @param array<string, mixed> $options
123 */
124 public function addUniqueConstraint(
125 array $columnNames,
126 ?string $indexName = null,
127 array $flags = [],
128 array $options = [],
129 ): self {
130 $indexName ??= $this->_generateIdentifierName(
131 array_merge([$this->getName()], $columnNames),
132 'uniq',
133 $this->_getMaxIdentifierLength(),
134 );
135
136 return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options));
137 }
138
139 /**
140 * @param array<int, string> $columnNames
141 * @param array<int, string> $flags
142 * @param array<string, mixed> $options
143 */
144 public function addIndex(
145 array $columnNames,
146 ?string $indexName = null,
147 array $flags = [],
148 array $options = [],
149 ): self {
150 $indexName ??= $this->_generateIdentifierName(
151 array_merge([$this->getName()], $columnNames),
152 'idx',
153 $this->_getMaxIdentifierLength(),
154 );
155
156 return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options));
157 }
158
159 /**
160 * Drops the primary key from this table.
161 */
162 public function dropPrimaryKey(): void
163 {
164 if ($this->_primaryKeyName === null) {
165 return;
166 }
167
168 $this->dropIndex($this->_primaryKeyName);
169 $this->_primaryKeyName = null;
170 }
171
172 /**
173 * Drops an index from this table.
174 */
175 public function dropIndex(string $name): void
176 {
177 $name = $this->normalizeIdentifier($name);
178
179 if (! $this->hasIndex($name)) {
180 throw IndexDoesNotExist::new($name, $this->_name);
181 }
182
183 unset($this->_indexes[$name]);
184 }
185
186 /**
187 * @param array<int, string> $columnNames
188 * @param array<string, mixed> $options
189 */
190 public function addUniqueIndex(array $columnNames, ?string $indexName = null, array $options = []): self
191 {
192 $indexName ??= $this->_generateIdentifierName(
193 array_merge([$this->getName()], $columnNames),
194 'uniq',
195 $this->_getMaxIdentifierLength(),
196 );
197
198 return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options));
199 }
200
201 /**
202 * Renames an index.
203 *
204 * @param string $oldName The name of the index to rename from.
205 * @param string|null $newName The name of the index to rename to. If null is given, the index name
206 * will be auto-generated.
207 */
208 public function renameIndex(string $oldName, ?string $newName = null): self
209 {
210 $oldName = $this->normalizeIdentifier($oldName);
211 $normalizedNewName = $this->normalizeIdentifier($newName);
212
213 if ($oldName === $normalizedNewName) {
214 return $this;
215 }
216
217 if (! $this->hasIndex($oldName)) {
218 throw IndexDoesNotExist::new($oldName, $this->_name);
219 }
220
221 if ($this->hasIndex($normalizedNewName)) {
222 throw IndexAlreadyExists::new($normalizedNewName, $this->_name);
223 }
224
225 $oldIndex = $this->_indexes[$oldName];
226
227 if ($oldIndex->isPrimary()) {
228 $this->dropPrimaryKey();
229
230 return $this->setPrimaryKey($oldIndex->getColumns(), $newName ?? null);
231 }
232
233 unset($this->_indexes[$oldName]);
234
235 if ($oldIndex->isUnique()) {
236 return $this->addUniqueIndex($oldIndex->getColumns(), $newName, $oldIndex->getOptions());
237 }
238
239 return $this->addIndex($oldIndex->getColumns(), $newName, $oldIndex->getFlags(), $oldIndex->getOptions());
240 }
241
242 /**
243 * Checks if an index begins in the order of the given columns.
244 *
245 * @param array<int, string> $columnNames
246 */
247 public function columnsAreIndexed(array $columnNames): bool
248 {
249 foreach ($this->getIndexes() as $index) {
250 if ($index->spansColumns($columnNames)) {
251 return true;
252 }
253 }
254
255 return false;
256 }
257
258 /** @param array<string, mixed> $options */
259 public function addColumn(string $name, string $typeName, array $options = []): Column
260 {
261 $column = new Column($name, Type::getType($typeName), $options);
262
263 $this->_addColumn($column);
264
265 return $column;
266 }
267
268 /** @param array<string, mixed> $options */
269 public function modifyColumn(string $name, array $options): self
270 {
271 $column = $this->getColumn($name);
272 $column->setOptions($options);
273
274 return $this;
275 }
276
277 /**
278 * Drops a Column from the Table.
279 */
280 public function dropColumn(string $name): self
281 {
282 $name = $this->normalizeIdentifier($name);
283
284 unset($this->_columns[$name]);
285
286 return $this;
287 }
288
289 /**
290 * Adds a foreign key constraint.
291 *
292 * Name is inferred from the local columns.
293 *
294 * @param array<int, string> $localColumnNames
295 * @param array<int, string> $foreignColumnNames
296 * @param array<string, mixed> $options
297 */
298 public function addForeignKeyConstraint(
299 string $foreignTableName,
300 array $localColumnNames,
301 array $foreignColumnNames,
302 array $options = [],
303 ?string $name = null,
304 ): self {
305 $name ??= $this->_generateIdentifierName(
306 array_merge([$this->getName()], $localColumnNames),
307 'fk',
308 $this->_getMaxIdentifierLength(),
309 );
310
311 foreach ($localColumnNames as $columnName) {
312 if (! $this->hasColumn($columnName)) {
313 throw ColumnDoesNotExist::new($columnName, $this->_name);
314 }
315 }
316
317 $constraint = new ForeignKeyConstraint(
318 $localColumnNames,
319 $foreignTableName,
320 $foreignColumnNames,
321 $name,
322 $options,
323 );
324
325 return $this->_addForeignKeyConstraint($constraint);
326 }
327
328 public function addOption(string $name, mixed $value): self
329 {
330 $this->_options[$name] = $value;
331
332 return $this;
333 }
334
335 /**
336 * Returns whether this table has a foreign key constraint with the given name.
337 */
338 public function hasForeignKey(string $name): bool
339 {
340 $name = $this->normalizeIdentifier($name);
341
342 return isset($this->_fkConstraints[$name]);
343 }
344
345 /**
346 * Returns the foreign key constraint with the given name.
347 */
348 public function getForeignKey(string $name): ForeignKeyConstraint
349 {
350 $name = $this->normalizeIdentifier($name);
351
352 if (! $this->hasForeignKey($name)) {
353 throw ForeignKeyDoesNotExist::new($name, $this->_name);
354 }
355
356 return $this->_fkConstraints[$name];
357 }
358
359 /**
360 * Removes the foreign key constraint with the given name.
361 */
362 public function removeForeignKey(string $name): void
363 {
364 $name = $this->normalizeIdentifier($name);
365
366 if (! $this->hasForeignKey($name)) {
367 throw ForeignKeyDoesNotExist::new($name, $this->_name);
368 }
369
370 unset($this->_fkConstraints[$name]);
371 }
372
373 /**
374 * Returns whether this table has a unique constraint with the given name.
375 */
376 public function hasUniqueConstraint(string $name): bool
377 {
378 $name = $this->normalizeIdentifier($name);
379
380 return isset($this->uniqueConstraints[$name]);
381 }
382
383 /**
384 * Returns the unique constraint with the given name.
385 */
386 public function getUniqueConstraint(string $name): UniqueConstraint
387 {
388 $name = $this->normalizeIdentifier($name);
389
390 if (! $this->hasUniqueConstraint($name)) {
391 throw UniqueConstraintDoesNotExist::new($name, $this->_name);
392 }
393
394 return $this->uniqueConstraints[$name];
395 }
396
397 /**
398 * Removes the unique constraint with the given name.
399 */
400 public function removeUniqueConstraint(string $name): void
401 {
402 $name = $this->normalizeIdentifier($name);
403
404 if (! $this->hasUniqueConstraint($name)) {
405 throw UniqueConstraintDoesNotExist::new($name, $this->_name);
406 }
407
408 unset($this->uniqueConstraints[$name]);
409 }
410
411 /**
412 * Returns the list of table columns.
413 *
414 * @return list<Column>
415 */
416 public function getColumns(): array
417 {
418 return array_values($this->_columns);
419 }
420
421 /**
422 * Returns whether this table has a Column with the given name.
423 */
424 public function hasColumn(string $name): bool
425 {
426 $name = $this->normalizeIdentifier($name);
427
428 return isset($this->_columns[$name]);
429 }
430
431 /**
432 * Returns the Column with the given name.
433 */
434 public function getColumn(string $name): Column
435 {
436 $name = $this->normalizeIdentifier($name);
437
438 if (! $this->hasColumn($name)) {
439 throw ColumnDoesNotExist::new($name, $this->_name);
440 }
441
442 return $this->_columns[$name];
443 }
444
445 /**
446 * Returns the primary key.
447 */
448 public function getPrimaryKey(): ?Index
449 {
450 if ($this->_primaryKeyName !== null) {
451 return $this->getIndex($this->_primaryKeyName);
452 }
453
454 return null;
455 }
456
457 /**
458 * Returns whether this table has an Index with the given name.
459 */
460 public function hasIndex(string $name): bool
461 {
462 $name = $this->normalizeIdentifier($name);
463
464 return isset($this->_indexes[$name]);
465 }
466
467 /**
468 * Returns the Index with the given name.
469 */
470 public function getIndex(string $name): Index
471 {
472 $name = $this->normalizeIdentifier($name);
473
474 if (! $this->hasIndex($name)) {
475 throw IndexDoesNotExist::new($name, $this->_name);
476 }
477
478 return $this->_indexes[$name];
479 }
480
481 /** @return array<string, Index> */
482 public function getIndexes(): array
483 {
484 return $this->_indexes;
485 }
486
487 /**
488 * Returns the unique constraints.
489 *
490 * @return array<string, UniqueConstraint>
491 */
492 public function getUniqueConstraints(): array
493 {
494 return $this->uniqueConstraints;
495 }
496
497 /**
498 * Returns the foreign key constraints.
499 *
500 * @return array<string, ForeignKeyConstraint>
501 */
502 public function getForeignKeys(): array
503 {
504 return $this->_fkConstraints;
505 }
506
507 public function hasOption(string $name): bool
508 {
509 return isset($this->_options[$name]);
510 }
511
512 public function getOption(string $name): mixed
513 {
514 return $this->_options[$name] ?? null;
515 }
516
517 /** @return array<string, mixed> */
518 public function getOptions(): array
519 {
520 return $this->_options;
521 }
522
523 /**
524 * Clone of a Table triggers a deep clone of all affected assets.
525 */
526 public function __clone()
527 {
528 foreach ($this->_columns as $k => $column) {
529 $this->_columns[$k] = clone $column;
530 }
531
532 foreach ($this->_indexes as $k => $index) {
533 $this->_indexes[$k] = clone $index;
534 }
535
536 foreach ($this->_fkConstraints as $k => $fk) {
537 $this->_fkConstraints[$k] = clone $fk;
538 }
539 }
540
541 protected function _getMaxIdentifierLength(): int
542 {
543 return $this->_schemaConfig instanceof SchemaConfig
544 ? $this->_schemaConfig->getMaxIdentifierLength()
545 : 63;
546 }
547
548 protected function _addColumn(Column $column): void
549 {
550 $columnName = $column->getName();
551 $columnName = $this->normalizeIdentifier($columnName);
552
553 if (isset($this->_columns[$columnName])) {
554 throw ColumnAlreadyExists::new($this->getName(), $columnName);
555 }
556
557 $this->_columns[$columnName] = $column;
558 }
559
560 /**
561 * Adds an index to the table.
562 */
563 protected function _addIndex(Index $indexCandidate): self
564 {
565 $indexName = $indexCandidate->getName();
566 $indexName = $this->normalizeIdentifier($indexName);
567 $replacedImplicitIndexes = [];
568
569 foreach ($this->implicitIndexes as $name => $implicitIndex) {
570 if (! $implicitIndex->isFulfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) {
571 continue;
572 }
573
574 $replacedImplicitIndexes[] = $name;
575 }
576
577 if (
578 (isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) ||
579 ($this->_primaryKeyName !== null && $indexCandidate->isPrimary())
580 ) {
581 throw IndexAlreadyExists::new($indexName, $this->_name);
582 }
583
584 foreach ($replacedImplicitIndexes as $name) {
585 unset($this->_indexes[$name], $this->implicitIndexes[$name]);
586 }
587
588 if ($indexCandidate->isPrimary()) {
589 $this->_primaryKeyName = $indexName;
590 }
591
592 $this->_indexes[$indexName] = $indexCandidate;
593
594 return $this;
595 }
596
597 protected function _addUniqueConstraint(UniqueConstraint $constraint): self
598 {
599 $name = $constraint->getName() !== ''
600 ? $constraint->getName()
601 : $this->_generateIdentifierName(
602 array_merge((array) $this->getName(), $constraint->getColumns()),
603 'fk',
604 $this->_getMaxIdentifierLength(),
605 );
606
607 $name = $this->normalizeIdentifier($name);
608
609 $this->uniqueConstraints[$name] = $constraint;
610
611 // If there is already an index that fulfills this requirements drop the request. In the case of __construct
612 // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
613 // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
614 $indexName = $this->_generateIdentifierName(
615 array_merge([$this->getName()], $constraint->getColumns()),
616 'idx',
617 $this->_getMaxIdentifierLength(),
618 );
619
620 $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false);
621
622 foreach ($this->_indexes as $existingIndex) {
623 if ($indexCandidate->isFulfilledBy($existingIndex)) {
624 return $this;
625 }
626 }
627
628 $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
629
630 return $this;
631 }
632
633 protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint): self
634 {
635 $name = $constraint->getName() !== ''
636 ? $constraint->getName()
637 : $this->_generateIdentifierName(
638 array_merge((array) $this->getName(), $constraint->getLocalColumns()),
639 'fk',
640 $this->_getMaxIdentifierLength(),
641 );
642
643 $name = $this->normalizeIdentifier($name);
644
645 $this->_fkConstraints[$name] = $constraint;
646
647 // add an explicit index on the foreign key columns.
648 // If there is already an index that fulfills this requirements drop the request. In the case of __construct
649 // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates.
650 // This creates computation overhead in this case, however no duplicate indexes are ever added (column based).
651 $indexName = $this->_generateIdentifierName(
652 array_merge([$this->getName()], $constraint->getLocalColumns()),
653 'idx',
654 $this->_getMaxIdentifierLength(),
655 );
656
657 $indexCandidate = $this->_createIndex($constraint->getLocalColumns(), $indexName, false, false);
658
659 foreach ($this->_indexes as $existingIndex) {
660 if ($indexCandidate->isFulfilledBy($existingIndex)) {
661 return $this;
662 }
663 }
664
665 $this->_addIndex($indexCandidate);
666 $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate;
667
668 return $this;
669 }
670
671 /**
672 * Normalizes a given identifier.
673 *
674 * Trims quotes and lowercases the given identifier.
675 */
676 private function normalizeIdentifier(?string $identifier): string
677 {
678 if ($identifier === null) {
679 return '';
680 }
681
682 return $this->trimQuotes(strtolower($identifier));
683 }
684
685 public function setComment(string $comment): self
686 {
687 // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options.
688 $this->addOption('comment', $comment);
689
690 return $this;
691 }
692
693 public function getComment(): ?string
694 {
695 return $this->_options['comment'] ?? null;
696 }
697
698 /**
699 * @param array<string|int, string> $columns
700 * @param array<int, string> $flags
701 * @param array<string, mixed> $options
702 */
703 private function _createUniqueConstraint(
704 array $columns,
705 string $indexName,
706 array $flags = [],
707 array $options = [],
708 ): UniqueConstraint {
709 if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
710 throw IndexNameInvalid::new($indexName);
711 }
712
713 foreach ($columns as $index => $value) {
714 if (is_string($index)) {
715 $columnName = $index;
716 } else {
717 $columnName = $value;
718 }
719
720 if (! $this->hasColumn($columnName)) {
721 throw ColumnDoesNotExist::new($columnName, $this->_name);
722 }
723 }
724
725 return new UniqueConstraint($indexName, $columns, $flags, $options);
726 }
727
728 /**
729 * @param array<int, string> $columns
730 * @param array<int, string> $flags
731 * @param array<string, mixed> $options
732 */
733 private function _createIndex(
734 array $columns,
735 string $indexName,
736 bool $isUnique,
737 bool $isPrimary,
738 array $flags = [],
739 array $options = [],
740 ): Index {
741 if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) {
742 throw IndexNameInvalid::new($indexName);
743 }
744
745 foreach ($columns as $columnName) {
746 if (! $this->hasColumn($columnName)) {
747 throw ColumnDoesNotExist::new($columnName, $this->_name);
748 }
749 }
750
751 return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options);
752 }
753}
diff --git a/vendor/doctrine/dbal/src/Schema/TableDiff.php b/vendor/doctrine/dbal/src/Schema/TableDiff.php
new file mode 100644
index 0000000..1cd59e8
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/TableDiff.php
@@ -0,0 +1,164 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use function array_filter;
8use function count;
9
10/**
11 * Table Diff.
12 */
13class TableDiff
14{
15 /**
16 * Constructs a TableDiff object.
17 *
18 * @internal The diff can be only instantiated by a {@see Comparator}.
19 *
20 * @param array<ForeignKeyConstraint> $droppedForeignKeys
21 * @param array<Column> $addedColumns
22 * @param array<ColumnDiff> $modifiedColumns
23 * @param array<Column> $droppedColumns
24 * @param array<string, Column> $renamedColumns
25 * @param array<Index> $addedIndexes
26 * @param array<Index> $modifiedIndexes
27 * @param array<Index> $droppedIndexes
28 * @param array<string, Index> $renamedIndexes
29 * @param array<ForeignKeyConstraint> $addedForeignKeys
30 * @param array<ForeignKeyConstraint> $modifiedForeignKeys
31 */
32 public function __construct(
33 private readonly Table $oldTable,
34 private readonly array $addedColumns,
35 private readonly array $modifiedColumns,
36 private readonly array $droppedColumns,
37 private readonly array $renamedColumns,
38 private array $addedIndexes,
39 private readonly array $modifiedIndexes,
40 private array $droppedIndexes,
41 private readonly array $renamedIndexes,
42 private readonly array $addedForeignKeys,
43 private readonly array $modifiedForeignKeys,
44 private readonly array $droppedForeignKeys,
45 ) {
46 }
47
48 public function getOldTable(): Table
49 {
50 return $this->oldTable;
51 }
52
53 /** @return array<Column> */
54 public function getAddedColumns(): array
55 {
56 return $this->addedColumns;
57 }
58
59 /** @return array<ColumnDiff> */
60 public function getModifiedColumns(): array
61 {
62 return $this->modifiedColumns;
63 }
64
65 /** @return array<Column> */
66 public function getDroppedColumns(): array
67 {
68 return $this->droppedColumns;
69 }
70
71 /** @return array<string,Column> */
72 public function getRenamedColumns(): array
73 {
74 return $this->renamedColumns;
75 }
76
77 /** @return array<Index> */
78 public function getAddedIndexes(): array
79 {
80 return $this->addedIndexes;
81 }
82
83 /**
84 * @internal This method exists only for compatibility with the current implementation of schema managers
85 * that modify the diff while processing it.
86 */
87 public function unsetAddedIndex(Index $index): void
88 {
89 $this->addedIndexes = array_filter(
90 $this->addedIndexes,
91 static function (Index $addedIndex) use ($index): bool {
92 return $addedIndex !== $index;
93 },
94 );
95 }
96
97 /** @return array<Index> */
98 public function getModifiedIndexes(): array
99 {
100 return $this->modifiedIndexes;
101 }
102
103 /** @return array<Index> */
104 public function getDroppedIndexes(): array
105 {
106 return $this->droppedIndexes;
107 }
108
109 /**
110 * @internal This method exists only for compatibility with the current implementation of schema managers
111 * that modify the diff while processing it.
112 */
113 public function unsetDroppedIndex(Index $index): void
114 {
115 $this->droppedIndexes = array_filter(
116 $this->droppedIndexes,
117 static function (Index $droppedIndex) use ($index): bool {
118 return $droppedIndex !== $index;
119 },
120 );
121 }
122
123 /** @return array<string,Index> */
124 public function getRenamedIndexes(): array
125 {
126 return $this->renamedIndexes;
127 }
128
129 /** @return array<ForeignKeyConstraint> */
130 public function getAddedForeignKeys(): array
131 {
132 return $this->addedForeignKeys;
133 }
134
135 /** @return array<ForeignKeyConstraint> */
136 public function getModifiedForeignKeys(): array
137 {
138 return $this->modifiedForeignKeys;
139 }
140
141 /** @return array<ForeignKeyConstraint> */
142 public function getDroppedForeignKeys(): array
143 {
144 return $this->droppedForeignKeys;
145 }
146
147 /**
148 * Returns whether the diff is empty (contains no changes).
149 */
150 public function isEmpty(): bool
151 {
152 return count($this->addedColumns) === 0
153 && count($this->modifiedColumns) === 0
154 && count($this->droppedColumns) === 0
155 && count($this->renamedColumns) === 0
156 && count($this->addedIndexes) === 0
157 && count($this->modifiedIndexes) === 0
158 && count($this->droppedIndexes) === 0
159 && count($this->renamedIndexes) === 0
160 && count($this->addedForeignKeys) === 0
161 && count($this->modifiedForeignKeys) === 0
162 && count($this->droppedForeignKeys) === 0;
163 }
164}
diff --git a/vendor/doctrine/dbal/src/Schema/UniqueConstraint.php b/vendor/doctrine/dbal/src/Schema/UniqueConstraint.php
new file mode 100644
index 0000000..a33d446
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/UniqueConstraint.php
@@ -0,0 +1,152 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8
9use function array_keys;
10use function array_map;
11use function strtolower;
12
13/**
14 * Class for a unique constraint.
15 */
16class UniqueConstraint extends AbstractAsset
17{
18 /**
19 * Asset identifier instances of the column names the unique constraint is associated with.
20 *
21 * @var array<string, Identifier>
22 */
23 protected array $columns = [];
24
25 /**
26 * Platform specific flags
27 *
28 * @var array<string, true>
29 */
30 protected array $flags = [];
31
32 /**
33 * @param array<string> $columns
34 * @param array<string> $flags
35 * @param array<string, mixed> $options
36 */
37 public function __construct(
38 string $name,
39 array $columns,
40 array $flags = [],
41 private readonly array $options = [],
42 ) {
43 $this->_setName($name);
44
45 foreach ($columns as $column) {
46 $this->addColumn($column);
47 }
48
49 foreach ($flags as $flag) {
50 $this->addFlag($flag);
51 }
52 }
53
54 /**
55 * Returns the names of the referencing table columns the constraint is associated with.
56 *
57 * @return list<string>
58 */
59 public function getColumns(): array
60 {
61 return array_keys($this->columns);
62 }
63
64 /**
65 * Returns the quoted representation of the column names the constraint is associated with.
66 *
67 * But only if they were defined with one or a column name
68 * is a keyword reserved by the platform.
69 * Otherwise, the plain unquoted value as inserted is returned.
70 *
71 * @param AbstractPlatform $platform The platform to use for quotation.
72 *
73 * @return list<string>
74 */
75 public function getQuotedColumns(AbstractPlatform $platform): array
76 {
77 $columns = [];
78
79 foreach ($this->columns as $column) {
80 $columns[] = $column->getQuotedName($platform);
81 }
82
83 return $columns;
84 }
85
86 /** @return array<int, string> */
87 public function getUnquotedColumns(): array
88 {
89 return array_map($this->trimQuotes(...), $this->getColumns());
90 }
91
92 /**
93 * Returns platform specific flags for unique constraint.
94 *
95 * @return array<int, string>
96 */
97 public function getFlags(): array
98 {
99 return array_keys($this->flags);
100 }
101
102 /**
103 * Adds flag for a unique constraint that translates to platform specific handling.
104 *
105 * @return $this
106 *
107 * @example $uniqueConstraint->addFlag('CLUSTERED')
108 */
109 public function addFlag(string $flag): self
110 {
111 $this->flags[strtolower($flag)] = true;
112
113 return $this;
114 }
115
116 /**
117 * Does this unique constraint have a specific flag?
118 */
119 public function hasFlag(string $flag): bool
120 {
121 return isset($this->flags[strtolower($flag)]);
122 }
123
124 /**
125 * Removes a flag.
126 */
127 public function removeFlag(string $flag): void
128 {
129 unset($this->flags[strtolower($flag)]);
130 }
131
132 public function hasOption(string $name): bool
133 {
134 return isset($this->options[strtolower($name)]);
135 }
136
137 public function getOption(string $name): mixed
138 {
139 return $this->options[strtolower($name)];
140 }
141
142 /** @return array<string, mixed> */
143 public function getOptions(): array
144 {
145 return $this->options;
146 }
147
148 protected function addColumn(string $column): void
149 {
150 $this->columns[$column] = new Identifier($column);
151 }
152}
diff --git a/vendor/doctrine/dbal/src/Schema/View.php b/vendor/doctrine/dbal/src/Schema/View.php
new file mode 100644
index 0000000..81f5f8a
--- /dev/null
+++ b/vendor/doctrine/dbal/src/Schema/View.php
@@ -0,0 +1,21 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7/**
8 * Representation of a Database View.
9 */
10class View extends AbstractAsset
11{
12 public function __construct(string $name, private readonly string $sql)
13 {
14 $this->_setName($name);
15 }
16
17 public function getSql(): string
18 {
19 return $this->sql;
20 }
21}