From bf6655a534a6775d30cafa67bd801276bda1d98d Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 13 Aug 2024 23:45:21 +0200 Subject: =?UTF-8?q?VERSION=200.2=20doctrine=20ORM=20et=20entit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vendor/doctrine/dbal/src/Schema/AbstractAsset.php | 157 ++++ .../dbal/src/Schema/AbstractSchemaManager.php | 864 +++++++++++++++++++++ vendor/doctrine/dbal/src/Schema/Column.php | 252 ++++++ vendor/doctrine/dbal/src/Schema/ColumnDiff.php | 106 +++ vendor/doctrine/dbal/src/Schema/Comparator.php | 417 ++++++++++ .../doctrine/dbal/src/Schema/DB2SchemaManager.php | 371 +++++++++ .../src/Schema/DefaultSchemaManagerFactory.php | 20 + .../src/Schema/Exception/ColumnAlreadyExists.php | 19 + .../src/Schema/Exception/ColumnDoesNotExist.php | 19 + .../Schema/Exception/ForeignKeyDoesNotExist.php | 21 + .../src/Schema/Exception/IndexAlreadyExists.php | 21 + .../src/Schema/Exception/IndexDoesNotExist.php | 19 + .../dbal/src/Schema/Exception/IndexNameInvalid.php | 19 + .../dbal/src/Schema/Exception/InvalidTableName.php | 19 + .../Schema/Exception/NamespaceAlreadyExists.php | 19 + .../src/Schema/Exception/SequenceAlreadyExists.php | 19 + .../src/Schema/Exception/SequenceDoesNotExist.php | 19 + .../src/Schema/Exception/TableAlreadyExists.php | 19 + .../src/Schema/Exception/TableDoesNotExist.php | 19 + .../Exception/UniqueConstraintDoesNotExist.php | 21 + .../src/Schema/Exception/UnknownColumnOption.php | 21 + .../dbal/src/Schema/ForeignKeyConstraint.php | 291 +++++++ vendor/doctrine/dbal/src/Schema/Identifier.php | 29 + vendor/doctrine/dbal/src/Schema/Index.php | 314 ++++++++ .../dbal/src/Schema/MySQLSchemaManager.php | 548 +++++++++++++ .../dbal/src/Schema/OracleSchemaManager.php | 475 +++++++++++ .../dbal/src/Schema/PostgreSQLSchemaManager.php | 572 ++++++++++++++ .../dbal/src/Schema/SQLServerSchemaManager.php | 498 ++++++++++++ .../dbal/src/Schema/SQLiteSchemaManager.php | 620 +++++++++++++++ vendor/doctrine/dbal/src/Schema/Schema.php | 374 +++++++++ vendor/doctrine/dbal/src/Schema/SchemaConfig.php | 61 ++ vendor/doctrine/dbal/src/Schema/SchemaDiff.php | 109 +++ .../doctrine/dbal/src/Schema/SchemaException.php | 12 + .../dbal/src/Schema/SchemaManagerFactory.php | 17 + vendor/doctrine/dbal/src/Schema/Sequence.php | 98 +++ vendor/doctrine/dbal/src/Schema/Table.php | 753 ++++++++++++++++++ vendor/doctrine/dbal/src/Schema/TableDiff.php | 164 ++++ .../doctrine/dbal/src/Schema/UniqueConstraint.php | 152 ++++ vendor/doctrine/dbal/src/Schema/View.php | 21 + 39 files changed, 7569 insertions(+) create mode 100644 vendor/doctrine/dbal/src/Schema/AbstractAsset.php create mode 100644 vendor/doctrine/dbal/src/Schema/AbstractSchemaManager.php create mode 100644 vendor/doctrine/dbal/src/Schema/Column.php create mode 100644 vendor/doctrine/dbal/src/Schema/ColumnDiff.php create mode 100644 vendor/doctrine/dbal/src/Schema/Comparator.php create mode 100644 vendor/doctrine/dbal/src/Schema/DB2SchemaManager.php create mode 100644 vendor/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/InvalidTableName.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php create mode 100644 vendor/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php create mode 100644 vendor/doctrine/dbal/src/Schema/ForeignKeyConstraint.php create mode 100644 vendor/doctrine/dbal/src/Schema/Identifier.php create mode 100644 vendor/doctrine/dbal/src/Schema/Index.php create mode 100644 vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php create mode 100644 vendor/doctrine/dbal/src/Schema/OracleSchemaManager.php create mode 100644 vendor/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php create mode 100644 vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php create mode 100644 vendor/doctrine/dbal/src/Schema/SQLiteSchemaManager.php create mode 100644 vendor/doctrine/dbal/src/Schema/Schema.php create mode 100644 vendor/doctrine/dbal/src/Schema/SchemaConfig.php create mode 100644 vendor/doctrine/dbal/src/Schema/SchemaDiff.php create mode 100644 vendor/doctrine/dbal/src/Schema/SchemaException.php create mode 100644 vendor/doctrine/dbal/src/Schema/SchemaManagerFactory.php create mode 100644 vendor/doctrine/dbal/src/Schema/Sequence.php create mode 100644 vendor/doctrine/dbal/src/Schema/Table.php create mode 100644 vendor/doctrine/dbal/src/Schema/TableDiff.php create mode 100644 vendor/doctrine/dbal/src/Schema/UniqueConstraint.php create mode 100644 vendor/doctrine/dbal/src/Schema/View.php (limited to 'vendor/doctrine/dbal/src/Schema') 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 @@ + Table($tableName)); if you want to rename the table, you have to make sure this does not get + * recreated during schema migration. + */ +abstract class AbstractAsset +{ + protected string $_name = ''; + + /** + * Namespace of the asset. If none isset the default namespace is assumed. + */ + protected ?string $_namespace = null; + + protected bool $_quoted = false; + + /** + * Sets the name of this asset. + */ + protected function _setName(string $name): void + { + if ($this->isIdentifierQuoted($name)) { + $this->_quoted = true; + $name = $this->trimQuotes($name); + } + + if (str_contains($name, '.')) { + $parts = explode('.', $name); + $this->_namespace = $parts[0]; + $name = $parts[1]; + } + + $this->_name = $name; + } + + /** + * Is this asset in the default namespace? + */ + public function isInDefaultNamespace(string $defaultNamespaceName): bool + { + return $this->_namespace === $defaultNamespaceName || $this->_namespace === null; + } + + /** + * Gets the namespace name of this asset. + * + * If NULL is returned this means the default namespace is used. + */ + public function getNamespaceName(): ?string + { + return $this->_namespace; + } + + /** + * The shortest name is stripped of the default namespace. All other + * namespaced elements are returned as full-qualified names. + */ + public function getShortestName(?string $defaultNamespaceName): string + { + $shortestName = $this->getName(); + if ($this->_namespace === $defaultNamespaceName) { + $shortestName = $this->_name; + } + + return strtolower($shortestName); + } + + /** + * Checks if this asset's name is quoted. + */ + public function isQuoted(): bool + { + return $this->_quoted; + } + + /** + * Checks if this identifier is quoted. + */ + protected function isIdentifierQuoted(string $identifier): bool + { + return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '['); + } + + /** + * Trim quotes from the identifier. + */ + protected function trimQuotes(string $identifier): string + { + return str_replace(['`', '"', '[', ']'], '', $identifier); + } + + /** + * Returns the name of this schema asset. + */ + public function getName(): string + { + if ($this->_namespace !== null) { + return $this->_namespace . '.' . $this->_name; + } + + return $this->_name; + } + + /** + * Gets the quoted representation of this asset but only if it was defined with one. Otherwise + * return the plain unquoted value as inserted. + */ + public function getQuotedName(AbstractPlatform $platform): string + { + $keywords = $platform->getReservedKeywordsList(); + $parts = explode('.', $this->getName()); + foreach ($parts as $k => $v) { + $parts[$k] = $this->_quoted || $keywords->isKeyword($v) ? $platform->quoteIdentifier($v) : $v; + } + + return implode('.', $parts); + } + + /** + * Generates an identifier from a list of column names obeying a certain string length. + * + * This is especially important for Oracle, since it does not allow identifiers larger than 30 chars, + * however building idents automatically for foreign keys, composite keys or such can easily create + * very long names. + * + * @param array $columnNames + */ + protected function _generateIdentifierName(array $columnNames, string $prefix = '', int $maxSize = 30): string + { + $hash = implode('', array_map(static function ($column): string { + return dechex(crc32($column)); + }, $columnNames)); + + return strtoupper(substr($prefix . '_' . $hash, 0, $maxSize)); + } +} 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 @@ + + * + * @throws Exception + */ + public function listDatabases(): array + { + return array_map(function (array $row): string { + return $this->_getPortableDatabaseDefinition($row); + }, $this->connection->fetchAllAssociative( + $this->platform->getListDatabasesSQL(), + )); + } + + /** + * Returns a list of the names of all schemata in the current database. + * + * @return list + * + * @throws Exception + */ + public function listSchemaNames(): array + { + throw NotSupported::new(__METHOD__); + } + + /** + * Lists the available sequences for this connection. + * + * @return array + * + * @throws Exception + */ + public function listSequences(): array + { + return $this->filterAssetNames( + array_map(function (array $row): Sequence { + return $this->_getPortableSequenceDefinition($row); + }, $this->connection->fetchAllAssociative( + $this->platform->getListSequencesSQL( + $this->getDatabase(__METHOD__), + ), + )), + ); + } + + /** + * Lists the columns for a given table. + * + * In contrast to other libraries and to the old version of Doctrine, + * this column definition does try to contain the 'primary' column for + * the reason that it is not portable across different RDBMS. Use + * {@see listTableIndexes($tableName)} to retrieve the primary key + * of a table. Where a RDBMS specifies more details, these are held + * in the platformDetails array. + * + * @return array + * + * @throws Exception + */ + public function listTableColumns(string $table): array + { + $database = $this->getDatabase(__METHOD__); + + return $this->_getPortableTableColumnList( + $table, + $database, + $this->selectTableColumns($database, $this->normalizeName($table)) + ->fetchAllAssociative(), + ); + } + + /** + * Lists the indexes for a given table returning an array of Index instances. + * + * Keys of the portable indexes list are all lower-cased. + * + * @return array + * + * @throws Exception + */ + public function listTableIndexes(string $table): array + { + $database = $this->getDatabase(__METHOD__); + $table = $this->normalizeName($table); + + return $this->_getPortableTableIndexesList( + $this->selectIndexColumns( + $database, + $table, + )->fetchAllAssociative(), + $table, + ); + } + + /** + * Returns true if all the given tables exist. + * + * @param array $names + * + * @throws Exception + */ + public function tablesExist(array $names): bool + { + $names = array_map('strtolower', $names); + + return count($names) === count(array_intersect($names, array_map('strtolower', $this->listTableNames()))); + } + + public function tableExists(string $tableName): bool + { + return $this->tablesExist([$tableName]); + } + + /** + * Returns a list of all tables in the current database. + * + * @return array + * + * @throws Exception + */ + public function listTableNames(): array + { + return $this->filterAssetNames( + array_map(function (array $row): string { + return $this->_getPortableTableDefinition($row); + }, $this->selectTableNames( + $this->getDatabase(__METHOD__), + )->fetchAllAssociative()), + ); + } + + /** + * Filters asset names if they are configured to return only a subset of all + * the found elements. + * + * @param array $assetNames + * + * @return array + */ + private function filterAssetNames(array $assetNames): array + { + $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter(); + + return array_values(array_filter($assetNames, $filter)); + } + + /** + * Lists the tables for this connection. + * + * @return list + * + * @throws Exception + */ + public function listTables(): array + { + $database = $this->getDatabase(__METHOD__); + + $tableColumnsByTable = $this->fetchTableColumnsByTable($database); + $indexColumnsByTable = $this->fetchIndexColumnsByTable($database); + $foreignKeyColumnsByTable = $this->fetchForeignKeyColumnsByTable($database); + $tableOptionsByTable = $this->fetchTableOptionsByTable($database); + + $filter = $this->connection->getConfiguration()->getSchemaAssetsFilter(); + $tables = []; + + foreach ($tableColumnsByTable as $tableName => $tableColumns) { + if (! $filter($tableName)) { + continue; + } + + $tables[] = new Table( + $tableName, + $this->_getPortableTableColumnList($tableName, $database, $tableColumns), + $this->_getPortableTableIndexesList($indexColumnsByTable[$tableName] ?? [], $tableName), + [], + $this->_getPortableTableForeignKeysList($foreignKeyColumnsByTable[$tableName] ?? []), + $tableOptionsByTable[$tableName] ?? [], + ); + } + + return $tables; + } + + /** + * An extension point for those platforms where case sensitivity of the object name depends on whether it's quoted. + * + * Such platforms should convert a possibly quoted name into a value of the corresponding case. + */ + protected function normalizeName(string $name): string + { + $identifier = new Identifier($name); + + return $identifier->getName(); + } + + /** + * Selects names of tables in the specified database. + * + * @throws Exception + */ + abstract protected function selectTableNames(string $databaseName): Result; + + /** + * Selects definitions of table columns in the specified database. If the table name is specified, narrows down + * the selection to this table. + * + * @throws Exception + */ + abstract protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result; + + /** + * Selects definitions of index columns in the specified database. If the table name is specified, narrows down + * the selection to this table. + * + * @throws Exception + */ + abstract protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result; + + /** + * Selects definitions of foreign key columns in the specified database. If the table name is specified, + * narrows down the selection to this table. + * + * @throws Exception + */ + abstract protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result; + + /** + * Fetches definitions of table columns in the specified database and returns them grouped by table name. + * + * @return array>> + * + * @throws Exception + */ + protected function fetchTableColumnsByTable(string $databaseName): array + { + return $this->fetchAllAssociativeGrouped($this->selectTableColumns($databaseName)); + } + + /** + * Fetches definitions of index columns in the specified database and returns them grouped by table name. + * + * @return array>> + * + * @throws Exception + */ + protected function fetchIndexColumnsByTable(string $databaseName): array + { + return $this->fetchAllAssociativeGrouped($this->selectIndexColumns($databaseName)); + } + + /** + * Fetches definitions of foreign key columns in the specified database and returns them grouped by table name. + * + * @return array>> + * + * @throws Exception + */ + protected function fetchForeignKeyColumnsByTable(string $databaseName): array + { + return $this->fetchAllAssociativeGrouped( + $this->selectForeignKeyColumns($databaseName), + ); + } + + /** + * Fetches table options for the tables in the specified database and returns them grouped by table name. + * If the table name is specified, narrows down the selection to this table. + * + * @return array> + * + * @throws Exception + */ + abstract protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array; + + /** + * Introspects the table with the given name. + * + * @throws Exception + */ + public function introspectTable(string $name): Table + { + $columns = $this->listTableColumns($name); + + if ($columns === []) { + throw TableDoesNotExist::new($name); + } + + return new Table( + $name, + $columns, + $this->listTableIndexes($name), + [], + $this->listTableForeignKeys($name), + $this->getTableOptions($name), + ); + } + + /** + * Lists the views this connection has. + * + * @return list + * + * @throws Exception + */ + public function listViews(): array + { + return array_map(function (array $row): View { + return $this->_getPortableViewDefinition($row); + }, $this->connection->fetchAllAssociative( + $this->platform->getListViewsSQL( + $this->getDatabase(__METHOD__), + ), + )); + } + + /** + * Lists the foreign keys for the given table. + * + * @return array + * + * @throws Exception + */ + public function listTableForeignKeys(string $table): array + { + $database = $this->getDatabase(__METHOD__); + + return $this->_getPortableTableForeignKeysList( + $this->selectForeignKeyColumns( + $database, + $this->normalizeName($table), + )->fetchAllAssociative(), + ); + } + + /** + * @return array + * + * @throws Exception + */ + private function getTableOptions(string $name): array + { + $normalizedName = $this->normalizeName($name); + + return $this->fetchTableOptionsByTable( + $this->getDatabase(__METHOD__), + $normalizedName, + )[$normalizedName] ?? []; + } + + /* drop*() Methods */ + + /** + * Drops a database. + * + * NOTE: You can not drop the database this SchemaManager is currently connected to. + * + * @throws Exception + */ + public function dropDatabase(string $database): void + { + $this->connection->executeStatement( + $this->platform->getDropDatabaseSQL($database), + ); + } + + /** + * Drops a schema. + * + * @throws Exception + */ + public function dropSchema(string $schemaName): void + { + $this->connection->executeStatement( + $this->platform->getDropSchemaSQL($schemaName), + ); + } + + /** + * Drops the given table. + * + * @throws Exception + */ + public function dropTable(string $name): void + { + $this->connection->executeStatement( + $this->platform->getDropTableSQL($name), + ); + } + + /** + * Drops the index from the given table. + * + * @throws Exception + */ + public function dropIndex(string $index, string $table): void + { + $this->connection->executeStatement( + $this->platform->getDropIndexSQL($index, $table), + ); + } + + /** + * Drops a foreign key from a table. + * + * @throws Exception + */ + public function dropForeignKey(string $name, string $table): void + { + $this->connection->executeStatement( + $this->platform->getDropForeignKeySQL($name, $table), + ); + } + + /** + * Drops a sequence with a given name. + * + * @throws Exception + */ + public function dropSequence(string $name): void + { + $this->connection->executeStatement( + $this->platform->getDropSequenceSQL($name), + ); + } + + /** + * Drops the unique constraint from the given table. + * + * @throws Exception + */ + public function dropUniqueConstraint(string $name, string $tableName): void + { + $this->connection->executeStatement( + $this->platform->getDropUniqueConstraintSQL($name, $tableName), + ); + } + + /** + * Drops a view. + * + * @throws Exception + */ + public function dropView(string $name): void + { + $this->connection->executeStatement( + $this->platform->getDropViewSQL($name), + ); + } + + /* create*() Methods */ + + /** @throws Exception */ + public function createSchemaObjects(Schema $schema): void + { + $this->executeStatements($schema->toSql($this->platform)); + } + + /** + * Creates a new database. + * + * @throws Exception + */ + public function createDatabase(string $database): void + { + $this->connection->executeStatement( + $this->platform->getCreateDatabaseSQL($database), + ); + } + + /** + * Creates a new table. + * + * @throws Exception + */ + public function createTable(Table $table): void + { + $this->executeStatements($this->platform->getCreateTableSQL($table)); + } + + /** + * Creates a new sequence. + * + * @throws Exception + */ + public function createSequence(Sequence $sequence): void + { + $this->connection->executeStatement( + $this->platform->getCreateSequenceSQL($sequence), + ); + } + + /** + * Creates a new index on a table. + * + * @param string $table The name of the table on which the index is to be created. + * + * @throws Exception + */ + public function createIndex(Index $index, string $table): void + { + $this->connection->executeStatement( + $this->platform->getCreateIndexSQL($index, $table), + ); + } + + /** + * Creates a new foreign key. + * + * @param ForeignKeyConstraint $foreignKey The ForeignKey instance. + * @param string $table The name of the table on which the foreign key is to be created. + * + * @throws Exception + */ + public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void + { + $this->connection->executeStatement( + $this->platform->getCreateForeignKeySQL($foreignKey, $table), + ); + } + + /** + * Creates a unique constraint on a table. + * + * @throws Exception + */ + public function createUniqueConstraint(UniqueConstraint $uniqueConstraint, string $tableName): void + { + $this->connection->executeStatement( + $this->platform->getCreateUniqueConstraintSQL($uniqueConstraint, $tableName), + ); + } + + /** + * Creates a new view. + * + * @throws Exception + */ + public function createView(View $view): void + { + $this->connection->executeStatement( + $this->platform->getCreateViewSQL( + $view->getQuotedName($this->platform), + $view->getSql(), + ), + ); + } + + /** @throws Exception */ + public function dropSchemaObjects(Schema $schema): void + { + $this->executeStatements($schema->toDropSql($this->platform)); + } + + /** + * Alters an existing schema. + * + * @throws Exception + */ + public function alterSchema(SchemaDiff $schemaDiff): void + { + $this->executeStatements($this->platform->getAlterSchemaSQL($schemaDiff)); + } + + /** + * Migrates an existing schema to a new schema. + * + * @throws Exception + */ + public function migrateSchema(Schema $newSchema): void + { + $schemaDiff = $this->createComparator() + ->compareSchemas($this->introspectSchema(), $newSchema); + + $this->alterSchema($schemaDiff); + } + + /* alterTable() Methods */ + + /** + * Alters an existing tables schema. + * + * @throws Exception + */ + public function alterTable(TableDiff $tableDiff): void + { + $this->executeStatements($this->platform->getAlterTableSQL($tableDiff)); + } + + /** + * Renames a given table to another name. + * + * @throws Exception + */ + public function renameTable(string $name, string $newName): void + { + $this->connection->executeStatement( + $this->platform->getRenameTableSQL($name, $newName), + ); + } + + /** + * Methods for filtering return values of list*() methods to convert + * the native DBMS data definition to a portable Doctrine definition + */ + + /** @param array $database */ + protected function _getPortableDatabaseDefinition(array $database): string + { + throw NotSupported::new(__METHOD__); + } + + /** @param array $sequence */ + protected function _getPortableSequenceDefinition(array $sequence): Sequence + { + throw NotSupported::new(__METHOD__); + } + + /** + * Independent of the database the keys of the column list result are lowercased. + * + * The name of the created column instance however is kept in its case. + * + * @param array> $tableColumns + * + * @return array + * + * @throws Exception + */ + protected function _getPortableTableColumnList(string $table, string $database, array $tableColumns): array + { + $list = []; + foreach ($tableColumns as $tableColumn) { + $column = $this->_getPortableTableColumnDefinition($tableColumn); + + $name = strtolower($column->getQuotedName($this->platform)); + $list[$name] = $column; + } + + return $list; + } + + /** + * Gets Table Column Definition. + * + * @param array $tableColumn + * + * @throws Exception + */ + abstract protected function _getPortableTableColumnDefinition(array $tableColumn): Column; + + /** + * Aggregates and groups the index results according to the required data result. + * + * @param array> $tableIndexes + * + * @return array + * + * @throws Exception + */ + protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array + { + $result = []; + foreach ($tableIndexes as $tableIndex) { + $indexName = $keyName = $tableIndex['key_name']; + if ($tableIndex['primary']) { + $keyName = 'primary'; + } + + $keyName = strtolower($keyName); + + if (! isset($result[$keyName])) { + $options = [ + 'lengths' => [], + ]; + + if (isset($tableIndex['where'])) { + $options['where'] = $tableIndex['where']; + } + + $result[$keyName] = [ + 'name' => $indexName, + 'columns' => [], + 'unique' => ! $tableIndex['non_unique'], + 'primary' => $tableIndex['primary'], + 'flags' => $tableIndex['flags'] ?? [], + 'options' => $options, + ]; + } + + $result[$keyName]['columns'][] = $tableIndex['column_name']; + $result[$keyName]['options']['lengths'][] = $tableIndex['length'] ?? null; + } + + $indexes = []; + foreach ($result as $indexKey => $data) { + $indexes[$indexKey] = new Index( + $data['name'], + $data['columns'], + $data['unique'], + $data['primary'], + $data['flags'], + $data['options'], + ); + } + + return $indexes; + } + + /** @param array $table */ + abstract protected function _getPortableTableDefinition(array $table): string; + + /** @param array $view */ + abstract protected function _getPortableViewDefinition(array $view): View; + + /** + * @param array> $tableForeignKeys + * + * @return array + */ + protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array + { + $list = []; + + foreach ($tableForeignKeys as $value) { + $list[] = $this->_getPortableTableForeignKeyDefinition($value); + } + + return $list; + } + + /** @param array $tableForeignKey */ + abstract protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint; + + /** + * @param array $sql + * + * @throws Exception + */ + private function executeStatements(array $sql): void + { + foreach ($sql as $query) { + $this->connection->executeStatement($query); + } + } + + /** + * Returns a {@see Schema} instance representing the current database schema. + * + * @throws Exception + */ + public function introspectSchema(): Schema + { + $schemaNames = []; + + if ($this->platform->supportsSchemas()) { + $schemaNames = $this->listSchemaNames(); + } + + $sequences = []; + + if ($this->platform->supportsSequences()) { + $sequences = $this->listSequences(); + } + + $tables = $this->listTables(); + + return new Schema($tables, $sequences, $this->createSchemaConfig(), $schemaNames); + } + + /** + * Creates the configuration for this schema. + * + * @throws Exception + */ + public function createSchemaConfig(): SchemaConfig + { + $schemaConfig = new SchemaConfig(); + $schemaConfig->setMaxIdentifierLength($this->platform->getMaxIdentifierLength()); + + $params = $this->connection->getParams(); + if (! isset($params['defaultTableOptions'])) { + $params['defaultTableOptions'] = []; + } + + if (! isset($params['defaultTableOptions']['charset']) && isset($params['charset'])) { + $params['defaultTableOptions']['charset'] = $params['charset']; + } + + $schemaConfig->setDefaultTableOptions($params['defaultTableOptions']); + + return $schemaConfig; + } + + /** @throws Exception */ + private function getDatabase(string $methodName): string + { + $database = $this->connection->getDatabase(); + + if ($database === null) { + throw DatabaseRequired::new($methodName); + } + + return $database; + } + + public function createComparator(): Comparator + { + return new Comparator($this->platform); + } + + /** + * @return array>> + * + * @throws Exception + */ + private function fetchAllAssociativeGrouped(Result $result): array + { + $data = []; + + foreach ($result->fetchAllAssociative() as $row) { + $tableName = $this->_getPortableTableDefinition($row); + $data[$tableName][] = $row; + } + + return $data; + } +} 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 @@ + */ + protected array $_platformOptions = []; + + protected ?string $_columnDefinition = null; + + protected string $_comment = ''; + + /** + * Creates a new Column. + * + * @param array $options + */ + public function __construct(string $name, Type $type, array $options = []) + { + $this->_setName($name); + $this->setType($type); + $this->setOptions($options); + } + + /** @param array $options */ + public function setOptions(array $options): self + { + foreach ($options as $name => $value) { + $method = 'set' . $name; + + if (! method_exists($this, $method)) { + throw UnknownColumnOption::new($name); + } + + $this->$method($value); + } + + return $this; + } + + public function setType(Type $type): self + { + $this->_type = $type; + + return $this; + } + + public function setLength(?int $length): self + { + $this->_length = $length; + + return $this; + } + + public function setPrecision(?int $precision): self + { + $this->_precision = $precision; + + return $this; + } + + public function setScale(int $scale): self + { + $this->_scale = $scale; + + return $this; + } + + public function setUnsigned(bool $unsigned): self + { + $this->_unsigned = $unsigned; + + return $this; + } + + public function setFixed(bool $fixed): self + { + $this->_fixed = $fixed; + + return $this; + } + + public function setNotnull(bool $notnull): self + { + $this->_notnull = $notnull; + + return $this; + } + + public function setDefault(mixed $default): self + { + $this->_default = $default; + + return $this; + } + + /** @param array $platformOptions */ + public function setPlatformOptions(array $platformOptions): self + { + $this->_platformOptions = $platformOptions; + + return $this; + } + + public function setPlatformOption(string $name, mixed $value): self + { + $this->_platformOptions[$name] = $value; + + return $this; + } + + public function setColumnDefinition(?string $value): self + { + $this->_columnDefinition = $value; + + return $this; + } + + public function getType(): Type + { + return $this->_type; + } + + public function getLength(): ?int + { + return $this->_length; + } + + public function getPrecision(): ?int + { + return $this->_precision; + } + + public function getScale(): int + { + return $this->_scale; + } + + public function getUnsigned(): bool + { + return $this->_unsigned; + } + + public function getFixed(): bool + { + return $this->_fixed; + } + + public function getNotnull(): bool + { + return $this->_notnull; + } + + public function getDefault(): mixed + { + return $this->_default; + } + + /** @return array */ + public function getPlatformOptions(): array + { + return $this->_platformOptions; + } + + public function hasPlatformOption(string $name): bool + { + return isset($this->_platformOptions[$name]); + } + + public function getPlatformOption(string $name): mixed + { + return $this->_platformOptions[$name]; + } + + public function getColumnDefinition(): ?string + { + return $this->_columnDefinition; + } + + public function getAutoincrement(): bool + { + return $this->_autoincrement; + } + + public function setAutoincrement(bool $flag): self + { + $this->_autoincrement = $flag; + + return $this; + } + + public function setComment(string $comment): self + { + $this->_comment = $comment; + + return $this; + } + + public function getComment(): string + { + return $this->_comment; + } + + /** @return array */ + public function toArray(): array + { + return array_merge([ + 'name' => $this->_name, + 'type' => $this->_type, + 'default' => $this->_default, + 'notnull' => $this->_notnull, + 'length' => $this->_length, + 'precision' => $this->_precision, + 'scale' => $this->_scale, + 'fixed' => $this->_fixed, + 'unsigned' => $this->_unsigned, + 'autoincrement' => $this->_autoincrement, + 'columnDefinition' => $this->_columnDefinition, + 'comment' => $this->_comment, + ], $this->_platformOptions); + } +} 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 @@ +oldColumn; + } + + public function getNewColumn(): Column + { + return $this->newColumn; + } + + public function hasTypeChanged(): bool + { + return $this->newColumn->getType()::class !== $this->oldColumn->getType()::class; + } + + public function hasLengthChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): ?int { + return $column->getLength(); + }); + } + + public function hasPrecisionChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): ?int { + return $column->getPrecision(); + }); + } + + public function hasScaleChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): int { + return $column->getScale(); + }); + } + + public function hasUnsignedChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): bool { + return $column->getUnsigned(); + }); + } + + public function hasFixedChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): bool { + return $column->getFixed(); + }); + } + + public function hasNotNullChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): bool { + return $column->getNotnull(); + }); + } + + public function hasDefaultChanged(): bool + { + $oldDefault = $this->oldColumn->getDefault(); + $newDefault = $this->newColumn->getDefault(); + + // Null values need to be checked additionally as they tell whether to create or drop a default value. + // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation. + if (($newDefault === null) xor ($oldDefault === null)) { + return true; + } + + return $newDefault != $oldDefault; + } + + public function hasAutoIncrementChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): bool { + return $column->getAutoincrement(); + }); + } + + public function hasCommentChanged(): bool + { + return $this->hasPropertyChanged(static function (Column $column): string { + return $column->getComment(); + }); + } + + private function hasPropertyChanged(callable $property): bool + { + return $property($this->newColumn) !== $property($this->oldColumn); + } +} 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 @@ +getNamespaces() as $newNamespace) { + if ($oldSchema->hasNamespace($newNamespace)) { + continue; + } + + $createdSchemas[] = $newNamespace; + } + + foreach ($oldSchema->getNamespaces() as $oldNamespace) { + if ($newSchema->hasNamespace($oldNamespace)) { + continue; + } + + $droppedSchemas[] = $oldNamespace; + } + + foreach ($newSchema->getTables() as $newTable) { + $newTableName = $newTable->getShortestName($newSchema->getName()); + if (! $oldSchema->hasTable($newTableName)) { + $createdTables[] = $newSchema->getTable($newTableName); + } else { + $tableDiff = $this->compareTables( + $oldSchema->getTable($newTableName), + $newSchema->getTable($newTableName), + ); + + if (! $tableDiff->isEmpty()) { + $alteredTables[] = $tableDiff; + } + } + } + + // Check if there are tables removed + foreach ($oldSchema->getTables() as $oldTable) { + $oldTableName = $oldTable->getShortestName($oldSchema->getName()); + + $oldTable = $oldSchema->getTable($oldTableName); + if ($newSchema->hasTable($oldTableName)) { + continue; + } + + $droppedTables[] = $oldTable; + } + + foreach ($newSchema->getSequences() as $newSequence) { + $newSequenceName = $newSequence->getShortestName($newSchema->getName()); + if (! $oldSchema->hasSequence($newSequenceName)) { + if (! $this->isAutoIncrementSequenceInSchema($oldSchema, $newSequence)) { + $createdSequences[] = $newSequence; + } + } else { + if ($this->diffSequence($newSequence, $oldSchema->getSequence($newSequenceName))) { + $alteredSequences[] = $newSchema->getSequence($newSequenceName); + } + } + } + + foreach ($oldSchema->getSequences() as $oldSequence) { + if ($this->isAutoIncrementSequenceInSchema($newSchema, $oldSequence)) { + continue; + } + + $oldSequenceName = $oldSequence->getShortestName($oldSchema->getName()); + + if ($newSchema->hasSequence($oldSequenceName)) { + continue; + } + + $droppedSequences[] = $oldSequence; + } + + return new SchemaDiff( + $createdSchemas, + $droppedSchemas, + $createdTables, + $alteredTables, + $droppedTables, + $createdSequences, + $alteredSequences, + $droppedSequences, + ); + } + + private function isAutoIncrementSequenceInSchema(Schema $schema, Sequence $sequence): bool + { + foreach ($schema->getTables() as $table) { + if ($sequence->isAutoIncrementsFor($table)) { + return true; + } + } + + return false; + } + + public function diffSequence(Sequence $sequence1, Sequence $sequence2): bool + { + if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) { + return true; + } + + return $sequence1->getInitialValue() !== $sequence2->getInitialValue(); + } + + /** + * Compares the tables and returns the difference between them. + */ + public function compareTables(Table $oldTable, Table $newTable): TableDiff + { + $addedColumns = []; + $modifiedColumns = []; + $droppedColumns = []; + $addedIndexes = []; + $modifiedIndexes = []; + $droppedIndexes = []; + $addedForeignKeys = []; + $modifiedForeignKeys = []; + $droppedForeignKeys = []; + + $oldColumns = $oldTable->getColumns(); + $newColumns = $newTable->getColumns(); + + // See if all the columns in the old table exist in the new table + foreach ($newColumns as $newColumn) { + $newColumnName = strtolower($newColumn->getName()); + + if ($oldTable->hasColumn($newColumnName)) { + continue; + } + + $addedColumns[$newColumnName] = $newColumn; + } + + // See if there are any removed columns in the new table + foreach ($oldColumns as $oldColumn) { + $oldColumnName = strtolower($oldColumn->getName()); + + // See if column is removed in the new table. + if (! $newTable->hasColumn($oldColumnName)) { + $droppedColumns[$oldColumnName] = $oldColumn; + + continue; + } + + $newColumn = $newTable->getColumn($oldColumnName); + + if ($this->columnsEqual($oldColumn, $newColumn)) { + continue; + } + + $modifiedColumns[] = new ColumnDiff($oldColumn, $newColumn); + } + + $renamedColumns = $this->detectRenamedColumns($addedColumns, $droppedColumns); + + $oldIndexes = $oldTable->getIndexes(); + $newIndexes = $newTable->getIndexes(); + + // See if all the indexes from the old table exist in the new one + foreach ($newIndexes as $newIndexName => $newIndex) { + if (($newIndex->isPrimary() && $oldTable->getPrimaryKey() !== null) || $oldTable->hasIndex($newIndexName)) { + continue; + } + + $addedIndexes[$newIndexName] = $newIndex; + } + + // See if there are any removed indexes in the new table + foreach ($oldIndexes as $oldIndexName => $oldIndex) { + // See if the index is removed in the new table. + if ( + ($oldIndex->isPrimary() && $newTable->getPrimaryKey() === null) || + ! $oldIndex->isPrimary() && ! $newTable->hasIndex($oldIndexName) + ) { + $droppedIndexes[$oldIndexName] = $oldIndex; + + continue; + } + + // See if index has changed in the new table. + $newIndex = $oldIndex->isPrimary() ? $newTable->getPrimaryKey() : $newTable->getIndex($oldIndexName); + assert($newIndex instanceof Index); + + if (! $this->diffIndex($oldIndex, $newIndex)) { + continue; + } + + $modifiedIndexes[] = $newIndex; + } + + $renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes); + + $oldForeignKeys = $oldTable->getForeignKeys(); + $newForeignKeys = $newTable->getForeignKeys(); + + foreach ($oldForeignKeys as $oldKey => $oldForeignKey) { + foreach ($newForeignKeys as $newKey => $newForeignKey) { + if ($this->diffForeignKey($oldForeignKey, $newForeignKey) === false) { + unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]); + } else { + if (strtolower($oldForeignKey->getName()) === strtolower($newForeignKey->getName())) { + $modifiedForeignKeys[] = $newForeignKey; + + unset($oldForeignKeys[$oldKey], $newForeignKeys[$newKey]); + } + } + } + } + + foreach ($oldForeignKeys as $oldForeignKey) { + $droppedForeignKeys[] = $oldForeignKey; + } + + foreach ($newForeignKeys as $newForeignKey) { + $addedForeignKeys[] = $newForeignKey; + } + + return new TableDiff( + $oldTable, + $addedColumns, + $modifiedColumns, + $droppedColumns, + $renamedColumns, + $addedIndexes, + $modifiedIndexes, + $droppedIndexes, + $renamedIndexes, + $addedForeignKeys, + $modifiedForeignKeys, + $droppedForeignKeys, + ); + } + + /** + * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop + * however ambiguities between different possibilities should not lead to renaming at all. + * + * @param array $addedColumns + * @param array $removedColumns + * + * @return array + */ + private function detectRenamedColumns(array &$addedColumns, array &$removedColumns): array + { + $candidatesByName = []; + + foreach ($addedColumns as $addedColumnName => $addedColumn) { + foreach ($removedColumns as $removedColumn) { + if (! $this->columnsEqual($addedColumn, $removedColumn)) { + continue; + } + + $candidatesByName[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName]; + } + } + + $renamedColumns = []; + + foreach ($candidatesByName as $candidates) { + if (count($candidates) !== 1) { + continue; + } + + [$removedColumn, $addedColumn] = $candidates[0]; + $removedColumnName = $removedColumn->getName(); + $addedColumnName = strtolower($addedColumn->getName()); + + if (isset($renamedColumns[$removedColumnName])) { + continue; + } + + $renamedColumns[$removedColumnName] = $addedColumn; + unset( + $addedColumns[$addedColumnName], + $removedColumns[strtolower($removedColumnName)], + ); + } + + return $renamedColumns; + } + + /** + * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop + * however ambiguities between different possibilities should not lead to renaming at all. + * + * @param array $addedIndexes + * @param array $removedIndexes + * + * @return array + */ + private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array + { + $candidatesByName = []; + + // Gather possible rename candidates by comparing each added and removed index based on semantics. + foreach ($addedIndexes as $addedIndexName => $addedIndex) { + foreach ($removedIndexes as $removedIndex) { + if ($this->diffIndex($addedIndex, $removedIndex)) { + continue; + } + + $candidatesByName[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName]; + } + } + + $renamedIndexes = []; + + foreach ($candidatesByName as $candidates) { + // If the current rename candidate contains exactly one semantically equal index, + // we can safely rename it. + // Otherwise, it is unclear if a rename action is really intended, + // therefore we let those ambiguous indexes be added/dropped. + if (count($candidates) !== 1) { + continue; + } + + [$removedIndex, $addedIndex] = $candidates[0]; + + $removedIndexName = strtolower($removedIndex->getName()); + $addedIndexName = strtolower($addedIndex->getName()); + + if (isset($renamedIndexes[$removedIndexName])) { + continue; + } + + $renamedIndexes[$removedIndexName] = $addedIndex; + unset( + $addedIndexes[$addedIndexName], + $removedIndexes[$removedIndexName], + ); + } + + return $renamedIndexes; + } + + protected function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2): bool + { + if ( + array_map('strtolower', $key1->getUnquotedLocalColumns()) + !== array_map('strtolower', $key2->getUnquotedLocalColumns()) + ) { + return true; + } + + if ( + array_map('strtolower', $key1->getUnquotedForeignColumns()) + !== array_map('strtolower', $key2->getUnquotedForeignColumns()) + ) { + return true; + } + + if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) { + return true; + } + + if ($key1->onUpdate() !== $key2->onUpdate()) { + return true; + } + + return $key1->onDelete() !== $key2->onDelete(); + } + + /** + * Compares the definitions of the given columns + */ + protected function columnsEqual(Column $column1, Column $column2): bool + { + return $this->platform->columnsEqual($column1, $column2); + } + + /** + * Finds the difference between the indexes $index1 and $index2. + * + * Compares $index1 with $index2 and returns true if there are any + * differences or false in case there are no differences. + */ + protected function diffIndex(Index $index1, Index $index2): bool + { + return ! ($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1)); + } +} 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 @@ + + */ +class DB2SchemaManager extends AbstractSchemaManager +{ + /** + * {@inheritDoc} + * + * @throws Exception + */ + protected function _getPortableTableColumnDefinition(array $tableColumn): Column + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $length = $precision = $default = null; + $scale = 0; + $fixed = false; + + if ($tableColumn['default'] !== null && $tableColumn['default'] !== 'NULL') { + $default = $tableColumn['default']; + + if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) { + $default = str_replace("''", "'", $matches[1]); + } + } + + $type = $this->platform->getDoctrineTypeMapping($tableColumn['typename']); + + switch (strtolower($tableColumn['typename'])) { + case 'varchar': + if ($tableColumn['codepage'] === 0) { + $type = Types::BINARY; + } + + $length = $tableColumn['length']; + break; + + case 'character': + if ($tableColumn['codepage'] === 0) { + $type = Types::BINARY; + } + + $length = $tableColumn['length']; + $fixed = true; + break; + + case 'clob': + $length = $tableColumn['length']; + break; + + case 'decimal': + case 'double': + case 'real': + $scale = $tableColumn['scale']; + $precision = $tableColumn['length']; + break; + } + + $options = [ + 'length' => $length, + 'unsigned' => false, + 'fixed' => $fixed, + 'default' => $default, + 'autoincrement' => (bool) $tableColumn['autoincrement'], + 'notnull' => $tableColumn['nulls'] === 'N', + 'platformOptions' => [], + ]; + + if (isset($tableColumn['comment'])) { + $options['comment'] = $tableColumn['comment']; + } + + if ($scale !== null && $precision !== null) { + $options['scale'] = $scale; + $options['precision'] = $precision; + } + + return new Column($tableColumn['colname'], Type::getType($type), $options); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition(array $table): string + { + $table = array_change_key_case($table, CASE_LOWER); + + return $table['name']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array + { + foreach ($tableIndexes as &$tableIndexRow) { + $tableIndexRow = array_change_key_case($tableIndexRow, CASE_LOWER); + $tableIndexRow['primary'] = (bool) $tableIndexRow['primary']; + } + + return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint + { + return new ForeignKeyConstraint( + $tableForeignKey['local_columns'], + $tableForeignKey['foreign_table'], + $tableForeignKey['foreign_columns'], + $tableForeignKey['name'], + $tableForeignKey['options'], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array + { + $foreignKeys = []; + + foreach ($tableForeignKeys as $tableForeignKey) { + $tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER); + + if (! isset($foreignKeys[$tableForeignKey['index_name']])) { + $foreignKeys[$tableForeignKey['index_name']] = [ + 'local_columns' => [$tableForeignKey['local_column']], + 'foreign_table' => $tableForeignKey['foreign_table'], + 'foreign_columns' => [$tableForeignKey['foreign_column']], + 'name' => $tableForeignKey['index_name'], + 'options' => [ + 'onUpdate' => $tableForeignKey['on_update'], + 'onDelete' => $tableForeignKey['on_delete'], + ], + ]; + } else { + $foreignKeys[$tableForeignKey['index_name']]['local_columns'][] = $tableForeignKey['local_column']; + $foreignKeys[$tableForeignKey['index_name']]['foreign_columns'][] = $tableForeignKey['foreign_column']; + } + } + + return parent::_getPortableTableForeignKeysList($foreignKeys); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition(array $view): View + { + $view = array_change_key_case($view, CASE_LOWER); + + $sql = ''; + $pos = strpos($view['text'], ' AS '); + + if ($pos !== false) { + $sql = substr($view['text'], $pos + 4); + } + + return new View($view['name'], $sql); + } + + protected function normalizeName(string $name): string + { + $identifier = new Identifier($name); + + return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name); + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT NAME +FROM SYSIBM.SYSTABLES +WHERE TYPE = 'T' + AND CREATOR = ? +SQL; + + return $this->connection->executeQuery($sql, [$databaseName]); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' C.TABNAME AS NAME,'; + } + + $sql .= <<<'SQL' + C.COLNAME, + C.TYPENAME, + C.CODEPAGE, + C.NULLS, + C.LENGTH, + C.SCALE, + C.REMARKS AS COMMENT, + CASE + WHEN C.GENERATED = 'D' THEN 1 + ELSE 0 + END AS AUTOINCREMENT, + C.DEFAULT +FROM SYSCAT.COLUMNS C + JOIN SYSCAT.TABLES AS T + ON T.TABSCHEMA = C.TABSCHEMA + AND T.TABNAME = C.TABNAME +SQL; + + $conditions = ['C.TABSCHEMA = ?', "T.TYPE = 'T'"]; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'C.TABNAME = ?'; + $params[] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.TABNAME, C.COLNO'; + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' IDX.TABNAME AS NAME,'; + } + + $sql .= <<<'SQL' + IDX.INDNAME AS KEY_NAME, + IDXCOL.COLNAME AS COLUMN_NAME, + CASE + WHEN IDX.UNIQUERULE = 'P' THEN 1 + ELSE 0 + END AS PRIMARY, + CASE + WHEN IDX.UNIQUERULE = 'D' THEN 1 + ELSE 0 + END AS NON_UNIQUE + FROM SYSCAT.INDEXES AS IDX + JOIN SYSCAT.TABLES AS T + ON IDX.TABSCHEMA = T.TABSCHEMA AND IDX.TABNAME = T.TABNAME + JOIN SYSCAT.INDEXCOLUSE AS IDXCOL + ON IDX.INDSCHEMA = IDXCOL.INDSCHEMA AND IDX.INDNAME = IDXCOL.INDNAME +SQL; + + $conditions = ['IDX.TABSCHEMA = ?', "T.TYPE = 'T'"]; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'IDX.TABNAME = ?'; + $params[] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IDX.INDNAME, IDXCOL.COLSEQ'; + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' R.TABNAME AS NAME,'; + } + + $sql .= <<<'SQL' + FKCOL.COLNAME AS LOCAL_COLUMN, + R.REFTABNAME AS FOREIGN_TABLE, + PKCOL.COLNAME AS FOREIGN_COLUMN, + R.CONSTNAME AS INDEX_NAME, + CASE + WHEN R.UPDATERULE = 'R' THEN 'RESTRICT' + END AS ON_UPDATE, + CASE + WHEN R.DELETERULE = 'C' THEN 'CASCADE' + WHEN R.DELETERULE = 'N' THEN 'SET NULL' + WHEN R.DELETERULE = 'R' THEN 'RESTRICT' + END AS ON_DELETE + FROM SYSCAT.REFERENCES AS R + JOIN SYSCAT.TABLES AS T + ON T.TABSCHEMA = R.TABSCHEMA + AND T.TABNAME = R.TABNAME + JOIN SYSCAT.KEYCOLUSE AS FKCOL + ON FKCOL.CONSTNAME = R.CONSTNAME + AND FKCOL.TABSCHEMA = R.TABSCHEMA + AND FKCOL.TABNAME = R.TABNAME + JOIN SYSCAT.KEYCOLUSE AS PKCOL + ON PKCOL.CONSTNAME = R.REFKEYNAME + AND PKCOL.TABSCHEMA = R.REFTABSCHEMA + AND PKCOL.TABNAME = R.REFTABNAME + AND PKCOL.COLSEQ = FKCOL.COLSEQ +SQL; + + $conditions = ['R.TABSCHEMA = ?', "T.TYPE = 'T'"]; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'R.TABNAME = ?'; + $params[] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY R.CONSTNAME, FKCOL.COLSEQ'; + + return $this->connection->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = 'SELECT NAME, REMARKS'; + + $conditions = []; + $params = []; + + if ($tableName !== null) { + $conditions[] = 'NAME = ?'; + $params[] = $tableName; + } + + $sql .= ' FROM SYSIBM.SYSTABLES'; + + if ($conditions !== []) { + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } + + /** @var array> $metadata */ + $metadata = $this->connection->executeQuery($sql, $params) + ->fetchAllAssociativeIndexed(); + + $tableOptions = []; + foreach ($metadata as $table => $data) { + $data = array_change_key_case($data, CASE_LOWER); + + $tableOptions[$table] = ['comment' => $data['remarks']]; + } + + return $tableOptions; + } +} 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 @@ +getDatabasePlatform()->createSchemaManager($connection); + } +} 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 @@ + + */ + protected array $_localColumnNames; + + /** + * Table or asset identifier instance of the referenced table name the foreign key constraint is associated with. + */ + protected Identifier $_foreignTableName; + + /** + * Asset identifier instances of the referenced table column names the foreign key constraint is associated with. + * + * @var array + */ + protected array $_foreignColumnNames; + + /** + * Initializes the foreign key constraint. + * + * @param array $localColumnNames Names of the referencing table columns. + * @param string $foreignTableName Referenced table. + * @param array $foreignColumnNames Names of the referenced table columns. + * @param string $name Name of the foreign key constraint. + * @param array $options Options associated with the foreign key constraint. + */ + public function __construct( + array $localColumnNames, + string $foreignTableName, + array $foreignColumnNames, + string $name = '', + protected array $options = [], + ) { + $this->_setName($name); + + $this->_localColumnNames = $this->createIdentifierMap($localColumnNames); + $this->_foreignTableName = new Identifier($foreignTableName); + + $this->_foreignColumnNames = $this->createIdentifierMap($foreignColumnNames); + } + + /** + * @param array $names + * + * @return array + */ + private function createIdentifierMap(array $names): array + { + $identifiers = []; + + foreach ($names as $name) { + $identifiers[$name] = new Identifier($name); + } + + return $identifiers; + } + + /** + * Returns the names of the referencing table columns + * the foreign key constraint is associated with. + * + * @return array + */ + public function getLocalColumns(): array + { + return array_keys($this->_localColumnNames); + } + + /** + * Returns the quoted representation of the referencing table column names + * the foreign key constraint is associated with. + * + * But only if they were defined with one or the referencing table column name + * is a keyword reserved by the platform. + * Otherwise the plain unquoted value as inserted is returned. + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return array + */ + public function getQuotedLocalColumns(AbstractPlatform $platform): array + { + $columns = []; + + foreach ($this->_localColumnNames as $column) { + $columns[] = $column->getQuotedName($platform); + } + + return $columns; + } + + /** + * Returns unquoted representation of local table column names for comparison with other FK + * + * @return array + */ + public function getUnquotedLocalColumns(): array + { + return array_map($this->trimQuotes(...), $this->getLocalColumns()); + } + + /** + * Returns unquoted representation of foreign table column names for comparison with other FK + * + * @return array + */ + public function getUnquotedForeignColumns(): array + { + return array_map($this->trimQuotes(...), $this->getForeignColumns()); + } + + /** + * Returns the name of the referenced table + * the foreign key constraint is associated with. + */ + public function getForeignTableName(): string + { + return $this->_foreignTableName->getName(); + } + + /** + * Returns the non-schema qualified foreign table name. + */ + public function getUnqualifiedForeignTableName(): string + { + $name = $this->_foreignTableName->getName(); + $position = strrpos($name, '.'); + + if ($position !== false) { + $name = substr($name, $position + 1); + } + + return strtolower($name); + } + + /** + * Returns the quoted representation of the referenced table name + * the foreign key constraint is associated with. + * + * But only if it was defined with one or the referenced table name + * is a keyword reserved by the platform. + * Otherwise the plain unquoted value as inserted is returned. + * + * @param AbstractPlatform $platform The platform to use for quotation. + */ + public function getQuotedForeignTableName(AbstractPlatform $platform): string + { + return $this->_foreignTableName->getQuotedName($platform); + } + + /** + * Returns the names of the referenced table columns + * the foreign key constraint is associated with. + * + * @return array + */ + public function getForeignColumns(): array + { + return array_keys($this->_foreignColumnNames); + } + + /** + * Returns the quoted representation of the referenced table column names + * the foreign key constraint is associated with. + * + * But only if they were defined with one or the referenced table column name + * is a keyword reserved by the platform. + * Otherwise the plain unquoted value as inserted is returned. + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return array + */ + public function getQuotedForeignColumns(AbstractPlatform $platform): array + { + $columns = []; + + foreach ($this->_foreignColumnNames as $column) { + $columns[] = $column->getQuotedName($platform); + } + + return $columns; + } + + /** + * Returns whether or not a given option + * is associated with the foreign key constraint. + */ + public function hasOption(string $name): bool + { + return isset($this->options[$name]); + } + + /** + * Returns an option associated with the foreign key constraint. + */ + public function getOption(string $name): mixed + { + return $this->options[$name]; + } + + /** + * Returns the options associated with the foreign key constraint. + * + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * Returns the referential action for UPDATE operations + * on the referenced table the foreign key constraint is associated with. + */ + public function onUpdate(): ?string + { + return $this->onEvent('onUpdate'); + } + + /** + * Returns the referential action for DELETE operations + * on the referenced table the foreign key constraint is associated with. + */ + public function onDelete(): ?string + { + return $this->onEvent('onDelete'); + } + + /** + * Returns the referential action for a given database operation + * on the referenced table the foreign key constraint is associated with. + * + * @param string $event Name of the database operation/event to return the referential action for. + */ + private function onEvent(string $event): ?string + { + if (isset($this->options[$event])) { + $onEvent = strtoupper($this->options[$event]); + + if ($onEvent !== 'NO ACTION' && $onEvent !== 'RESTRICT') { + return $onEvent; + } + } + + return null; + } + + /** + * Checks whether this foreign key constraint intersects the given index columns. + * + * Returns `true` if at least one of this foreign key's local columns + * matches one of the given index's columns, `false` otherwise. + * + * @param Index $index The index to be checked against. + */ + public function intersectsIndexColumns(Index $index): bool + { + foreach ($index->getColumns() as $indexColumn) { + foreach ($this->_localColumnNames as $localColumn) { + if (strtolower($indexColumn) === strtolower($localColumn->getName())) { + return true; + } + } + } + + return false; + } +} 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 @@ +_setName($identifier); + + if (! $quote || $this->_quoted) { + return; + } + + $this->_setName('"' . $this->getName() . '"'); + } +} 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 @@ + + */ + protected array $_columns = []; + + protected bool $_isUnique = false; + + protected bool $_isPrimary = false; + + /** + * Platform specific flags for indexes. + * + * @var array + */ + protected array $_flags = []; + + /** + * @param array $columns + * @param array $flags + * @param array $options + */ + public function __construct( + ?string $name, + array $columns, + bool $isUnique = false, + bool $isPrimary = false, + array $flags = [], + private readonly array $options = [], + ) { + $isUnique = $isUnique || $isPrimary; + + if ($name !== null) { + $this->_setName($name); + } + + $this->_isUnique = $isUnique; + $this->_isPrimary = $isPrimary; + + foreach ($columns as $column) { + $this->_addColumn($column); + } + + foreach ($flags as $flag) { + $this->addFlag($flag); + } + } + + protected function _addColumn(string $column): void + { + $this->_columns[$column] = new Identifier($column); + } + + /** + * Returns the names of the referencing table columns the constraint is associated with. + * + * @return list + */ + public function getColumns(): array + { + return array_keys($this->_columns); + } + + /** + * Returns the quoted representation of the column names the constraint is associated with. + * + * But only if they were defined with one or a column name + * is a keyword reserved by the platform. + * Otherwise, the plain unquoted value as inserted is returned. + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return list + */ + public function getQuotedColumns(AbstractPlatform $platform): array + { + $subParts = $platform->supportsColumnLengthIndexes() && $this->hasOption('lengths') + ? $this->getOption('lengths') : []; + + $columns = []; + + foreach ($this->_columns as $column) { + $length = array_shift($subParts); + + $quotedColumn = $column->getQuotedName($platform); + + if ($length !== null) { + $quotedColumn .= '(' . $length . ')'; + } + + $columns[] = $quotedColumn; + } + + return $columns; + } + + /** @return array */ + public function getUnquotedColumns(): array + { + return array_map($this->trimQuotes(...), $this->getColumns()); + } + + /** + * Is the index neither unique nor primary key? + */ + public function isSimpleIndex(): bool + { + return ! $this->_isPrimary && ! $this->_isUnique; + } + + public function isUnique(): bool + { + return $this->_isUnique; + } + + public function isPrimary(): bool + { + return $this->_isPrimary; + } + + public function hasColumnAtPosition(string $name, int $pos = 0): bool + { + $name = $this->trimQuotes(strtolower($name)); + $indexColumns = array_map('strtolower', $this->getUnquotedColumns()); + + return array_search($name, $indexColumns, true) === $pos; + } + + /** + * Checks if this index exactly spans the given column names in the correct order. + * + * @param array $columnNames + */ + public function spansColumns(array $columnNames): bool + { + $columns = $this->getColumns(); + $numberOfColumns = count($columns); + $sameColumns = true; + + for ($i = 0; $i < $numberOfColumns; $i++) { + if ( + isset($columnNames[$i]) + && $this->trimQuotes(strtolower($columns[$i])) === $this->trimQuotes(strtolower($columnNames[$i])) + ) { + continue; + } + + $sameColumns = false; + } + + return $sameColumns; + } + + /** + * Checks if the other index already fulfills all the indexing and constraint needs of the current one. + */ + public function isFulfilledBy(Index $other): bool + { + // allow the other index to be equally large only. It being larger is an option + // but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo) + if (count($other->getColumns()) !== count($this->getColumns())) { + return false; + } + + // Check if columns are the same, and even in the same order + $sameColumns = $this->spansColumns($other->getColumns()); + + if ($sameColumns) { + if (! $this->samePartialIndex($other)) { + return false; + } + + if (! $this->hasSameColumnLengths($other)) { + return false; + } + + if (! $this->isUnique() && ! $this->isPrimary()) { + // this is a special case: If the current key is neither primary or unique, any unique or + // primary key will always have the same effect for the index and there cannot be any constraint + // overlaps. This means a primary or unique index can always fulfill the requirements of just an + // index that has no constraints. + return true; + } + + if ($other->isPrimary() !== $this->isPrimary()) { + return false; + } + + return $other->isUnique() === $this->isUnique(); + } + + return false; + } + + /** + * Detects if the other index is a non-unique, non primary index that can be overwritten by this one. + */ + public function overrules(Index $other): bool + { + if ($other->isPrimary()) { + return false; + } + + if ($this->isSimpleIndex() && $other->isUnique()) { + return false; + } + + return $this->spansColumns($other->getColumns()) + && ($this->isPrimary() || $this->isUnique()) + && $this->samePartialIndex($other); + } + + /** + * Returns platform specific flags for indexes. + * + * @return array + */ + public function getFlags(): array + { + return array_keys($this->_flags); + } + + /** + * Adds Flag for an index that translates to platform specific handling. + * + * @example $index->addFlag('CLUSTERED') + */ + public function addFlag(string $flag): self + { + $this->_flags[strtolower($flag)] = true; + + return $this; + } + + /** + * Does this index have a specific flag? + */ + public function hasFlag(string $flag): bool + { + return isset($this->_flags[strtolower($flag)]); + } + + /** + * Removes a flag. + */ + public function removeFlag(string $flag): void + { + unset($this->_flags[strtolower($flag)]); + } + + public function hasOption(string $name): bool + { + return isset($this->options[strtolower($name)]); + } + + public function getOption(string $name): mixed + { + return $this->options[strtolower($name)]; + } + + /** @return array */ + public function getOptions(): array + { + return $this->options; + } + + /** + * Return whether the two indexes have the same partial index + */ + private function samePartialIndex(Index $other): bool + { + if ( + $this->hasOption('where') + && $other->hasOption('where') + && $this->getOption('where') === $other->getOption('where') + ) { + return true; + } + + return ! $this->hasOption('where') && ! $other->hasOption('where'); + } + + /** + * Returns whether the index has the same column lengths as the other + */ + private function hasSameColumnLengths(self $other): bool + { + $filter = static function (?int $length): bool { + return $length !== null; + }; + + return array_filter($this->options['lengths'] ?? [], $filter) + === array_filter($other->options['lengths'] ?? [], $filter); + } +} 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 @@ + + */ +class MySQLSchemaManager extends AbstractSchemaManager +{ + /** @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences */ + private const MARIADB_ESCAPE_SEQUENCES = [ + '\\0' => "\0", + "\\'" => "'", + '\\"' => '"', + '\\b' => "\b", + '\\n' => "\n", + '\\r' => "\r", + '\\t' => "\t", + '\\Z' => "\x1a", + '\\\\' => '\\', + '\\%' => '%', + '\\_' => '_', + + // Internally, MariaDB escapes single quotes using the standard syntax + "''" => "'", + ]; + + private ?DefaultTableOptions $defaultTableOptions = null; + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition(array $table): string + { + return $table['TABLE_NAME']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition(array $view): View + { + return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array + { + foreach ($tableIndexes as $k => $v) { + $v = array_change_key_case($v, CASE_LOWER); + if ($v['key_name'] === 'PRIMARY') { + $v['primary'] = true; + } else { + $v['primary'] = false; + } + + if (str_contains($v['index_type'], 'FULLTEXT')) { + $v['flags'] = ['FULLTEXT']; + } elseif (str_contains($v['index_type'], 'SPATIAL')) { + $v['flags'] = ['SPATIAL']; + } + + // Ignore prohibited prefix `length` for spatial index + if (! str_contains($v['index_type'], 'SPATIAL')) { + $v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null; + } + + $tableIndexes[$k] = $v; + } + + return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableDatabaseDefinition(array $database): string + { + return $database['Database']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition(array $tableColumn): Column + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $dbType = strtolower($tableColumn['type']); + $dbType = strtok($dbType, '(), '); + assert(is_string($dbType)); + + $length = $tableColumn['length'] ?? strtok('(), '); + + $fixed = false; + + if (! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $scale = 0; + $precision = null; + + $type = $this->platform->getDoctrineTypeMapping($dbType); + + switch ($dbType) { + case 'char': + case 'binary': + $fixed = true; + break; + + case 'float': + case 'double': + case 'real': + case 'numeric': + case 'decimal': + if ( + preg_match( + '([A-Za-z]+\(([0-9]+),([0-9]+)\))', + $tableColumn['type'], + $match, + ) === 1 + ) { + $precision = (int) $match[1]; + $scale = (int) $match[2]; + $length = null; + } + + break; + + case 'tinytext': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT; + break; + + case 'text': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_TEXT; + break; + + case 'mediumtext': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT; + break; + + case 'tinyblob': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB; + break; + + case 'blob': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_BLOB; + break; + + case 'mediumblob': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB; + break; + + case 'tinyint': + case 'smallint': + case 'mediumint': + case 'int': + case 'integer': + case 'bigint': + case 'year': + $length = null; + break; + } + + if ($this->platform instanceof MariaDBPlatform) { + $columnDefault = $this->getMariaDBColumnDefault($this->platform, $tableColumn['default']); + } else { + $columnDefault = $tableColumn['default']; + } + + $options = [ + 'length' => $length !== null ? (int) $length : null, + 'unsigned' => str_contains($tableColumn['type'], 'unsigned'), + 'fixed' => $fixed, + 'default' => $columnDefault, + 'notnull' => $tableColumn['null'] !== 'YES', + 'scale' => $scale, + 'precision' => $precision, + 'autoincrement' => str_contains($tableColumn['extra'], 'auto_increment'), + ]; + + if (isset($tableColumn['comment'])) { + $options['comment'] = $tableColumn['comment']; + } + + $column = new Column($tableColumn['field'], Type::getType($type), $options); + + if (isset($tableColumn['characterset'])) { + $column->setPlatformOption('charset', $tableColumn['characterset']); + } + + if (isset($tableColumn['collation'])) { + $column->setPlatformOption('collation', $tableColumn['collation']); + } + + return $column; + } + + /** + * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers. + * + * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted + * to distinguish them from expressions (see MDEV-10134). + * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema + * as current_timestamp(), currdate(), currtime() + * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have + * null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053) + * - \' is always stored as '' in information_schema (normalized) + * + * @link https://mariadb.com/kb/en/library/information-schema-columns-table/ + * @link https://jira.mariadb.org/browse/MDEV-13132 + * + * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7 + */ + private function getMariaDBColumnDefault(MariaDBPlatform $platform, ?string $columnDefault): ?string + { + if ($columnDefault === 'NULL' || $columnDefault === null) { + return null; + } + + if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches) === 1) { + return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES); + } + + return match ($columnDefault) { + 'current_timestamp()' => $platform->getCurrentTimestampSQL(), + 'curdate()' => $platform->getCurrentDateSQL(), + 'curtime()' => $platform->getCurrentTimeSQL(), + default => $columnDefault, + }; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array + { + $list = []; + foreach ($tableForeignKeys as $value) { + $value = array_change_key_case($value, CASE_LOWER); + if (! isset($list[$value['constraint_name']])) { + if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') { + $value['delete_rule'] = null; + } + + if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') { + $value['update_rule'] = null; + } + + $list[$value['constraint_name']] = [ + 'name' => $value['constraint_name'], + 'local' => [], + 'foreign' => [], + 'foreignTable' => $value['referenced_table_name'], + 'onDelete' => $value['delete_rule'], + 'onUpdate' => $value['update_rule'], + ]; + } + + $list[$value['constraint_name']]['local'][] = $value['column_name']; + $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name']; + } + + return parent::_getPortableTableForeignKeysList($list); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint + { + return new ForeignKeyConstraint( + $tableForeignKey['local'], + $tableForeignKey['foreignTable'], + $tableForeignKey['foreign'], + $tableForeignKey['name'], + [ + 'onDelete' => $tableForeignKey['onDelete'], + 'onUpdate' => $tableForeignKey['onUpdate'], + ], + ); + } + + /** @throws Exception */ + public function createComparator(): Comparator + { + return new MySQL\Comparator( + $this->platform, + new CachingCharsetMetadataProvider( + new ConnectionCharsetMetadataProvider($this->connection), + ), + new CachingCollationMetadataProvider( + new ConnectionCollationMetadataProvider($this->connection), + ), + $this->getDefaultTableOptions(), + ); + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT TABLE_NAME +FROM information_schema.TABLES +WHERE TABLE_SCHEMA = ? + AND TABLE_TYPE = 'BASE TABLE' +ORDER BY TABLE_NAME +SQL; + + return $this->connection->executeQuery($sql, [$databaseName]); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $columnTypeSQL = $this->platform->getColumnTypeSQLSnippet('c', $databaseName); + + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' c.TABLE_NAME,'; + } + + $sql .= <<connection->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' TABLE_NAME,'; + } + + $sql .= <<<'SQL' + NON_UNIQUE AS Non_Unique, + INDEX_NAME AS Key_name, + COLUMN_NAME AS Column_Name, + SUB_PART AS Sub_Part, + INDEX_TYPE AS Index_Type +FROM information_schema.STATISTICS +SQL; + + $conditions = ['TABLE_SCHEMA = ?']; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'TABLE_NAME = ?'; + $params[] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY SEQ_IN_INDEX'; + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT DISTINCT'; + + if ($tableName === null) { + $sql .= ' k.TABLE_NAME,'; + } + + $sql .= <<<'SQL' + k.CONSTRAINT_NAME, + k.COLUMN_NAME, + k.REFERENCED_TABLE_NAME, + k.REFERENCED_COLUMN_NAME, + k.ORDINAL_POSITION, + c.UPDATE_RULE, + c.DELETE_RULE +FROM information_schema.key_column_usage k +INNER JOIN information_schema.referential_constraints c +ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME +AND c.TABLE_NAME = k.TABLE_NAME +SQL; + + $conditions = ['k.TABLE_SCHEMA = ?']; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'k.TABLE_NAME = ?'; + $params[] = $tableName; + } + + // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition + // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions + // caused by https://bugs.mysql.com/bug.php?id=81347 + $conditions[] = 'c.CONSTRAINT_SCHEMA = ?'; + $params[] = $databaseName; + + $conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL'; + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY k.ORDINAL_POSITION'; + + return $this->connection->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + // MariaDB-10.10.1 added FULL_COLLATION_NAME to the information_schema.COLLATION_CHARACTER_SET_APPLICABILITY. + // A base collation like uca1400_ai_ci can refer to multiple character sets. The value in the + // information_schema.TABLES.TABLE_COLLATION corresponds to the full collation name. + // The MariaDB executable comment syntax with version, /*M!101001, is exclusively executed on + // MariaDB-10.10.1+ servers for backwards compatibility, and compatiblity to MySQL servers. + $sql = <<<'SQL' + SELECT t.TABLE_NAME, + t.ENGINE, + t.AUTO_INCREMENT, + t.TABLE_COMMENT, + t.CREATE_OPTIONS, + t.TABLE_COLLATION, + ccsa.CHARACTER_SET_NAME + FROM information_schema.TABLES t + INNER JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY ccsa + ON /*M!101001 ccsa.FULL_COLLATION_NAME = t.TABLE_COLLATION OR */ + ccsa.COLLATION_NAME = t.TABLE_COLLATION +SQL; + + $conditions = ['t.TABLE_SCHEMA = ?']; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 't.TABLE_NAME = ?'; + $params[] = $tableName; + } + + $conditions[] = "t.TABLE_TYPE = 'BASE TABLE'"; + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + + /** @var array> $metadata */ + $metadata = $this->connection->executeQuery($sql, $params) + ->fetchAllAssociativeIndexed(); + + $tableOptions = []; + foreach ($metadata as $table => $data) { + $data = array_change_key_case($data, CASE_LOWER); + + $tableOptions[$table] = [ + 'engine' => $data['engine'], + 'collation' => $data['table_collation'], + 'charset' => $data['character_set_name'], + 'autoincrement' => $data['auto_increment'], + 'comment' => $data['table_comment'], + 'create_options' => $this->parseCreateOptions($data['create_options']), + ]; + } + + return $tableOptions; + } + + /** @return array|array */ + private function parseCreateOptions(?string $string): array + { + $options = []; + + if ($string === null || $string === '') { + return $options; + } + + foreach (explode(' ', $string) as $pair) { + $parts = explode('=', $pair, 2); + + $options[$parts[0]] = $parts[1] ?? true; + } + + return $options; + } + + /** @throws Exception */ + private function getDefaultTableOptions(): DefaultTableOptions + { + if ($this->defaultTableOptions === null) { + $row = $this->connection->fetchNumeric( + 'SELECT @@character_set_database, @@collation_database', + ); + + assert($row !== false); + + $this->defaultTableOptions = new DefaultTableOptions(...$row); + } + + return $this->defaultTableOptions; + } +} 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 @@ + + */ +class OracleSchemaManager extends AbstractSchemaManager +{ + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition(array $view): View + { + $view = array_change_key_case($view, CASE_LOWER); + + return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition(array $table): string + { + $table = array_change_key_case($table, CASE_LOWER); + + return $this->getQuotedIdentifierName($table['table_name']); + } + + /** + * {@inheritDoc} + * + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + */ + protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array + { + $indexBuffer = []; + foreach ($tableIndexes as $tableIndex) { + $tableIndex = array_change_key_case($tableIndex, CASE_LOWER); + + $keyName = strtolower($tableIndex['name']); + $buffer = []; + + if ($tableIndex['is_primary'] === 'P') { + $keyName = 'primary'; + $buffer['primary'] = true; + $buffer['non_unique'] = false; + } else { + $buffer['primary'] = false; + $buffer['non_unique'] = ! $tableIndex['is_unique']; + } + + $buffer['key_name'] = $keyName; + $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']); + $indexBuffer[] = $buffer; + } + + return parent::_getPortableTableIndexesList($indexBuffer, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition(array $tableColumn): Column + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $dbType = strtolower($tableColumn['data_type']); + if (str_starts_with($dbType, 'timestamp(')) { + if (str_contains($dbType, 'with time zone')) { + $dbType = 'timestamptz'; + } else { + $dbType = 'timestamp'; + } + } + + $length = $precision = null; + $scale = 0; + $fixed = false; + + if (! isset($tableColumn['column_name'])) { + $tableColumn['column_name'] = ''; + } + + assert(array_key_exists('data_default', $tableColumn)); + + // Default values returned from database sometimes have trailing spaces. + if (is_string($tableColumn['data_default'])) { + $tableColumn['data_default'] = trim($tableColumn['data_default']); + } + + if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') { + $tableColumn['data_default'] = null; + } + + if ($tableColumn['data_default'] !== null) { + // Default values returned from database are represented as literal expressions + if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches) === 1) { + $tableColumn['data_default'] = str_replace("''", "'", $matches[1]); + } + } + + if ($tableColumn['data_precision'] !== null) { + $precision = (int) $tableColumn['data_precision']; + } + + if ($tableColumn['data_scale'] !== null) { + $scale = (int) $tableColumn['data_scale']; + } + + $type = $this->platform->getDoctrineTypeMapping($dbType); + + switch ($dbType) { + case 'number': + if ($precision === 20 && $scale === 0) { + $type = 'bigint'; + } elseif ($precision === 5 && $scale === 0) { + $type = 'smallint'; + } elseif ($precision === 1 && $scale === 0) { + $type = 'boolean'; + } elseif ($scale > 0) { + $type = 'decimal'; + } + + break; + + case 'varchar': + case 'varchar2': + case 'nvarchar2': + $length = (int) $tableColumn['char_length']; + break; + + case 'raw': + $length = (int) $tableColumn['data_length']; + $fixed = true; + break; + + case 'char': + case 'nchar': + $length = (int) $tableColumn['char_length']; + $fixed = true; + break; + } + + $options = [ + 'notnull' => $tableColumn['nullable'] === 'N', + 'fixed' => $fixed, + 'default' => $tableColumn['data_default'], + 'length' => $length, + 'precision' => $precision, + 'scale' => $scale, + ]; + + if (isset($tableColumn['comments'])) { + $options['comment'] = $tableColumn['comments']; + } + + return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array + { + $list = []; + foreach ($tableForeignKeys as $value) { + $value = array_change_key_case($value, CASE_LOWER); + if (! isset($list[$value['constraint_name']])) { + if ($value['delete_rule'] === 'NO ACTION') { + $value['delete_rule'] = null; + } + + $list[$value['constraint_name']] = [ + 'name' => $this->getQuotedIdentifierName($value['constraint_name']), + 'local' => [], + 'foreign' => [], + 'foreignTable' => $value['references_table'], + 'onDelete' => $value['delete_rule'], + ]; + } + + $localColumn = $this->getQuotedIdentifierName($value['local_column']); + $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']); + + $list[$value['constraint_name']]['local'][$value['position']] = $localColumn; + $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn; + } + + return parent::_getPortableTableForeignKeysList($list); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint + { + return new ForeignKeyConstraint( + array_values($tableForeignKey['local']), + $this->getQuotedIdentifierName($tableForeignKey['foreignTable']), + array_values($tableForeignKey['foreign']), + $this->getQuotedIdentifierName($tableForeignKey['name']), + ['onDelete' => $tableForeignKey['onDelete']], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableSequenceDefinition(array $sequence): Sequence + { + $sequence = array_change_key_case($sequence, CASE_LOWER); + + return new Sequence( + $this->getQuotedIdentifierName($sequence['sequence_name']), + (int) $sequence['increment_by'], + (int) $sequence['min_value'], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableDatabaseDefinition(array $database): string + { + $database = array_change_key_case($database, CASE_LOWER); + + return $database['username']; + } + + public function createDatabase(string $database): void + { + $statement = $this->platform->getCreateDatabaseSQL($database); + + $params = $this->connection->getParams(); + + if (isset($params['password'])) { + $statement .= ' IDENTIFIED BY ' . $params['password']; + } + + $this->connection->executeStatement($statement); + + $statement = 'GRANT DBA TO ' . $database; + $this->connection->executeStatement($statement); + } + + /** @throws Exception */ + protected function dropAutoincrement(string $table): bool + { + $sql = $this->platform->getDropAutoincrementSql($table); + foreach ($sql as $query) { + $this->connection->executeStatement($query); + } + + return true; + } + + public function dropTable(string $name): void + { + try { + $this->dropAutoincrement($name); + } catch (DatabaseObjectNotFoundException) { + } + + parent::dropTable($name); + } + + /** + * Returns the quoted representation of the given identifier name. + * + * Quotes non-uppercase identifiers explicitly to preserve case + * and thus make references to the particular identifier work. + */ + private function getQuotedIdentifierName(string $identifier): string + { + if (preg_match('/[a-z]/', $identifier) === 1) { + return $this->platform->quoteIdentifier($identifier); + } + + return $identifier; + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT TABLE_NAME +FROM ALL_TABLES +WHERE OWNER = :OWNER +ORDER BY TABLE_NAME +SQL; + + return $this->connection->executeQuery($sql, ['OWNER' => $databaseName]); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' C.TABLE_NAME,'; + } + + $sql .= <<<'SQL' + C.COLUMN_NAME, + C.DATA_TYPE, + C.DATA_DEFAULT, + C.DATA_PRECISION, + C.DATA_SCALE, + C.CHAR_LENGTH, + C.DATA_LENGTH, + C.NULLABLE, + D.COMMENTS + FROM ALL_TAB_COLUMNS C + INNER JOIN ALL_TABLES T + ON T.OWNER = C.OWNER + AND T.TABLE_NAME = C.TABLE_NAME + LEFT JOIN ALL_COL_COMMENTS D + ON D.OWNER = C.OWNER + AND D.TABLE_NAME = C.TABLE_NAME + AND D.COLUMN_NAME = C.COLUMN_NAME +SQL; + + $conditions = ['C.OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; + + if ($tableName !== null) { + $conditions[] = 'C.TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.COLUMN_ID'; + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' IND_COL.TABLE_NAME,'; + } + + $sql .= <<<'SQL' + IND_COL.INDEX_NAME AS NAME, + IND.INDEX_TYPE AS TYPE, + DECODE(IND.UNIQUENESS, 'NONUNIQUE', 0, 'UNIQUE', 1) AS IS_UNIQUE, + IND_COL.COLUMN_NAME, + IND_COL.COLUMN_POSITION AS COLUMN_POS, + CON.CONSTRAINT_TYPE AS IS_PRIMARY + FROM ALL_IND_COLUMNS IND_COL + LEFT JOIN ALL_INDEXES IND + ON IND.OWNER = IND_COL.INDEX_OWNER + AND IND.INDEX_NAME = IND_COL.INDEX_NAME + LEFT JOIN ALL_CONSTRAINTS CON + ON CON.OWNER = IND_COL.INDEX_OWNER + AND CON.INDEX_NAME = IND_COL.INDEX_NAME +SQL; + + $conditions = ['IND_COL.INDEX_OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; + + if ($tableName !== null) { + $conditions[] = 'IND_COL.TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IND_COL.TABLE_NAME, IND_COL.INDEX_NAME' + . ', IND_COL.COLUMN_POSITION'; + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' COLS.TABLE_NAME,'; + } + + $sql .= <<<'SQL' + ALC.CONSTRAINT_NAME, + ALC.DELETE_RULE, + COLS.COLUMN_NAME LOCAL_COLUMN, + COLS.POSITION, + R_COLS.TABLE_NAME REFERENCES_TABLE, + R_COLS.COLUMN_NAME FOREIGN_COLUMN + FROM ALL_CONS_COLUMNS COLS + LEFT JOIN ALL_CONSTRAINTS ALC ON ALC.OWNER = COLS.OWNER AND ALC.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME + LEFT JOIN ALL_CONS_COLUMNS R_COLS ON R_COLS.OWNER = ALC.R_OWNER AND + R_COLS.CONSTRAINT_NAME = ALC.R_CONSTRAINT_NAME AND + R_COLS.POSITION = COLS.POSITION +SQL; + + $conditions = ["ALC.CONSTRAINT_TYPE = 'R'", 'COLS.OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; + + if ($tableName !== null) { + $conditions[] = 'COLS.TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY COLS.TABLE_NAME, COLS.CONSTRAINT_NAME' + . ', COLS.POSITION'; + + return $this->connection->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = 'SELECT TABLE_NAME, COMMENTS'; + + $conditions = ['OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; + + if ($tableName !== null) { + $conditions[] = 'TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; + } + + $sql .= ' FROM ALL_TAB_COMMENTS WHERE ' . implode(' AND ', $conditions); + + /** @var array> $metadata */ + $metadata = $this->connection->executeQuery($sql, $params) + ->fetchAllAssociativeIndexed(); + + $tableOptions = []; + foreach ($metadata as $table => $data) { + $data = array_change_key_case($data, CASE_LOWER); + + $tableOptions[$table] = [ + 'comment' => $data['comments'], + ]; + } + + return $tableOptions; + } + + protected function normalizeName(string $name): string + { + $identifier = new Identifier($name); + + return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name); + } +} 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 @@ + + */ +class PostgreSQLSchemaManager extends AbstractSchemaManager +{ + private ?string $currentSchema = null; + + /** + * {@inheritDoc} + */ + public function listSchemaNames(): array + { + return $this->connection->fetchFirstColumn( + <<<'SQL' +SELECT schema_name +FROM information_schema.schemata +WHERE schema_name NOT LIKE 'pg\_%' +AND schema_name != 'information_schema' +SQL, + ); + } + + public function createSchemaConfig(): SchemaConfig + { + $config = parent::createSchemaConfig(); + + $config->setName($this->getCurrentSchema()); + + return $config; + } + + /** + * Returns the name of the current schema. + * + * @throws Exception + */ + protected function getCurrentSchema(): ?string + { + return $this->currentSchema ??= $this->determineCurrentSchema(); + } + + /** + * Determines the name of the current schema. + * + * @throws Exception + */ + protected function determineCurrentSchema(): string + { + $currentSchema = $this->connection->fetchOne('SELECT current_schema()'); + assert(is_string($currentSchema)); + + return $currentSchema; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint + { + $onUpdate = null; + $onDelete = null; + + if ( + preg_match( + '(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', + $tableForeignKey['condef'], + $match, + ) === 1 + ) { + $onUpdate = $match[1]; + } + + if ( + preg_match( + '(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', + $tableForeignKey['condef'], + $match, + ) === 1 + ) { + $onDelete = $match[1]; + } + + $result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values); + assert($result === 1); + + // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get + // the idea to trim them here. + $localColumns = array_map('trim', explode(',', $values[1])); + $foreignColumns = array_map('trim', explode(',', $values[3])); + $foreignTable = $values[2]; + + return new ForeignKeyConstraint( + $localColumns, + $foreignTable, + $foreignColumns, + $tableForeignKey['conname'], + ['onUpdate' => $onUpdate, 'onDelete' => $onDelete], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition(array $view): View + { + return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition(array $table): string + { + $currentSchema = $this->getCurrentSchema(); + + if ($table['schema_name'] === $currentSchema) { + return $table['table_name']; + } + + return $table['schema_name'] . '.' . $table['table_name']; + } + + /** + * {@inheritDoc} + * + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + */ + protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array + { + $buffer = []; + foreach ($tableIndexes as $row) { + $colNumbers = array_map('intval', explode(' ', $row['indkey'])); + $columnNameSql = sprintf( + 'SELECT attnum, attname FROM pg_attribute WHERE attrelid=%d AND attnum IN (%s) ORDER BY attnum ASC', + $row['indrelid'], + implode(' ,', $colNumbers), + ); + + $indexColumns = $this->connection->fetchAllAssociative($columnNameSql); + + // required for getting the order of the columns right. + foreach ($colNumbers as $colNum) { + foreach ($indexColumns as $colRow) { + if ($colNum !== $colRow['attnum']) { + continue; + } + + $buffer[] = [ + 'key_name' => $row['relname'], + 'column_name' => trim($colRow['attname']), + 'non_unique' => ! $row['indisunique'], + 'primary' => $row['indisprimary'], + 'where' => $row['where'], + ]; + } + } + } + + return parent::_getPortableTableIndexesList($buffer, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableDatabaseDefinition(array $database): string + { + return $database['datname']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableSequenceDefinition(array $sequence): Sequence + { + if ($sequence['schemaname'] !== 'public') { + $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname']; + } else { + $sequenceName = $sequence['relname']; + } + + return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition(array $tableColumn): Column + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $length = null; + + if ( + in_array(strtolower($tableColumn['type']), ['varchar', 'bpchar'], true) + && preg_match('/\((\d*)\)/', $tableColumn['complete_type'], $matches) === 1 + ) { + $length = (int) $matches[1]; + } + + $autoincrement = $tableColumn['attidentity'] === 'd'; + + $matches = []; + + assert(array_key_exists('default', $tableColumn)); + assert(array_key_exists('complete_type', $tableColumn)); + + if ($tableColumn['default'] !== null) { + if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) { + $tableColumn['default'] = $matches[1]; + } elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) { + $tableColumn['default'] = null; + } + } + + if ($length === -1 && isset($tableColumn['atttypmod'])) { + $length = $tableColumn['atttypmod'] - 4; + } + + if ((int) $length <= 0) { + $length = null; + } + + $fixed = false; + + if (! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $precision = null; + $scale = 0; + $jsonb = null; + + $dbType = strtolower($tableColumn['type']); + if ( + $tableColumn['domain_type'] !== null + && $tableColumn['domain_type'] !== '' + && ! $this->platform->hasDoctrineTypeMappingFor($tableColumn['type']) + ) { + $dbType = strtolower($tableColumn['domain_type']); + $tableColumn['complete_type'] = $tableColumn['domain_complete_type']; + } + + $type = $this->platform->getDoctrineTypeMapping($dbType); + + switch ($dbType) { + case 'smallint': + case 'int2': + case 'int': + case 'int4': + case 'integer': + case 'bigint': + case 'int8': + $length = null; + break; + + case 'bool': + case 'boolean': + if ($tableColumn['default'] === 'true') { + $tableColumn['default'] = true; + } + + if ($tableColumn['default'] === 'false') { + $tableColumn['default'] = false; + } + + $length = null; + break; + + case 'json': + case 'text': + case '_varchar': + case 'varchar': + $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']); + break; + + case 'char': + case 'bpchar': + $fixed = true; + break; + + case 'float': + case 'float4': + case 'float8': + case 'double': + case 'double precision': + case 'real': + case 'decimal': + case 'money': + case 'numeric': + if ( + preg_match( + '([A-Za-z]+\(([0-9]+),([0-9]+)\))', + $tableColumn['complete_type'], + $match, + ) === 1 + ) { + $precision = (int) $match[1]; + $scale = (int) $match[2]; + $length = null; + } + + break; + + case 'year': + $length = null; + break; + + // PostgreSQL 9.4+ only + case 'jsonb': + $jsonb = true; + break; + } + + if ( + is_string($tableColumn['default']) && preg_match( + "('([^']+)'::)", + $tableColumn['default'], + $match, + ) === 1 + ) { + $tableColumn['default'] = $match[1]; + } + + $options = [ + 'length' => $length, + 'notnull' => (bool) $tableColumn['isnotnull'], + 'default' => $tableColumn['default'], + 'precision' => $precision, + 'scale' => $scale, + 'fixed' => $fixed, + 'autoincrement' => $autoincrement, + ]; + + if (isset($tableColumn['comment'])) { + $options['comment'] = $tableColumn['comment']; + } + + $column = new Column($tableColumn['field'], Type::getType($type), $options); + + if (! empty($tableColumn['collation'])) { + $column->setPlatformOption('collation', $tableColumn['collation']); + } + + if ($column->getType() instanceof JsonType) { + $column->setPlatformOption('jsonb', $jsonb); + } + + return $column; + } + + /** + * Parses a default value expression as given by PostgreSQL + */ + private function parseDefaultExpression(?string $default): ?string + { + if ($default === null) { + return $default; + } + + return str_replace("''", "'", $default); + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT quote_ident(table_name) AS table_name, + table_schema AS schema_name +FROM information_schema.tables +WHERE table_catalog = ? + AND table_schema NOT LIKE 'pg\_%' + AND table_schema != 'information_schema' + AND table_name != 'geometry_columns' + AND table_name != 'spatial_ref_sys' + AND table_type = 'BASE TABLE' +SQL; + + return $this->connection->executeQuery($sql, [$databaseName]); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' c.relname AS table_name, n.nspname AS schema_name,'; + } + + $sql .= <<<'SQL' + a.attnum, + quote_ident(a.attname) AS field, + t.typname AS type, + format_type(a.atttypid, a.atttypmod) AS complete_type, + (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation, + (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, + (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM + pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, + a.attnotnull AS isnotnull, + a.attidentity, + (SELECT 't' + FROM pg_index + WHERE c.oid = pg_index.indrelid + AND pg_index.indkey[0] = a.attnum + AND pg_index.indisprimary = 't' + ) AS pri, + (SELECT pg_get_expr(adbin, adrelid) + FROM pg_attrdef + WHERE c.oid = pg_attrdef.adrelid + AND pg_attrdef.adnum=a.attnum + ) AS default, + (SELECT pg_description.description + FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid + ) AS comment + FROM pg_attribute a + INNER JOIN pg_class c + ON c.oid = a.attrelid + INNER JOIN pg_type t + ON t.oid = a.atttypid + INNER JOIN pg_namespace n + ON n.oid = c.relnamespace + LEFT JOIN pg_depend d + ON d.objid = c.oid + AND d.deptype = 'e' + AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class') +SQL; + + $conditions = array_merge([ + 'a.attnum > 0', + "c.relkind = 'r'", + 'd.refobjid IS NULL', + ], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY a.attnum'; + + return $this->connection->executeQuery($sql); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; + } + + $sql .= <<<'SQL' + quote_ident(ic.relname) AS relname, + i.indisunique, + i.indisprimary, + i.indkey, + i.indrelid, + pg_get_expr(indpred, indrelid) AS "where" + FROM pg_index i + JOIN pg_class AS tc ON tc.oid = i.indrelid + JOIN pg_namespace tn ON tn.oid = tc.relnamespace + JOIN pg_class AS ic ON ic.oid = i.indexrelid + WHERE ic.oid IN ( + SELECT indexrelid + FROM pg_index i, pg_class c, pg_namespace n +SQL; + + $conditions = array_merge([ + 'c.oid = i.indrelid', + 'c.relnamespace = n.oid', + ], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ')'; + + return $this->connection->executeQuery($sql); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' tc.relname AS table_name, tn.nspname AS schema_name,'; + } + + $sql .= <<<'SQL' + quote_ident(r.conname) as conname, + pg_get_constraintdef(r.oid, true) as condef + FROM pg_constraint r + JOIN pg_class AS tc ON tc.oid = r.conrelid + JOIN pg_namespace tn ON tn.oid = tc.relnamespace + WHERE r.conrelid IN + ( + SELECT c.oid + FROM pg_class c, pg_namespace n +SQL; + + $conditions = array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f'"; + + return $this->connection->executeQuery($sql); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = <<<'SQL' +SELECT c.relname, + CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged, + obj_description(c.oid, 'pg_class') AS comment +FROM pg_class c + INNER JOIN pg_namespace n + ON n.oid = c.relnamespace +SQL; + + $conditions = array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + + return $this->connection->fetchAllAssociativeIndexed($sql); + } + + /** @return list */ + private function buildQueryConditions(?string $tableName): array + { + $conditions = []; + + if ($tableName !== null) { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + $conditions[] = 'n.nspname = ' . $this->platform->quoteStringLiteral($schemaName); + } else { + $conditions[] = 'n.nspname = ANY(current_schemas(false))'; + } + + $identifier = new Identifier($tableName); + $conditions[] = 'c.relname = ' . $this->platform->quoteStringLiteral($identifier->getName()); + } + + $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')"; + + return $conditions; + } +} 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 @@ + + */ +class SQLServerSchemaManager extends AbstractSchemaManager +{ + private ?string $databaseCollation = null; + + /** + * {@inheritDoc} + */ + public function listSchemaNames(): array + { + return $this->connection->fetchFirstColumn( + <<<'SQL' +SELECT name +FROM sys.schemas +WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys') +SQL, + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableSequenceDefinition(array $sequence): Sequence + { + return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition(array $tableColumn): Column + { + $dbType = strtok($tableColumn['type'], '(), '); + assert(is_string($dbType)); + + $length = (int) $tableColumn['length']; + + $precision = null; + + $scale = 0; + $fixed = false; + + if (! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + if ($tableColumn['scale'] !== null) { + $scale = (int) $tableColumn['scale']; + } + + if ($tableColumn['precision'] !== null) { + $precision = (int) $tableColumn['precision']; + } + + switch ($dbType) { + case 'nchar': + case 'ntext': + // Unicode data requires 2 bytes per character + $length /= 2; + break; + + case 'nvarchar': + if ($length === -1) { + break; + } + + // Unicode data requires 2 bytes per character + $length /= 2; + break; + + case 'varchar': + // TEXT type is returned as VARCHAR(MAX) with a length of -1 + if ($length === -1) { + $dbType = 'text'; + } + + break; + + case 'varbinary': + if ($length === -1) { + $dbType = 'blob'; + } + + break; + } + + if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') { + $fixed = true; + } + + $type = $this->platform->getDoctrineTypeMapping($dbType); + + $options = [ + 'fixed' => $fixed, + 'notnull' => (bool) $tableColumn['notnull'], + 'scale' => $scale, + 'precision' => $precision, + 'autoincrement' => (bool) $tableColumn['autoincrement'], + ]; + + if (isset($tableColumn['comment'])) { + $options['comment'] = $tableColumn['comment']; + } + + if ($length !== 0 && ($type === 'text' || $type === 'string' || $type === 'binary')) { + $options['length'] = $length; + } + + $column = new Column($tableColumn['name'], Type::getType($type), $options); + + if ($tableColumn['default'] !== null) { + $default = $this->parseDefaultExpression($tableColumn['default']); + + $column->setDefault($default); + $column->setPlatformOption( + SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME, + $tableColumn['df_name'], + ); + } + + if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') { + $column->setPlatformOption('collation', $tableColumn['collation']); + } + + return $column; + } + + private function parseDefaultExpression(string $value): ?string + { + while (preg_match('/^\((.*)\)$/s', $value, $matches)) { + $value = $matches[1]; + } + + if ($value === 'NULL') { + return null; + } + + if (preg_match('/^\'(.*)\'$/s', $value, $matches) === 1) { + $value = str_replace("''", "'", $matches[1]); + } + + if ($value === 'getdate()') { + return $this->platform->getCurrentTimestampSQL(); + } + + return $value; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array + { + $foreignKeys = []; + + foreach ($tableForeignKeys as $tableForeignKey) { + $name = $tableForeignKey['ForeignKey']; + + if (! isset($foreignKeys[$name])) { + $foreignKeys[$name] = [ + 'local_columns' => [$tableForeignKey['ColumnName']], + 'foreign_table' => $tableForeignKey['ReferenceTableName'], + 'foreign_columns' => [$tableForeignKey['ReferenceColumnName']], + 'name' => $name, + 'options' => [ + 'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']), + 'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']), + ], + ]; + } else { + $foreignKeys[$name]['local_columns'][] = $tableForeignKey['ColumnName']; + $foreignKeys[$name]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName']; + } + } + + return parent::_getPortableTableForeignKeysList($foreignKeys); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array + { + foreach ($tableIndexes as &$tableIndex) { + $tableIndex['non_unique'] = (bool) $tableIndex['non_unique']; + $tableIndex['primary'] = (bool) $tableIndex['primary']; + $tableIndex['flags'] = $tableIndex['flags'] ? [$tableIndex['flags']] : null; + } + + return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint + { + return new ForeignKeyConstraint( + $tableForeignKey['local_columns'], + $tableForeignKey['foreign_table'], + $tableForeignKey['foreign_columns'], + $tableForeignKey['name'], + $tableForeignKey['options'], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition(array $table): string + { + if ($table['schema_name'] !== 'dbo') { + return $table['schema_name'] . '.' . $table['table_name']; + } + + return $table['table_name']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableDatabaseDefinition(array $database): string + { + return $database['name']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition(array $view): View + { + return new View($view['name'], $view['definition']); + } + + /** @throws Exception */ + public function createComparator(): Comparator + { + return new SQLServer\Comparator($this->platform, $this->getDatabaseCollation()); + } + + /** @throws Exception */ + private function getDatabaseCollation(): string + { + if ($this->databaseCollation === null) { + $databaseCollation = $this->connection->fetchOne( + 'SELECT collation_name FROM sys.databases WHERE name = ' + . $this->platform->getCurrentDatabaseExpression(), + ); + + // a database is always selected, even if omitted in the connection parameters + assert(is_string($databaseCollation)); + + $this->databaseCollation = $databaseCollation; + } + + return $this->databaseCollation; + } + + protected function selectTableNames(string $databaseName): Result + { + // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams + $sql = <<<'SQL' +SELECT name AS table_name, + SCHEMA_NAME(schema_id) AS schema_name +FROM sys.objects +WHERE type = 'U' + AND name != 'sysdiagrams' +ORDER BY name +SQL; + + return $this->connection->executeQuery($sql); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' obj.name AS table_name, scm.name AS schema_name,'; + } + + $sql .= <<<'SQL' + col.name, + type.name AS type, + col.max_length AS length, + ~col.is_nullable AS notnull, + def.definition AS [default], + def.name AS df_name, + col.scale, + col.precision, + col.is_identity AS autoincrement, + col.collation_name AS collation, + -- CAST avoids driver error for sql_variant type + CAST(prop.value AS NVARCHAR(MAX)) AS comment + FROM sys.columns AS col + JOIN sys.types AS type + ON col.user_type_id = type.user_type_id + JOIN sys.objects AS obj + ON col.object_id = obj.object_id + JOIN sys.schemas AS scm + ON obj.schema_id = scm.schema_id + LEFT JOIN sys.default_constraints def + ON col.default_object_id = def.object_id + AND col.object_id = def.parent_object_id + LEFT JOIN sys.extended_properties AS prop + ON obj.object_id = prop.major_id + AND col.column_id = prop.minor_id + AND prop.name = 'MS_Description' +SQL; + + // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams + $conditions = ["obj.type = 'U'", "obj.name != 'sysdiagrams'"]; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'obj.name'); + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' tbl.name AS table_name, scm.name AS schema_name,'; + } + + $sql .= <<<'SQL' + idx.name AS key_name, + col.name AS column_name, + ~idx.is_unique AS non_unique, + idx.is_primary_key AS [primary], + CASE idx.type + WHEN '1' THEN 'clustered' + WHEN '2' THEN 'nonclustered' + ELSE NULL + END AS flags + FROM sys.tables AS tbl + JOIN sys.schemas AS scm + ON tbl.schema_id = scm.schema_id + JOIN sys.indexes AS idx + ON tbl.object_id = idx.object_id + JOIN sys.index_columns AS idxcol + ON idx.object_id = idxcol.object_id + AND idx.index_id = idxcol.index_id + JOIN sys.columns AS col + ON idxcol.object_id = col.object_id + AND idxcol.column_id = col.column_id +SQL; + + $conditions = []; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name'); + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } + + $sql .= ' ORDER BY idx.index_id, idxcol.key_ordinal'; + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' OBJECT_NAME (f.parent_object_id) AS table_name, SCHEMA_NAME(f.schema_id) AS schema_name,'; + } + + $sql .= <<<'SQL' + f.name AS ForeignKey, + SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName, + OBJECT_NAME (f.parent_object_id) AS TableName, + COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName, + SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName, + OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName, + COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName, + f.delete_referential_action_desc, + f.update_referential_action_desc + FROM sys.foreign_keys AS f + INNER JOIN sys.foreign_key_columns AS fc + INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id + ON f.OBJECT_ID = fc.constraint_object_id +SQL; + + $conditions = []; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause( + $tableName, + 'SCHEMA_NAME(f.schema_id)', + 'OBJECT_NAME(f.parent_object_id)', + ); + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } + + $sql .= ' ORDER BY fc.constraint_column_id'; + + return $this->connection->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = <<<'SQL' + SELECT + tbl.name, + p.value AS [table_comment] + FROM + sys.tables AS tbl + INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1 +SQL; + + $conditions = ["SCHEMA_NAME(tbl.schema_id) = N'dbo'", "p.name = N'MS_Description'"]; + $params = []; + + if ($tableName !== null) { + $conditions[] = "tbl.name = N'" . $tableName . "'"; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + + /** @var array> $metadata */ + $metadata = $this->connection->executeQuery($sql, $params) + ->fetchAllAssociativeIndexed(); + + $tableOptions = []; + foreach ($metadata as $table => $data) { + $data = array_change_key_case($data, CASE_LOWER); + + $tableOptions[$table] = [ + 'comment' => $data['table_comment'], + ]; + } + + return $tableOptions; + } + + /** + * Returns the where clause to filter schema and table name in a query. + * + * @param string $table The full qualified name of the table. + * @param string $schemaColumn The name of the column to compare the schema to in the where clause. + * @param string $tableColumn The name of the column to compare the table to in the where clause. + */ + private function getTableWhereClause(string $table, string $schemaColumn, string $tableColumn): string + { + if (str_contains($table, '.')) { + [$schema, $table] = explode('.', $table); + $schema = $this->platform->quoteStringLiteral($schema); + $table = $this->platform->quoteStringLiteral($table); + } else { + $schema = 'SCHEMA_NAME()'; + $table = $this->platform->quoteStringLiteral($table); + } + + return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema); + } +} 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 @@ + + */ +class SQLiteSchemaManager extends AbstractSchemaManager +{ + /** + * {@inheritDoc} + */ + protected function fetchForeignKeyColumnsByTable(string $databaseName): array + { + $columnsByTable = parent::fetchForeignKeyColumnsByTable($databaseName); + + if (count($columnsByTable) > 0) { + foreach ($columnsByTable as $table => $columns) { + $columnsByTable[$table] = $this->addDetailsToTableForeignKeyColumns($table, $columns); + } + } + + return $columnsByTable; + } + + public function createForeignKey(ForeignKeyConstraint $foreignKey, string $table): void + { + $table = $this->introspectTable($table); + + $this->alterTable(new TableDiff($table, [], [], [], [], [], [], [], [], [$foreignKey], [], [])); + } + + public function dropForeignKey(string $name, string $table): void + { + $table = $this->introspectTable($table); + + $foreignKey = $table->getForeignKey($name); + + $this->alterTable(new TableDiff($table, [], [], [], [], [], [], [], [], [], [], [$foreignKey])); + } + + /** + * {@inheritDoc} + */ + public function listTableForeignKeys(string $table): array + { + $table = $this->normalizeName($table); + + $columns = $this->selectForeignKeyColumns('main', $table) + ->fetchAllAssociative(); + + if (count($columns) > 0) { + $columns = $this->addDetailsToTableForeignKeyColumns($table, $columns); + } + + return $this->_getPortableTableForeignKeysList($columns); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition(array $table): string + { + return $table['table_name']; + } + + /** + * {@inheritDoc} + * + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + */ + protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array + { + $indexBuffer = []; + + // fetch primary + $indexArray = $this->connection->fetchAllAssociative('SELECT * FROM PRAGMA_TABLE_INFO (?)', [$tableName]); + + usort( + $indexArray, + /** + * @param array $a + * @param array $b + */ + static function (array $a, array $b): int { + if ($a['pk'] === $b['pk']) { + return $a['cid'] - $b['cid']; + } + + return $a['pk'] - $b['pk']; + }, + ); + + foreach ($indexArray as $indexColumnRow) { + if ($indexColumnRow['pk'] === 0 || $indexColumnRow['pk'] === '0') { + continue; + } + + $indexBuffer[] = [ + 'key_name' => 'primary', + 'primary' => true, + 'non_unique' => false, + 'column_name' => $indexColumnRow['name'], + ]; + } + + // fetch regular indexes + foreach ($tableIndexes as $tableIndex) { + // Ignore indexes with reserved names, e.g. autoindexes + if (str_starts_with($tableIndex['name'], 'sqlite_')) { + continue; + } + + $keyName = $tableIndex['name']; + $idx = []; + $idx['key_name'] = $keyName; + $idx['primary'] = false; + $idx['non_unique'] = ! $tableIndex['unique']; + + $indexArray = $this->connection->fetchAllAssociative('SELECT * FROM PRAGMA_INDEX_INFO (?)', [$keyName]); + + foreach ($indexArray as $indexColumnRow) { + $idx['column_name'] = $indexColumnRow['name']; + $indexBuffer[] = $idx; + } + } + + return parent::_getPortableTableIndexesList($indexBuffer, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnList(string $table, string $database, array $tableColumns): array + { + $list = parent::_getPortableTableColumnList($table, $database, $tableColumns); + + // find column with autoincrement + $autoincrementColumn = null; + $autoincrementCount = 0; + + foreach ($tableColumns as $tableColumn) { + if ($tableColumn['pk'] === 0 || $tableColumn['pk'] === '0') { + continue; + } + + $autoincrementCount++; + if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') { + continue; + } + + $autoincrementColumn = $tableColumn['name']; + } + + if ($autoincrementCount === 1 && $autoincrementColumn !== null) { + foreach ($list as $column) { + if ($autoincrementColumn !== $column->getName()) { + continue; + } + + $column->setAutoincrement(true); + } + } + + // inspect column collation and comments + $createSql = $this->getCreateTableSQL($table); + + foreach ($list as $columnName => $column) { + $type = $column->getType(); + + if ($type instanceof StringType || $type instanceof TextType) { + $column->setPlatformOption( + 'collation', + $this->parseColumnCollationFromSQL($columnName, $createSql) ?? 'BINARY', + ); + } + + $comment = $this->parseColumnCommentFromSQL($columnName, $createSql); + + $column->setComment($comment); + } + + return $list; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition(array $tableColumn): Column + { + preg_match('/^([^()]*)\\s*(\\(((\\d+)(,\\s*(\\d+))?)\\))?/', $tableColumn['type'], $matches); + + $dbType = trim(strtolower($matches[1])); + + $length = $precision = $unsigned = null; + $fixed = $unsigned = false; + $scale = 0; + + if (count($matches) >= 6) { + $precision = (int) $matches[4]; + $scale = (int) $matches[6]; + } elseif (count($matches) >= 4) { + $length = (int) $matches[4]; + } + + if (str_contains($dbType, ' unsigned')) { + $dbType = str_replace(' unsigned', '', $dbType); + $unsigned = true; + } + + $type = $this->platform->getDoctrineTypeMapping($dbType); + $default = $tableColumn['dflt_value']; + if ($default === 'NULL') { + $default = null; + } + + if ($default !== null) { + // SQLite returns the default value as a literal expression, so we need to parse it + if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) { + $default = str_replace("''", "'", $matches[1]); + } + } + + $notnull = (bool) $tableColumn['notnull']; + + if (! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + if ($dbType === 'char') { + $fixed = true; + } + + $options = [ + 'length' => $length, + 'unsigned' => $unsigned, + 'fixed' => $fixed, + 'notnull' => $notnull, + 'default' => $default, + 'precision' => $precision, + 'scale' => $scale, + ]; + + return new Column($tableColumn['name'], Type::getType($type), $options); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition(array $view): View + { + return new View($view['name'], $view['sql']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array + { + $list = []; + foreach ($tableForeignKeys as $value) { + $value = array_change_key_case($value, CASE_LOWER); + $id = $value['id']; + if (! isset($list[$id])) { + if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') { + $value['on_delete'] = null; + } + + if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') { + $value['on_update'] = null; + } + + $list[$id] = [ + 'name' => $value['constraint_name'], + 'local' => [], + 'foreign' => [], + 'foreignTable' => $value['table'], + 'onDelete' => $value['on_delete'], + 'onUpdate' => $value['on_update'], + 'deferrable' => $value['deferrable'], + 'deferred' => $value['deferred'], + ]; + } + + $list[$id]['local'][] = $value['from']; + + if ($value['to'] === null) { + // Inferring a shorthand form for the foreign key constraint, where the "to" field is empty. + // @see https://www.sqlite.org/foreignkeys.html#fk_indexes. + $foreignTableIndexes = $this->_getPortableTableIndexesList([], $value['table']); + + if (! isset($foreignTableIndexes['primary'])) { + continue; + } + + $list[$id]['foreign'] = [...$list[$id]['foreign'], ...$foreignTableIndexes['primary']->getColumns()]; + + continue; + } + + $list[$id]['foreign'][] = $value['to']; + } + + return parent::_getPortableTableForeignKeysList($list); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint + { + return new ForeignKeyConstraint( + $tableForeignKey['local'], + $tableForeignKey['foreignTable'], + $tableForeignKey['foreign'], + $tableForeignKey['name'], + [ + 'onDelete' => $tableForeignKey['onDelete'], + 'onUpdate' => $tableForeignKey['onUpdate'], + 'deferrable' => $tableForeignKey['deferrable'], + 'deferred' => $tableForeignKey['deferred'], + ], + ); + } + + private function parseColumnCollationFromSQL(string $column, string $sql): ?string + { + $pattern = '{(?:\W' . preg_quote($column) . '\W|\W' + . preg_quote($this->platform->quoteSingleIdentifier($column)) + . '\W)[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + return $match[1]; + } + + private function parseTableCommentFromSQL(string $table, string $sql): ?string + { + $pattern = '/\s* # Allow whitespace characters at start of line +CREATE\sTABLE # Match "CREATE TABLE" +(?:\W"' . preg_quote($this->platform->quoteSingleIdentifier($table), '/') . '"\W|\W' . preg_quote($table, '/') + . '\W) # Match table name (quoted and unquoted) +( # Start capture + (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s) +)/ix'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + + return $comment === '' ? null : $comment; + } + + private function parseColumnCommentFromSQL(string $column, string $sql): string + { + $pattern = '{[\s(,](?:\W' . preg_quote($this->platform->quoteSingleIdentifier($column)) + . '\W|\W' . preg_quote($column) . '\W)(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return ''; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + assert(is_string($comment)); + + return $comment; + } + + /** @throws Exception */ + private function getCreateTableSQL(string $table): string + { + $sql = $this->connection->fetchOne( + <<<'SQL' +SELECT sql + FROM ( + SELECT * + FROM sqlite_master + UNION ALL + SELECT * + FROM sqlite_temp_master + ) +WHERE type = 'table' +AND name = ? +SQL + , + [$table], + ); + + if ($sql !== false) { + return $sql; + } + + return ''; + } + + /** + * @param list> $columns + * + * @return list> + * + * @throws Exception + */ + private function addDetailsToTableForeignKeyColumns(string $table, array $columns): array + { + $foreignKeyDetails = $this->getForeignKeyDetails($table); + $foreignKeyCount = count($foreignKeyDetails); + + foreach ($columns as $i => $column) { + // SQLite identifies foreign keys in reverse order of appearance in SQL + $columns[$i] = array_merge($column, $foreignKeyDetails[$foreignKeyCount - $column['id'] - 1]); + } + + return $columns; + } + + /** + * @return list> + * + * @throws Exception + */ + private function getForeignKeyDetails(string $table): array + { + $createSql = $this->getCreateTableSQL($table); + + if ( + preg_match_all( + '# + (?:CONSTRAINT\s+(\S+)\s+)? + (?:FOREIGN\s+KEY[^)]+\)\s*)? + REFERENCES\s+\S+\s*(?:\([^)]+\))? + (?: + [^,]*? + (NOT\s+DEFERRABLE|DEFERRABLE) + (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))? + )?#isx', + $createSql, + $match, + ) === 0 + ) { + return []; + } + + $names = $match[1]; + $deferrable = $match[2]; + $deferred = $match[3]; + $details = []; + + for ($i = 0, $count = count($match[0]); $i < $count; $i++) { + $details[] = [ + 'constraint_name' => $names[$i] ?? '', + 'deferrable' => isset($deferrable[$i]) && strcasecmp($deferrable[$i], 'deferrable') === 0, + 'deferred' => isset($deferred[$i]) && strcasecmp($deferred[$i], 'deferred') === 0, + ]; + } + + return $details; + } + + public function createComparator(): Comparator + { + return new SQLite\Comparator($this->platform); + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT name AS table_name +FROM sqlite_master +WHERE type = 'table' + AND name != 'sqlite_sequence' + AND name != 'geometry_columns' + AND name != 'spatial_ref_sys' +UNION ALL +SELECT name +FROM sqlite_temp_master +WHERE type = 'table' +ORDER BY name +SQL; + + return $this->connection->executeQuery($sql); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = <<<'SQL' + SELECT t.name AS table_name, + c.* + FROM sqlite_master t + JOIN pragma_table_info(t.name) c +SQL; + + $conditions = [ + "t.type = 'table'", + "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", + ]; + $params = []; + + if ($tableName !== null) { + $conditions[] = 't.name = ?'; + $params[] = str_replace('.', '__', $tableName); + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, c.cid'; + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = <<<'SQL' + SELECT t.name AS table_name, + i.* + FROM sqlite_master t + JOIN pragma_index_list(t.name) i +SQL; + + $conditions = [ + "t.type = 'table'", + "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", + ]; + $params = []; + + if ($tableName !== null) { + $conditions[] = 't.name = ?'; + $params[] = str_replace('.', '__', $tableName); + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, i.seq'; + + return $this->connection->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = <<<'SQL' + SELECT t.name AS table_name, + p.* + FROM sqlite_master t + JOIN pragma_foreign_key_list(t.name) p + ON p."seq" != '-1' +SQL; + + $conditions = [ + "t.type = 'table'", + "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", + ]; + $params = []; + + if ($tableName !== null) { + $conditions[] = 't.name = ?'; + $params[] = str_replace('.', '__', $tableName); + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, p.id DESC, p.seq'; + + return $this->connection->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + if ($tableName === null) { + $tables = $this->listTableNames(); + } else { + $tables = [$tableName]; + } + + $tableOptions = []; + foreach ($tables as $table) { + $comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table)); + + if ($comment === null) { + continue; + } + + $tableOptions[$table]['comment'] = $comment; + } + + return $tableOptions; + } +} 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 @@ + + */ + private array $namespaces = []; + + /** @var array */ + protected array $_tables = []; + + /** @var array */ + protected array $_sequences = []; + + protected SchemaConfig $_schemaConfig; + + /** + * @param array
$tables + * @param array $sequences + * @param array $namespaces + */ + public function __construct( + array $tables = [], + array $sequences = [], + ?SchemaConfig $schemaConfig = null, + array $namespaces = [], + ) { + $schemaConfig ??= new SchemaConfig(); + + $this->_schemaConfig = $schemaConfig; + + $name = $schemaConfig->getName(); + + if ($name !== null) { + $this->_setName($name); + } + + foreach ($namespaces as $namespace) { + $this->createNamespace($namespace); + } + + foreach ($tables as $table) { + $this->_addTable($table); + } + + foreach ($sequences as $sequence) { + $this->_addSequence($sequence); + } + } + + protected function _addTable(Table $table): void + { + $namespaceName = $table->getNamespaceName(); + $tableName = $this->normalizeName($table); + + if (isset($this->_tables[$tableName])) { + throw TableAlreadyExists::new($tableName); + } + + if ( + $namespaceName !== null + && ! $table->isInDefaultNamespace($this->getName()) + && ! $this->hasNamespace($namespaceName) + ) { + $this->createNamespace($namespaceName); + } + + $this->_tables[$tableName] = $table; + $table->setSchemaConfig($this->_schemaConfig); + } + + protected function _addSequence(Sequence $sequence): void + { + $namespaceName = $sequence->getNamespaceName(); + $seqName = $this->normalizeName($sequence); + + if (isset($this->_sequences[$seqName])) { + throw SequenceAlreadyExists::new($seqName); + } + + if ( + $namespaceName !== null + && ! $sequence->isInDefaultNamespace($this->getName()) + && ! $this->hasNamespace($namespaceName) + ) { + $this->createNamespace($namespaceName); + } + + $this->_sequences[$seqName] = $sequence; + } + + /** + * Returns the namespaces of this schema. + * + * @return list A list of namespace names. + */ + public function getNamespaces(): array + { + return array_values($this->namespaces); + } + + /** + * Gets all tables of this schema. + * + * @return list
+ */ + public function getTables(): array + { + return array_values($this->_tables); + } + + public function getTable(string $name): Table + { + $name = $this->getFullQualifiedAssetName($name); + if (! isset($this->_tables[$name])) { + throw TableDoesNotExist::new($name); + } + + return $this->_tables[$name]; + } + + private function getFullQualifiedAssetName(string $name): string + { + $name = $this->getUnquotedAssetName($name); + + if (! str_contains($name, '.')) { + $name = $this->getName() . '.' . $name; + } + + return strtolower($name); + } + + /** + * The normalized name is qualified and lower-cased. Lower-casing is + * actually wrong, but we have to do it to keep our sanity. If you are + * using database objects that only differentiate in the casing (FOO vs + * Foo) then you will NOT be able to use Doctrine Schema abstraction. + * + * Every non-namespaced element is prefixed with this schema name. + */ + private function normalizeName(AbstractAsset $asset): string + { + $name = $asset->getName(); + + if ($asset->getNamespaceName() === null) { + $name = $this->getName() . '.' . $name; + } + + return strtolower($name); + } + + /** + * Returns the unquoted representation of a given asset name. + */ + private function getUnquotedAssetName(string $assetName): string + { + if ($this->isIdentifierQuoted($assetName)) { + return $this->trimQuotes($assetName); + } + + return $assetName; + } + + /** + * Does this schema have a namespace with the given name? + */ + public function hasNamespace(string $name): bool + { + $name = strtolower($this->getUnquotedAssetName($name)); + + return isset($this->namespaces[$name]); + } + + /** + * Does this schema have a table with the given name? + */ + public function hasTable(string $name): bool + { + $name = $this->getFullQualifiedAssetName($name); + + return isset($this->_tables[$name]); + } + + public function hasSequence(string $name): bool + { + $name = $this->getFullQualifiedAssetName($name); + + return isset($this->_sequences[$name]); + } + + public function getSequence(string $name): Sequence + { + $name = $this->getFullQualifiedAssetName($name); + if (! $this->hasSequence($name)) { + throw SequenceDoesNotExist::new($name); + } + + return $this->_sequences[$name]; + } + + /** @return list */ + public function getSequences(): array + { + return array_values($this->_sequences); + } + + /** + * Creates a new namespace. + * + * @return $this + */ + public function createNamespace(string $name): self + { + $unquotedName = strtolower($this->getUnquotedAssetName($name)); + + if (isset($this->namespaces[$unquotedName])) { + throw NamespaceAlreadyExists::new($unquotedName); + } + + $this->namespaces[$unquotedName] = $name; + + return $this; + } + + /** + * Creates a new table. + */ + public function createTable(string $name): Table + { + $table = new Table($name); + $this->_addTable($table); + + foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) { + $table->addOption($option, $value); + } + + return $table; + } + + /** + * Renames a table. + * + * @return $this + */ + public function renameTable(string $oldName, string $newName): self + { + $table = $this->getTable($oldName); + $table->_setName($newName); + + $this->dropTable($oldName); + $this->_addTable($table); + + return $this; + } + + /** + * Drops a table from the schema. + * + * @return $this + */ + public function dropTable(string $name): self + { + $name = $this->getFullQualifiedAssetName($name); + $this->getTable($name); + unset($this->_tables[$name]); + + return $this; + } + + /** + * Creates a new sequence. + */ + public function createSequence(string $name, int $allocationSize = 1, int $initialValue = 1): Sequence + { + $seq = new Sequence($name, $allocationSize, $initialValue); + $this->_addSequence($seq); + + return $seq; + } + + /** @return $this */ + public function dropSequence(string $name): self + { + $name = $this->getFullQualifiedAssetName($name); + unset($this->_sequences[$name]); + + return $this; + } + + /** + * Returns an array of necessary SQL queries to create the schema on the given platform. + * + * @return list + * + * @throws Exception + */ + public function toSql(AbstractPlatform $platform): array + { + $builder = new CreateSchemaObjectsSQLBuilder($platform); + + return $builder->buildSQL($this); + } + + /** + * Return an array of necessary SQL queries to drop the schema on the given platform. + * + * @return list + */ + public function toDropSql(AbstractPlatform $platform): array + { + $builder = new DropSchemaObjectsSQLBuilder($platform); + + return $builder->buildSQL($this); + } + + /** + * Cloning a Schema triggers a deep clone of all related assets. + */ + public function __clone() + { + foreach ($this->_tables as $k => $table) { + $this->_tables[$k] = clone $table; + } + + foreach ($this->_sequences as $k => $sequence) { + $this->_sequences[$k] = clone $sequence; + } + } +} 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 @@ + */ + protected array $defaultTableOptions = []; + + public function setMaxIdentifierLength(int $length): void + { + $this->maxIdentifierLength = $length; + } + + public function getMaxIdentifierLength(): int + { + return $this->maxIdentifierLength; + } + + /** + * Gets the default namespace of schema objects. + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Sets the default namespace name of schema objects. + */ + public function setName(?string $name): void + { + $this->name = $name; + } + + /** + * Gets the default options that are passed to Table instances created with + * Schema#createTable(). + * + * @return array + */ + public function getDefaultTableOptions(): array + { + return $this->defaultTableOptions; + } + + /** @param array $defaultTableOptions */ + public function setDefaultTableOptions(array $defaultTableOptions): void + { + $this->defaultTableOptions = $defaultTableOptions; + } +} 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 @@ + */ + private readonly array $alteredTables; + + /** + * Constructs an SchemaDiff object. + * + * @internal The diff can be only instantiated by a {@see Comparator}. + * + * @param array $createdSchemas + * @param array $droppedSchemas + * @param array
$createdTables + * @param array $alteredTables + * @param array
$droppedTables + * @param array $createdSequences + * @param array $alteredSequences + * @param array $droppedSequences + */ + public function __construct( + private readonly array $createdSchemas, + private readonly array $droppedSchemas, + private readonly array $createdTables, + array $alteredTables, + private readonly array $droppedTables, + private readonly array $createdSequences, + private readonly array $alteredSequences, + private readonly array $droppedSequences, + ) { + $this->alteredTables = array_filter($alteredTables, static function (TableDiff $diff): bool { + return ! $diff->isEmpty(); + }); + } + + /** @return array */ + public function getCreatedSchemas(): array + { + return $this->createdSchemas; + } + + /** @return array */ + public function getDroppedSchemas(): array + { + return $this->droppedSchemas; + } + + /** @return array
*/ + public function getCreatedTables(): array + { + return $this->createdTables; + } + + /** @return array */ + public function getAlteredTables(): array + { + return $this->alteredTables; + } + + /** @return array
*/ + public function getDroppedTables(): array + { + return $this->droppedTables; + } + + /** @return array */ + public function getCreatedSequences(): array + { + return $this->createdSequences; + } + + /** @return array */ + public function getAlteredSequences(): array + { + return $this->alteredSequences; + } + + /** @return array */ + public function getDroppedSequences(): array + { + return $this->droppedSequences; + } + + /** + * Returns whether the diff is empty (contains no changes). + */ + public function isEmpty(): bool + { + return count($this->createdSchemas) === 0 + && count($this->droppedSchemas) === 0 + && count($this->createdTables) === 0 + && count($this->alteredTables) === 0 + && count($this->droppedTables) === 0 + && count($this->createdSequences) === 0 + && count($this->alteredSequences) === 0 + && count($this->droppedSequences) === 0; + } +} 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 @@ +_setName($name); + $this->setAllocationSize($allocationSize); + $this->setInitialValue($initialValue); + } + + public function getAllocationSize(): int + { + return $this->allocationSize; + } + + public function getInitialValue(): int + { + return $this->initialValue; + } + + public function getCache(): ?int + { + return $this->cache; + } + + public function setAllocationSize(int $allocationSize): self + { + $this->allocationSize = $allocationSize; + + return $this; + } + + public function setInitialValue(int $initialValue): self + { + $this->initialValue = $initialValue; + + return $this; + } + + public function setCache(int $cache): self + { + $this->cache = $cache; + + return $this; + } + + /** + * Checks if this sequence is an autoincrement sequence for a given table. + * + * This is used inside the comparator to not report sequences as missing, + * when the "from" schema implicitly creates the sequences. + */ + public function isAutoIncrementsFor(Table $table): bool + { + $primaryKey = $table->getPrimaryKey(); + + if ($primaryKey === null) { + return false; + } + + $pkColumns = $primaryKey->getColumns(); + + if (count($pkColumns) !== 1) { + return false; + } + + $column = $table->getColumn($pkColumns[0]); + + if (! $column->getAutoincrement()) { + return false; + } + + $sequenceName = $this->getShortestName($table->getNamespaceName()); + $tableName = $table->getShortestName($table->getNamespaceName()); + $tableSequenceName = sprintf('%s_%s_seq', $tableName, $column->getShortestName($table->getNamespaceName())); + + return $tableSequenceName === $sequenceName; + } +} 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 @@ + [], + ]; + + protected ?SchemaConfig $_schemaConfig = null; + + /** + * @param array $columns + * @param array $indexes + * @param array $uniqueConstraints + * @param array $fkConstraints + * @param array $options + */ + public function __construct( + string $name, + array $columns = [], + array $indexes = [], + array $uniqueConstraints = [], + array $fkConstraints = [], + array $options = [], + ) { + if ($name === '') { + throw InvalidTableName::new($name); + } + + $this->_setName($name); + + foreach ($columns as $column) { + $this->_addColumn($column); + } + + foreach ($indexes as $idx) { + $this->_addIndex($idx); + } + + foreach ($uniqueConstraints as $uniqueConstraint) { + $this->_addUniqueConstraint($uniqueConstraint); + } + + foreach ($fkConstraints as $fkConstraint) { + $this->_addForeignKeyConstraint($fkConstraint); + } + + $this->_options = array_merge($this->_options, $options); + } + + public function setSchemaConfig(SchemaConfig $schemaConfig): void + { + $this->_schemaConfig = $schemaConfig; + } + + /** + * Sets the Primary Key. + * + * @param array $columnNames + */ + public function setPrimaryKey(array $columnNames, ?string $indexName = null): self + { + if ($indexName === null) { + $indexName = 'primary'; + } + + $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true)); + + foreach ($columnNames as $columnName) { + $column = $this->getColumn($columnName); + $column->setNotnull(true); + } + + return $this; + } + + /** + * @param array $columnNames + * @param array $flags + * @param array $options + */ + public function addUniqueConstraint( + array $columnNames, + ?string $indexName = null, + array $flags = [], + array $options = [], + ): self { + $indexName ??= $this->_generateIdentifierName( + array_merge([$this->getName()], $columnNames), + 'uniq', + $this->_getMaxIdentifierLength(), + ); + + return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options)); + } + + /** + * @param array $columnNames + * @param array $flags + * @param array $options + */ + public function addIndex( + array $columnNames, + ?string $indexName = null, + array $flags = [], + array $options = [], + ): self { + $indexName ??= $this->_generateIdentifierName( + array_merge([$this->getName()], $columnNames), + 'idx', + $this->_getMaxIdentifierLength(), + ); + + return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options)); + } + + /** + * Drops the primary key from this table. + */ + public function dropPrimaryKey(): void + { + if ($this->_primaryKeyName === null) { + return; + } + + $this->dropIndex($this->_primaryKeyName); + $this->_primaryKeyName = null; + } + + /** + * Drops an index from this table. + */ + public function dropIndex(string $name): void + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasIndex($name)) { + throw IndexDoesNotExist::new($name, $this->_name); + } + + unset($this->_indexes[$name]); + } + + /** + * @param array $columnNames + * @param array $options + */ + public function addUniqueIndex(array $columnNames, ?string $indexName = null, array $options = []): self + { + $indexName ??= $this->_generateIdentifierName( + array_merge([$this->getName()], $columnNames), + 'uniq', + $this->_getMaxIdentifierLength(), + ); + + return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options)); + } + + /** + * Renames an index. + * + * @param string $oldName The name of the index to rename from. + * @param string|null $newName The name of the index to rename to. If null is given, the index name + * will be auto-generated. + */ + public function renameIndex(string $oldName, ?string $newName = null): self + { + $oldName = $this->normalizeIdentifier($oldName); + $normalizedNewName = $this->normalizeIdentifier($newName); + + if ($oldName === $normalizedNewName) { + return $this; + } + + if (! $this->hasIndex($oldName)) { + throw IndexDoesNotExist::new($oldName, $this->_name); + } + + if ($this->hasIndex($normalizedNewName)) { + throw IndexAlreadyExists::new($normalizedNewName, $this->_name); + } + + $oldIndex = $this->_indexes[$oldName]; + + if ($oldIndex->isPrimary()) { + $this->dropPrimaryKey(); + + return $this->setPrimaryKey($oldIndex->getColumns(), $newName ?? null); + } + + unset($this->_indexes[$oldName]); + + if ($oldIndex->isUnique()) { + return $this->addUniqueIndex($oldIndex->getColumns(), $newName, $oldIndex->getOptions()); + } + + return $this->addIndex($oldIndex->getColumns(), $newName, $oldIndex->getFlags(), $oldIndex->getOptions()); + } + + /** + * Checks if an index begins in the order of the given columns. + * + * @param array $columnNames + */ + public function columnsAreIndexed(array $columnNames): bool + { + foreach ($this->getIndexes() as $index) { + if ($index->spansColumns($columnNames)) { + return true; + } + } + + return false; + } + + /** @param array $options */ + public function addColumn(string $name, string $typeName, array $options = []): Column + { + $column = new Column($name, Type::getType($typeName), $options); + + $this->_addColumn($column); + + return $column; + } + + /** @param array $options */ + public function modifyColumn(string $name, array $options): self + { + $column = $this->getColumn($name); + $column->setOptions($options); + + return $this; + } + + /** + * Drops a Column from the Table. + */ + public function dropColumn(string $name): self + { + $name = $this->normalizeIdentifier($name); + + unset($this->_columns[$name]); + + return $this; + } + + /** + * Adds a foreign key constraint. + * + * Name is inferred from the local columns. + * + * @param array $localColumnNames + * @param array $foreignColumnNames + * @param array $options + */ + public function addForeignKeyConstraint( + string $foreignTableName, + array $localColumnNames, + array $foreignColumnNames, + array $options = [], + ?string $name = null, + ): self { + $name ??= $this->_generateIdentifierName( + array_merge([$this->getName()], $localColumnNames), + 'fk', + $this->_getMaxIdentifierLength(), + ); + + foreach ($localColumnNames as $columnName) { + if (! $this->hasColumn($columnName)) { + throw ColumnDoesNotExist::new($columnName, $this->_name); + } + } + + $constraint = new ForeignKeyConstraint( + $localColumnNames, + $foreignTableName, + $foreignColumnNames, + $name, + $options, + ); + + return $this->_addForeignKeyConstraint($constraint); + } + + public function addOption(string $name, mixed $value): self + { + $this->_options[$name] = $value; + + return $this; + } + + /** + * Returns whether this table has a foreign key constraint with the given name. + */ + public function hasForeignKey(string $name): bool + { + $name = $this->normalizeIdentifier($name); + + return isset($this->_fkConstraints[$name]); + } + + /** + * Returns the foreign key constraint with the given name. + */ + public function getForeignKey(string $name): ForeignKeyConstraint + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasForeignKey($name)) { + throw ForeignKeyDoesNotExist::new($name, $this->_name); + } + + return $this->_fkConstraints[$name]; + } + + /** + * Removes the foreign key constraint with the given name. + */ + public function removeForeignKey(string $name): void + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasForeignKey($name)) { + throw ForeignKeyDoesNotExist::new($name, $this->_name); + } + + unset($this->_fkConstraints[$name]); + } + + /** + * Returns whether this table has a unique constraint with the given name. + */ + public function hasUniqueConstraint(string $name): bool + { + $name = $this->normalizeIdentifier($name); + + return isset($this->uniqueConstraints[$name]); + } + + /** + * Returns the unique constraint with the given name. + */ + public function getUniqueConstraint(string $name): UniqueConstraint + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasUniqueConstraint($name)) { + throw UniqueConstraintDoesNotExist::new($name, $this->_name); + } + + return $this->uniqueConstraints[$name]; + } + + /** + * Removes the unique constraint with the given name. + */ + public function removeUniqueConstraint(string $name): void + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasUniqueConstraint($name)) { + throw UniqueConstraintDoesNotExist::new($name, $this->_name); + } + + unset($this->uniqueConstraints[$name]); + } + + /** + * Returns the list of table columns. + * + * @return list + */ + public function getColumns(): array + { + return array_values($this->_columns); + } + + /** + * Returns whether this table has a Column with the given name. + */ + public function hasColumn(string $name): bool + { + $name = $this->normalizeIdentifier($name); + + return isset($this->_columns[$name]); + } + + /** + * Returns the Column with the given name. + */ + public function getColumn(string $name): Column + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasColumn($name)) { + throw ColumnDoesNotExist::new($name, $this->_name); + } + + return $this->_columns[$name]; + } + + /** + * Returns the primary key. + */ + public function getPrimaryKey(): ?Index + { + if ($this->_primaryKeyName !== null) { + return $this->getIndex($this->_primaryKeyName); + } + + return null; + } + + /** + * Returns whether this table has an Index with the given name. + */ + public function hasIndex(string $name): bool + { + $name = $this->normalizeIdentifier($name); + + return isset($this->_indexes[$name]); + } + + /** + * Returns the Index with the given name. + */ + public function getIndex(string $name): Index + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasIndex($name)) { + throw IndexDoesNotExist::new($name, $this->_name); + } + + return $this->_indexes[$name]; + } + + /** @return array */ + public function getIndexes(): array + { + return $this->_indexes; + } + + /** + * Returns the unique constraints. + * + * @return array + */ + public function getUniqueConstraints(): array + { + return $this->uniqueConstraints; + } + + /** + * Returns the foreign key constraints. + * + * @return array + */ + public function getForeignKeys(): array + { + return $this->_fkConstraints; + } + + public function hasOption(string $name): bool + { + return isset($this->_options[$name]); + } + + public function getOption(string $name): mixed + { + return $this->_options[$name] ?? null; + } + + /** @return array */ + public function getOptions(): array + { + return $this->_options; + } + + /** + * Clone of a Table triggers a deep clone of all affected assets. + */ + public function __clone() + { + foreach ($this->_columns as $k => $column) { + $this->_columns[$k] = clone $column; + } + + foreach ($this->_indexes as $k => $index) { + $this->_indexes[$k] = clone $index; + } + + foreach ($this->_fkConstraints as $k => $fk) { + $this->_fkConstraints[$k] = clone $fk; + } + } + + protected function _getMaxIdentifierLength(): int + { + return $this->_schemaConfig instanceof SchemaConfig + ? $this->_schemaConfig->getMaxIdentifierLength() + : 63; + } + + protected function _addColumn(Column $column): void + { + $columnName = $column->getName(); + $columnName = $this->normalizeIdentifier($columnName); + + if (isset($this->_columns[$columnName])) { + throw ColumnAlreadyExists::new($this->getName(), $columnName); + } + + $this->_columns[$columnName] = $column; + } + + /** + * Adds an index to the table. + */ + protected function _addIndex(Index $indexCandidate): self + { + $indexName = $indexCandidate->getName(); + $indexName = $this->normalizeIdentifier($indexName); + $replacedImplicitIndexes = []; + + foreach ($this->implicitIndexes as $name => $implicitIndex) { + if (! $implicitIndex->isFulfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) { + continue; + } + + $replacedImplicitIndexes[] = $name; + } + + if ( + (isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) || + ($this->_primaryKeyName !== null && $indexCandidate->isPrimary()) + ) { + throw IndexAlreadyExists::new($indexName, $this->_name); + } + + foreach ($replacedImplicitIndexes as $name) { + unset($this->_indexes[$name], $this->implicitIndexes[$name]); + } + + if ($indexCandidate->isPrimary()) { + $this->_primaryKeyName = $indexName; + } + + $this->_indexes[$indexName] = $indexCandidate; + + return $this; + } + + protected function _addUniqueConstraint(UniqueConstraint $constraint): self + { + $name = $constraint->getName() !== '' + ? $constraint->getName() + : $this->_generateIdentifierName( + array_merge((array) $this->getName(), $constraint->getColumns()), + 'fk', + $this->_getMaxIdentifierLength(), + ); + + $name = $this->normalizeIdentifier($name); + + $this->uniqueConstraints[$name] = $constraint; + + // If there is already an index that fulfills this requirements drop the request. In the case of __construct + // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates. + // This creates computation overhead in this case, however no duplicate indexes are ever added (column based). + $indexName = $this->_generateIdentifierName( + array_merge([$this->getName()], $constraint->getColumns()), + 'idx', + $this->_getMaxIdentifierLength(), + ); + + $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false); + + foreach ($this->_indexes as $existingIndex) { + if ($indexCandidate->isFulfilledBy($existingIndex)) { + return $this; + } + } + + $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate; + + return $this; + } + + protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint): self + { + $name = $constraint->getName() !== '' + ? $constraint->getName() + : $this->_generateIdentifierName( + array_merge((array) $this->getName(), $constraint->getLocalColumns()), + 'fk', + $this->_getMaxIdentifierLength(), + ); + + $name = $this->normalizeIdentifier($name); + + $this->_fkConstraints[$name] = $constraint; + + // add an explicit index on the foreign key columns. + // If there is already an index that fulfills this requirements drop the request. In the case of __construct + // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates. + // This creates computation overhead in this case, however no duplicate indexes are ever added (column based). + $indexName = $this->_generateIdentifierName( + array_merge([$this->getName()], $constraint->getLocalColumns()), + 'idx', + $this->_getMaxIdentifierLength(), + ); + + $indexCandidate = $this->_createIndex($constraint->getLocalColumns(), $indexName, false, false); + + foreach ($this->_indexes as $existingIndex) { + if ($indexCandidate->isFulfilledBy($existingIndex)) { + return $this; + } + } + + $this->_addIndex($indexCandidate); + $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate; + + return $this; + } + + /** + * Normalizes a given identifier. + * + * Trims quotes and lowercases the given identifier. + */ + private function normalizeIdentifier(?string $identifier): string + { + if ($identifier === null) { + return ''; + } + + return $this->trimQuotes(strtolower($identifier)); + } + + public function setComment(string $comment): self + { + // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options. + $this->addOption('comment', $comment); + + return $this; + } + + public function getComment(): ?string + { + return $this->_options['comment'] ?? null; + } + + /** + * @param array $columns + * @param array $flags + * @param array $options + */ + private function _createUniqueConstraint( + array $columns, + string $indexName, + array $flags = [], + array $options = [], + ): UniqueConstraint { + if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) { + throw IndexNameInvalid::new($indexName); + } + + foreach ($columns as $index => $value) { + if (is_string($index)) { + $columnName = $index; + } else { + $columnName = $value; + } + + if (! $this->hasColumn($columnName)) { + throw ColumnDoesNotExist::new($columnName, $this->_name); + } + } + + return new UniqueConstraint($indexName, $columns, $flags, $options); + } + + /** + * @param array $columns + * @param array $flags + * @param array $options + */ + private function _createIndex( + array $columns, + string $indexName, + bool $isUnique, + bool $isPrimary, + array $flags = [], + array $options = [], + ): Index { + if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) { + throw IndexNameInvalid::new($indexName); + } + + foreach ($columns as $columnName) { + if (! $this->hasColumn($columnName)) { + throw ColumnDoesNotExist::new($columnName, $this->_name); + } + } + + return new Index($indexName, $columns, $isUnique, $isPrimary, $flags, $options); + } +} 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 @@ + $droppedForeignKeys + * @param array $addedColumns + * @param array $modifiedColumns + * @param array $droppedColumns + * @param array $renamedColumns + * @param array $addedIndexes + * @param array $modifiedIndexes + * @param array $droppedIndexes + * @param array $renamedIndexes + * @param array $addedForeignKeys + * @param array $modifiedForeignKeys + */ + public function __construct( + private readonly Table $oldTable, + private readonly array $addedColumns, + private readonly array $modifiedColumns, + private readonly array $droppedColumns, + private readonly array $renamedColumns, + private array $addedIndexes, + private readonly array $modifiedIndexes, + private array $droppedIndexes, + private readonly array $renamedIndexes, + private readonly array $addedForeignKeys, + private readonly array $modifiedForeignKeys, + private readonly array $droppedForeignKeys, + ) { + } + + public function getOldTable(): Table + { + return $this->oldTable; + } + + /** @return array */ + public function getAddedColumns(): array + { + return $this->addedColumns; + } + + /** @return array */ + public function getModifiedColumns(): array + { + return $this->modifiedColumns; + } + + /** @return array */ + public function getDroppedColumns(): array + { + return $this->droppedColumns; + } + + /** @return array */ + public function getRenamedColumns(): array + { + return $this->renamedColumns; + } + + /** @return array */ + public function getAddedIndexes(): array + { + return $this->addedIndexes; + } + + /** + * @internal This method exists only for compatibility with the current implementation of schema managers + * that modify the diff while processing it. + */ + public function unsetAddedIndex(Index $index): void + { + $this->addedIndexes = array_filter( + $this->addedIndexes, + static function (Index $addedIndex) use ($index): bool { + return $addedIndex !== $index; + }, + ); + } + + /** @return array */ + public function getModifiedIndexes(): array + { + return $this->modifiedIndexes; + } + + /** @return array */ + public function getDroppedIndexes(): array + { + return $this->droppedIndexes; + } + + /** + * @internal This method exists only for compatibility with the current implementation of schema managers + * that modify the diff while processing it. + */ + public function unsetDroppedIndex(Index $index): void + { + $this->droppedIndexes = array_filter( + $this->droppedIndexes, + static function (Index $droppedIndex) use ($index): bool { + return $droppedIndex !== $index; + }, + ); + } + + /** @return array */ + public function getRenamedIndexes(): array + { + return $this->renamedIndexes; + } + + /** @return array */ + public function getAddedForeignKeys(): array + { + return $this->addedForeignKeys; + } + + /** @return array */ + public function getModifiedForeignKeys(): array + { + return $this->modifiedForeignKeys; + } + + /** @return array */ + public function getDroppedForeignKeys(): array + { + return $this->droppedForeignKeys; + } + + /** + * Returns whether the diff is empty (contains no changes). + */ + public function isEmpty(): bool + { + return count($this->addedColumns) === 0 + && count($this->modifiedColumns) === 0 + && count($this->droppedColumns) === 0 + && count($this->renamedColumns) === 0 + && count($this->addedIndexes) === 0 + && count($this->modifiedIndexes) === 0 + && count($this->droppedIndexes) === 0 + && count($this->renamedIndexes) === 0 + && count($this->addedForeignKeys) === 0 + && count($this->modifiedForeignKeys) === 0 + && count($this->droppedForeignKeys) === 0; + } +} 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 @@ + + */ + protected array $columns = []; + + /** + * Platform specific flags + * + * @var array + */ + protected array $flags = []; + + /** + * @param array $columns + * @param array $flags + * @param array $options + */ + public function __construct( + string $name, + array $columns, + array $flags = [], + private readonly array $options = [], + ) { + $this->_setName($name); + + foreach ($columns as $column) { + $this->addColumn($column); + } + + foreach ($flags as $flag) { + $this->addFlag($flag); + } + } + + /** + * Returns the names of the referencing table columns the constraint is associated with. + * + * @return list + */ + public function getColumns(): array + { + return array_keys($this->columns); + } + + /** + * Returns the quoted representation of the column names the constraint is associated with. + * + * But only if they were defined with one or a column name + * is a keyword reserved by the platform. + * Otherwise, the plain unquoted value as inserted is returned. + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return list + */ + public function getQuotedColumns(AbstractPlatform $platform): array + { + $columns = []; + + foreach ($this->columns as $column) { + $columns[] = $column->getQuotedName($platform); + } + + return $columns; + } + + /** @return array */ + public function getUnquotedColumns(): array + { + return array_map($this->trimQuotes(...), $this->getColumns()); + } + + /** + * Returns platform specific flags for unique constraint. + * + * @return array + */ + public function getFlags(): array + { + return array_keys($this->flags); + } + + /** + * Adds flag for a unique constraint that translates to platform specific handling. + * + * @return $this + * + * @example $uniqueConstraint->addFlag('CLUSTERED') + */ + public function addFlag(string $flag): self + { + $this->flags[strtolower($flag)] = true; + + return $this; + } + + /** + * Does this unique constraint have a specific flag? + */ + public function hasFlag(string $flag): bool + { + return isset($this->flags[strtolower($flag)]); + } + + /** + * Removes a flag. + */ + public function removeFlag(string $flag): void + { + unset($this->flags[strtolower($flag)]); + } + + public function hasOption(string $name): bool + { + return isset($this->options[strtolower($name)]); + } + + public function getOption(string $name): mixed + { + return $this->options[strtolower($name)]; + } + + /** @return array */ + public function getOptions(): array + { + return $this->options; + } + + protected function addColumn(string $column): void + { + $this->columns[$column] = new Identifier($column); + } +} 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 @@ +_setName($name); + } + + public function getSql(): string + { + return $this->sql; + } +} -- cgit v1.2.3