From 194c6eb7b78173fb8ae0ece8bc85be80d0364d89 Mon Sep 17 00:00:00 2001 From: Sergei Morozov Date: Sat, 15 Jan 2022 19:07:36 -0800 Subject: [PATCH] Optimize schema managers' ::listTables() methods Co-authored-by: mondrake --- UPGRADE.md | 11 ++ psalm.xml.dist | 26 +++ src/Exception/DatabaseRequired.php | 20 ++ src/Platforms/AbstractMySQLPlatform.php | 11 ++ src/Platforms/AbstractPlatform.php | 8 + src/Platforms/DB2Platform.php | 9 + src/Platforms/OraclePlatform.php | 9 + src/Platforms/PostgreSQLPlatform.php | 9 + src/Platforms/SQLServerPlatform.php | 9 + src/Platforms/SqlitePlatform.php | 6 + src/Schema/AbstractSchemaManager.php | 170 +++++++++++++++++ src/Schema/DB2SchemaManager.php | 187 ++++++++++++++++++- src/Schema/MySQLSchemaManager.php | 178 ++++++++++++++++-- src/Schema/OracleSchemaManager.php | 170 ++++++++++++++++- src/Schema/PostgreSQLSchemaManager.php | 192 ++++++++++++++++++- src/Schema/SQLServerSchemaManager.php | 236 ++++++++++++++++++++++-- src/Schema/SqliteSchemaManager.php | 139 ++++++++++++-- 17 files changed, 1305 insertions(+), 85 deletions(-) create mode 100644 src/Exception/DatabaseRequired.php diff --git a/UPGRADE.md b/UPGRADE.md index ec3e46e77cd..9820c4ca5f0 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -8,6 +8,17 @@ awareness about deprecated code. # Upgrade to 3.4 +# Deprecated `AbstractPlatform` schema introspection methods + +The following schema introspection methods have been deprecated: + +- `AbstractPlatform::getListTablesSQL()`, +- `AbstractPlatform::getListTableColumnsSQL()`, +- `AbstractPlatform::getListTableIndexesSQL()`, +- `AbstractPlatform::getListTableForeignKeysSQL()`. + +The queries used for schema introspection are an internal implementation detail of the DBAL. + # Deprecated `collate` option for MySQL This undocumented option is deprecated in favor of `collation`. diff --git a/psalm.xml.dist b/psalm.xml.dist index c3bc709d3a1..82bc4967339 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -212,6 +212,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Exception/DatabaseRequired.php b/src/Exception/DatabaseRequired.php new file mode 100644 index 00000000000..d87ad3e655d --- /dev/null +++ b/src/Exception/DatabaseRequired.php @@ -0,0 +1,20 @@ +fromColumn !== null ? $this->getColumnComment($columnDiff->fromColumn) : null; } + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + */ public function getListTableMetadataSQL(string $table, ?string $schema = null): string { if ($schema !== null) { diff --git a/src/Platforms/SQLServerPlatform.php b/src/Platforms/SQLServerPlatform.php index 732cb636346..7a27a61f458 100644 --- a/src/Platforms/SQLServerPlatform.php +++ b/src/Platforms/SQLServerPlatform.php @@ -904,6 +904,8 @@ public function getListTablesSQL() } /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * * {@inheritDoc} */ public function getListTableColumnsSQL($table, $database = null) @@ -937,6 +939,8 @@ public function getListTableColumnsSQL($table, $database = null) } /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * * @param string $table * @param string|null $database * @@ -963,6 +967,8 @@ public function getListTableForeignKeysSQL($table, $database = null) } /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * * {@inheritDoc} */ public function getListTableIndexesSQL($table, $database = null) @@ -1610,6 +1616,9 @@ protected function getCommentOnTableSQL(string $tableName, ?string $comment): st ); } + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + */ public function getListTableMetadataSQL(string $table): string { return sprintf( diff --git a/src/Platforms/SqlitePlatform.php b/src/Platforms/SqlitePlatform.php index eb7d00ea57c..12b34032c93 100644 --- a/src/Platforms/SqlitePlatform.php +++ b/src/Platforms/SqlitePlatform.php @@ -472,6 +472,8 @@ public function getListTableConstraintsSQL($table) } /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * * {@inheritDoc} */ public function getListTableColumnsSQL($table, $database = null) @@ -482,6 +484,8 @@ public function getListTableColumnsSQL($table, $database = null) } /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * * {@inheritDoc} */ public function getListTableIndexesSQL($table, $database = null) @@ -876,6 +880,8 @@ public function getCreateTableSQL(Table $table, $createFlags = null) } /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * * @param string $table * @param string|null $database * diff --git a/src/Schema/AbstractSchemaManager.php b/src/Schema/AbstractSchemaManager.php index e44580b54b2..73fbef88b11 100644 --- a/src/Schema/AbstractSchemaManager.php +++ b/src/Schema/AbstractSchemaManager.php @@ -7,13 +7,16 @@ use Doctrine\DBAL\Event\SchemaIndexDefinitionEventArgs; use Doctrine\DBAL\Events; use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Exception\DatabaseRequired; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Result; use Doctrine\Deprecations\Deprecation; use Throwable; use function array_filter; use function array_intersect; use function array_map; +use function array_shift; use function array_values; use function assert; use function call_user_func_array; @@ -309,6 +312,54 @@ public function listTables() return $tables; } + /** + * @return list + * + * @throws Exception + */ + protected function doListTables(): array + { + $currentDatabase = $this->_conn->getDatabase(); + + if ($currentDatabase === null) { + throw DatabaseRequired::new(__METHOD__); + } + + /** @var array>> $columns */ + $columns = $this->fetchAllAssociativeGrouped( + $this->selectDatabaseColumns($currentDatabase) + ); + + $indexes = $this->fetchAllAssociativeGrouped( + $this->selectDatabaseIndexes($currentDatabase) + ); + + if ($this->_platform->supportsForeignKeyConstraints()) { + $foreignKeys = $this->fetchAllAssociativeGrouped( + $this->selectDatabaseForeignKeys($currentDatabase) + ); + } else { + $foreignKeys = []; + } + + $tableOptions = $this->getDatabaseTableOptions($currentDatabase); + + $tables = []; + + foreach ($columns as $tableName => $tableColumns) { + $tables[] = new Table( + $tableName, + $this->_getPortableTableColumnList($tableName, $currentDatabase, $tableColumns), + $this->_getPortableTableIndexesList($indexes[$tableName] ?? [], $tableName), + [], + $this->_getPortableTableForeignKeysList($foreignKeys[$tableName] ?? []), + $tableOptions[$tableName] ?? [] + ); + } + + return $tables; + } + /** * @param string $name * @@ -330,6 +381,109 @@ public function listTableDetails($name) return new Table($name, $columns, $indexes, [], $foreignKeys); } + /** + * @param string $name + * + * @throws Exception + */ + protected function doListTableDetails($name): Table + { + $currentDatabase = $this->_conn->getDatabase(); + + if ($currentDatabase === null) { + throw DatabaseRequired::new(__METHOD__); + } + + $normalizedName = $this->normalizeName($name); + + $tableOptions = $this->getDatabaseTableOptions($currentDatabase, $normalizedName); + + if ($this->_platform->supportsForeignKeyConstraints()) { + $foreignKeys = $this->_getPortableTableForeignKeysList( + $this->selectDatabaseForeignKeys($currentDatabase, $normalizedName) + ->fetchAllAssociative() + ); + } else { + $foreignKeys = []; + } + + return new Table( + $name, + $this->_getPortableTableColumnList( + $name, + $currentDatabase, + $this->selectDatabaseColumns($currentDatabase, $normalizedName) + ->fetchAllAssociative() + ), + $this->_getPortableTableIndexesList( + $this->selectDatabaseIndexes($currentDatabase, $normalizedName) + ->fetchAllAssociative(), + $name + ), + [], + $foreignKeys, + $tableOptions[$normalizedName] ?? [] + ); + } + + /** + * 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 + { + return $name; + } + + /** + * Selects column definitions of the tables in the specified database. If the table name is specified, narrows down + * the selection to this table. + * + * @throws Exception + * + * @abstract + */ + protected function selectDatabaseColumns(string $databaseName, ?string $tableName = null): Result + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Selects index definitions of the tables in the specified database. If the table name is specified, narrows down + * the selection to this table. + * + * @throws Exception + */ + protected function selectDatabaseIndexes(string $databaseName, ?string $tableName = null): Result + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Selects foreign key definitions of the tables in the specified database. If the table name is specified, + * narrows down the selection to this table. + * + * @throws Exception + */ + protected function selectDatabaseForeignKeys(string $databaseName, ?string $tableName = null): Result + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Returns table options for the tables in the specified database. If the table name is specified, narrows down + * the selection to this table. + * + * @return array> + * + * @throws Exception + */ + protected function getDatabaseTableOptions(string $databaseName, ?string $tableName = null): array + { + throw Exception::notSupported(__METHOD__); + } + /** * Lists the views this connection has. * @@ -1336,4 +1490,20 @@ public function createComparator(): Comparator { return new Comparator($this->getDatabasePlatform()); } + + /** + * @return array>> + * + * @throws Exception + */ + private function fetchAllAssociativeGrouped(Result $result): array + { + $data = []; + + foreach ($result->fetchAllAssociative() as $row) { + $data[array_shift($row)][] = $row; + } + + return $data; + } } diff --git a/src/Schema/DB2SchemaManager.php b/src/Schema/DB2SchemaManager.php index e06baf36e4f..79b8567fd28 100644 --- a/src/Schema/DB2SchemaManager.php +++ b/src/Schema/DB2SchemaManager.php @@ -4,14 +4,17 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\DB2Platform; +use Doctrine\DBAL\Result; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use function array_change_key_case; +use function implode; use function preg_match; use function str_replace; use function strpos; use function strtolower; +use function strtoupper; use function substr; use const CASE_LOWER; @@ -38,6 +41,22 @@ public function listTableNames() return $this->filterAssetNames($this->_getPortableTablesList($tables)); } + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + */ + public function listTableDetails($name) + { + return $this->doListTableDetails($name); + } + /** * {@inheritdoc} * @@ -229,21 +248,173 @@ protected function _getPortableViewDefinition($view) 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 selectDatabaseColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' C.TABNAME,'; + } + + $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->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseIndexes(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' IDX.TABNAME,'; + } + + $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->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseForeignKeys(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' R.TABNAME,'; + } + + $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->_conn->executeQuery($sql, $params); + } + /** - * {@inheritdoc} + * {@inheritDoc} */ - public function listTableDetails($name): Table + protected function getDatabaseTableOptions(string $databaseName, ?string $tableName = null): array { - $table = parent::listTableDetails($name); + $sql = 'SELECT NAME, REMARKS'; + + $conditions = []; + $params = []; + + if ($tableName !== null) { + $conditions[] = 'NAME = ?'; + $params[] = $tableName; + } + + $sql .= ' FROM SYSIBM.SYSTABLES'; + + if ($conditions !== []) { + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } - $sql = $this->_platform->getListTableCommentsSQL($name); + /** @var array> $metadata */ + $metadata = $this->_conn->executeQuery($sql, $params) + ->fetchAllAssociativeIndexed(); - $tableOptions = $this->_conn->fetchAssociative($sql); + $tableOptions = []; + foreach ($metadata as $table => $data) { + $data = array_change_key_case($data, CASE_LOWER); - if ($tableOptions !== false) { - $table->addOption('comment', $tableOptions['REMARKS']); + $tableOptions[$table] = ['comment' => $data['remarks']]; } - return $table; + return $tableOptions; } } diff --git a/src/Schema/MySQLSchemaManager.php b/src/Schema/MySQLSchemaManager.php index 7c175bb60bf..501add0614f 100644 --- a/src/Schema/MySQLSchemaManager.php +++ b/src/Schema/MySQLSchemaManager.php @@ -5,12 +5,14 @@ use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\MariaDb1027Platform; use Doctrine\DBAL\Platforms\MySQL; +use Doctrine\DBAL\Result; use Doctrine\DBAL\Types\Type; use function array_change_key_case; use function array_shift; use function assert; use function explode; +use function implode; use function is_string; use function preg_match; use function strpos; @@ -47,6 +49,22 @@ class MySQLSchemaManager extends AbstractSchemaManager "''" => "'", ]; + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + */ + public function listTableDetails($name) + { + return $this->doListTableDetails($name); + } + /** * {@inheritdoc} */ @@ -328,42 +346,162 @@ protected function _getPortableTableForeignKeysList($tableForeignKeys) return $result; } - /** - * {@inheritdoc} - */ - public function listTableDetails($name) + public function createComparator(): Comparator { - $table = parent::listTableDetails($name); + return new MySQL\Comparator($this->getDatabasePlatform()); + } - $sql = $this->_platform->getListTableMetadataSQL($name); + protected function selectDatabaseColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; - $tableOptions = $this->_conn->fetchAssociative($sql); + if ($tableName === null) { + $sql .= ' TABLE_NAME,'; + } - if ($tableOptions === false) { - return $table; + $sql .= <<<'SQL' + COLUMN_NAME AS field, + COLUMN_TYPE AS type, + IS_NULLABLE AS `null`, + COLUMN_KEY AS `key`, + COLUMN_DEFAULT AS `default`, + EXTRA, + COLUMN_COMMENT AS comment, + CHARACTER_SET_NAME AS characterset, + COLLATION_NAME AS collation +FROM information_schema.COLUMNS +SQL; + + $conditions = ['TABLE_SCHEMA = ?']; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'TABLE_NAME = ?'; + $params[] = $tableName; } - $table->addOption('engine', $tableOptions['ENGINE']); + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY ORDINAL_POSITION'; - if ($tableOptions['TABLE_COLLATION'] !== null) { - $table->addOption('collation', $tableOptions['TABLE_COLLATION']); + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseIndexes(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; } - $table->addOption('charset', $tableOptions['CHARACTER_SET_NAME']); + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY SEQ_IN_INDEX'; - if ($tableOptions['AUTO_INCREMENT'] !== null) { - $table->addOption('autoincrement', $tableOptions['AUTO_INCREMENT']); + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseForeignKeys(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 /*!50116, + c.UPDATE_RULE, + c.DELETE_RULE */ +FROM information_schema.key_column_usage k /*!50116 +INNER JOIN information_schema.referential_constraints c +ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME +AND c.TABLE_NAME = k.TABLE_NAME +AND c.CONSTRAINT_SCHEMA = k.TABLE_SCHEMA */ +SQL; + + $conditions = ['k.TABLE_SCHEMA = ?']; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'k.TABLE_NAME = ?'; + $params[] = $tableName; } - $table->addOption('comment', $tableOptions['TABLE_COMMENT']); - $table->addOption('create_options', $this->parseCreateOptions($tableOptions['CREATE_OPTIONS'])); + $conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL'; - return $table; + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY k.ORDINAL_POSITION'; + + return $this->_conn->executeQuery($sql, $params); } - public function createComparator(): Comparator + /** + * {@inheritDoc} + */ + protected function getDatabaseTableOptions(string $databaseName, ?string $tableName = null): array { - return new MySQL\Comparator($this->getDatabasePlatform()); + $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 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->_conn->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; } /** diff --git a/src/Schema/OracleSchemaManager.php b/src/Schema/OracleSchemaManager.php index 762492e727a..004ff4249f6 100644 --- a/src/Schema/OracleSchemaManager.php +++ b/src/Schema/OracleSchemaManager.php @@ -4,15 +4,18 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\OraclePlatform; +use Doctrine\DBAL\Result; use Doctrine\DBAL\Types\Type; use function array_change_key_case; use function array_values; +use function implode; use function is_string; use function preg_match; use function str_replace; use function strpos; use function strtolower; +use function strtoupper; use function trim; use const CASE_LOWER; @@ -24,6 +27,22 @@ */ class OracleSchemaManager extends AbstractSchemaManager { + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + */ + public function listTableDetails($name) + { + return $this->doListTableDetails($name); + } + /** * {@inheritdoc} */ @@ -318,22 +337,155 @@ private function getQuotedIdentifierName($identifier): string return $identifier; } + protected function selectDatabaseColumns(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 + 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->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseIndexes(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->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseForeignKeys(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->_conn->executeQuery($sql, $params); + } + /** - * {@inheritdoc} + * {@inheritDoc} */ - public function listTableDetails($name): Table + protected function getDatabaseTableOptions(string $databaseName, ?string $tableName = null): array { - $table = parent::listTableDetails($name); + $sql = 'SELECT TABLE_NAME, COMMENTS'; + + $conditions = ['OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; + + if ($tableName !== null) { + $conditions[] = 'TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; + } - $sql = $this->_platform->getListTableCommentsSQL($name); + $sql .= ' FROM ALL_TAB_COMMENTS WHERE ' . implode(' AND ', $conditions); - $tableOptions = $this->_conn->fetchAssociative($sql); + /** @var array> $metadata */ + $metadata = $this->_conn->executeQuery($sql, $params) + ->fetchAllAssociativeIndexed(); - if ($tableOptions !== false) { - $tableOptions = array_change_key_case($tableOptions, CASE_LOWER); - $table->addOption('comment', $tableOptions['comments']); + $tableOptions = []; + foreach ($metadata as $table => $data) { + $data = array_change_key_case($data, CASE_LOWER); + + $tableOptions[$table] = [ + 'comment' => $data['comments'], + ]; } - return $table; + return $tableOptions; + } + + protected function normalizeName(string $name): string + { + $identifier = new Identifier($name); + + return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name); } } diff --git a/src/Schema/PostgreSQLSchemaManager.php b/src/Schema/PostgreSQLSchemaManager.php index a76e57aae4d..c4eb31e003d 100644 --- a/src/Schema/PostgreSQLSchemaManager.php +++ b/src/Schema/PostgreSQLSchemaManager.php @@ -4,6 +4,7 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Result; use Doctrine\DBAL\Types\JsonType; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; @@ -39,6 +40,22 @@ class PostgreSQLSchemaManager extends AbstractSchemaManager /** @var string[]|null */ private $existingSchemaPaths; + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + */ + public function listTableDetails($name) + { + return $this->doListTableDetails($name); + } + /** * Gets all the existing schema names. * @@ -571,21 +588,180 @@ private function parseDefaultExpression(?string $default): ?string return str_replace("''", "'", $default); } + protected function selectDatabaseColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' c.relname,'; + } + + $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, + (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, pg_class c, pg_type t, pg_namespace n +SQL; + + $conditions = [ + 'a.attnum > 0', + 'a.attrelid = c.oid', + 'a.atttypid = t.oid', + 'n.oid = c.relnamespace', + "c.relkind = 'r'", + ]; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause($tableName, 'c', 'n'); + } else { + $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')"; + $conditions[] = 'n.nspname = ANY(current_schemas(false))'; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY a.attnum'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseIndexes(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' pg_index.indrelid::REGCLASS AS tablename,'; + } + + $sql .= <<<'SQL' + quote_ident(relname) AS relname, + pg_index.indisunique, + pg_index.indisprimary, + pg_index.indkey, + pg_index.indrelid, + pg_get_expr(indpred, indrelid) AS where + FROM pg_class, pg_index + WHERE oid IN ( + SELECT indexrelid + FROM pg_index si, pg_class sc, pg_namespace sn +SQL; + + $conditions = ['sc.oid=si.indrelid', 'sc.relnamespace = sn.oid']; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause($tableName, 'sc', 'sn'); + } else { + $conditions[] = "sn.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')"; + $conditions[] = 'sn.nspname = ANY(current_schemas(false))'; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ') AND pg_index.indexrelid = oid'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseForeignKeys(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' r.conrelid :: REGCLASS as tablename,'; + } + + $sql .= <<<'SQL' + quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef + FROM pg_catalog.pg_constraint r + WHERE r.conrelid IN + ( + SELECT c.oid + FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n +SQL; + + $conditions = ['n.oid = c.relnamespace']; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause($tableName); + } else { + $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')"; + $conditions[] = 'n.nspname = ANY(current_schemas(false))'; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f'"; + + return $this->_conn->executeQuery($sql, $params); + } + /** - * {@inheritdoc} + * {@inheritDoc} */ - public function listTableDetails($name): Table + protected function getDatabaseTableOptions(string $databaseName, ?string $tableName = null): array { - $table = parent::listTableDetails($name); + if ($tableName === null) { + $tables = $this->listTableNames(); + } else { + $tables = [$tableName]; + } - $sql = $this->_platform->getListTableMetadataSQL($name); + $tableOptions = []; + foreach ($tables as $table) { + $sql = 'SELECT obj_description(?::regclass) AS table_comment;'; + $comment = $this->_conn->executeQuery($sql, [$table])->fetchOne(); - $tableOptions = $this->_conn->fetchAssociative($sql); + if ($comment === null) { + continue; + } - if ($tableOptions !== false) { - $table->addOption('comment', $tableOptions['table_comment']); + $tableOptions[$table]['comment'] = $comment; } - return $table; + return $tableOptions; + } + + /** + * @param string $table + * @param string $classAlias + * @param string $namespaceAlias + */ + private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n'): string + { + $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND "; + if (strpos($table, '.') !== false) { + [$schema, $table] = explode('.', $table); + $schema = $this->_platform->quoteStringLiteral($schema); + } else { + $schema = 'ANY(current_schemas(false))'; + } + + $table = new Identifier($table); + $table = $this->_platform->quoteStringLiteral($table->getName()); + + return $whereClause . sprintf( + '%s.relname = %s AND %s.nspname = %s', + $classAlias, + $table, + $namespaceAlias, + $schema + ); } } diff --git a/src/Schema/SQLServerSchemaManager.php b/src/Schema/SQLServerSchemaManager.php index 8a8713f09b0..8188465ac48 100644 --- a/src/Schema/SQLServerSchemaManager.php +++ b/src/Schema/SQLServerSchemaManager.php @@ -5,11 +5,15 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\SQLServer; use Doctrine\DBAL\Platforms\SQLServerPlatform; +use Doctrine\DBAL\Result; use Doctrine\DBAL\Types\Type; use Doctrine\Deprecations\Deprecation; +use function array_change_key_case; use function assert; use function count; +use function explode; +use function implode; use function is_string; use function preg_match; use function sprintf; @@ -17,6 +21,8 @@ use function strpos; use function strtok; +use const CASE_LOWER; + /** * SQL Server Schema Manager. * @@ -27,6 +33,22 @@ class SQLServerSchemaManager extends AbstractSchemaManager /** @var string|null */ private $databaseCollation; + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + */ + public function listTableDetails($name) + { + return $this->doListTableDetails($name); + } + /** * {@inheritDoc} */ @@ -321,26 +343,6 @@ private function getColumnConstraints(string $table, string $column): iterable ); } - /** - * @param string $name - * - * @throws Exception - */ - public function listTableDetails($name): Table - { - $table = parent::listTableDetails($name); - - $sql = $this->_platform->getListTableMetadataSQL($name); - - $tableOptions = $this->_conn->fetchAssociative($sql); - - if ($tableOptions !== false) { - $table->addOption('comment', $tableOptions['table_comment']); - } - - return $table; - } - /** * @throws Exception */ @@ -368,4 +370,198 @@ private function getDatabaseCollation(): string return $this->databaseCollation; } + + protected function selectDatabaseColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' obj.name AS tablename,'; + } + + $sql .= <<<'SQL' + col.name, + type.name AS type, + col.max_length AS length, + ~col.is_nullable AS notnull, + def.definition AS [default], + 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; + + $conditions = ["obj.type = 'U'"]; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'obj.name'); + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseIndexes(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' tbl.name AS tablename,'; + } + + $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->_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseForeignKeys(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' OBJECT_NAME (f.parent_object_id),'; + } + + $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->_conn->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function getDatabaseTableOptions(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->_conn->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($table, $schemaColumn, $tableColumn): string + { + if (strpos($table, '.') !== false) { + [$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/src/Schema/SqliteSchemaManager.php b/src/Schema/SqliteSchemaManager.php index 3e0e5cfec68..ffa44498b06 100644 --- a/src/Schema/SqliteSchemaManager.php +++ b/src/Schema/SqliteSchemaManager.php @@ -6,6 +6,7 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\SQLite; use Doctrine\DBAL\Platforms\SqlitePlatform; +use Doctrine\DBAL\Result; use Doctrine\DBAL\Types\StringType; use Doctrine\DBAL\Types\TextType; use Doctrine\DBAL\Types\Type; @@ -17,6 +18,7 @@ use function array_reverse; use function explode; use function file_exists; +use function implode; use function preg_match; use function preg_match_all; use function preg_quote; @@ -38,6 +40,22 @@ */ class SqliteSchemaManager extends AbstractSchemaManager { + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + */ + public function listTableDetails($name) + { + return $this->doListTableDetails($name); + } + /** * {@inheritdoc} * @@ -562,26 +580,6 @@ private function getCreateTableSQL(string $table): string return ''; } - /** - * {@inheritDoc} - * - * @param string $name - */ - public function listTableDetails($name): Table - { - $table = parent::listTableDetails($name); - - $tableCreateSql = $this->getCreateTableSQL($name); - - $comment = $this->parseTableCommentFromSQL($name, $tableCreateSql); - - if ($comment !== null) { - $table->addOption('comment', $comment); - } - - return $table; - } - public function createComparator(): Comparator { return new SQLite\Comparator($this->getDatabasePlatform()); @@ -603,4 +601,105 @@ public function getSchemaSearchPaths() // SQLite does not support schemas or databases return []; } + + protected function selectDatabaseColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = <<_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseIndexes(string $databaseName, ?string $tableName = null): Result + { + $sql = <<_conn->executeQuery($sql, $params); + } + + protected function selectDatabaseForeignKeys(string $databaseName, ?string $tableName = null): Result + { + $sql = <<_conn->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function getDatabaseTableOptions(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; + } }