vendor/doctrine/dbal/src/Schema/MySQLSchemaManager.php line 83

Open in your IDE?
  1. <?php
  2. namespace Doctrine\DBAL\Schema;
  3. use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
  4. use Doctrine\DBAL\Platforms\MariaDb1027Platform;
  5. use Doctrine\DBAL\Platforms\MySQL;
  6. use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\CachingCollationMetadataProvider;
  7. use Doctrine\DBAL\Platforms\MySQL\CollationMetadataProvider\ConnectionCollationMetadataProvider;
  8. use Doctrine\DBAL\Result;
  9. use Doctrine\DBAL\Types\Type;
  10. use Doctrine\Deprecations\Deprecation;
  11. use function array_change_key_case;
  12. use function array_shift;
  13. use function assert;
  14. use function explode;
  15. use function implode;
  16. use function is_string;
  17. use function preg_match;
  18. use function strpos;
  19. use function strtok;
  20. use function strtolower;
  21. use function strtr;
  22. use const CASE_LOWER;
  23. /**
  24.  * Schema manager for the MySQL RDBMS.
  25.  *
  26.  * @extends AbstractSchemaManager<AbstractMySQLPlatform>
  27.  */
  28. class MySQLSchemaManager extends AbstractSchemaManager
  29. {
  30.     /** @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences */
  31.     private const MARIADB_ESCAPE_SEQUENCES = [
  32.         '\\0' => "\0",
  33.         "\\'" => "'",
  34.         '\\"' => '"',
  35.         '\\b' => "\b",
  36.         '\\n' => "\n",
  37.         '\\r' => "\r",
  38.         '\\t' => "\t",
  39.         '\\Z' => "\x1a",
  40.         '\\\\' => '\\',
  41.         '\\%' => '%',
  42.         '\\_' => '_',
  43.         // Internally, MariaDB escapes single quotes using the standard syntax
  44.         "''" => "'",
  45.     ];
  46.     /**
  47.      * {@inheritDoc}
  48.      */
  49.     public function listTableNames()
  50.     {
  51.         return $this->doListTableNames();
  52.     }
  53.     /**
  54.      * {@inheritDoc}
  55.      */
  56.     public function listTables()
  57.     {
  58.         return $this->doListTables();
  59.     }
  60.     /**
  61.      * {@inheritDoc}
  62.      *
  63.      * @deprecated Use {@see introspectTable()} instead.
  64.      */
  65.     public function listTableDetails($name)
  66.     {
  67.         Deprecation::triggerIfCalledFromOutside(
  68.             'doctrine/dbal',
  69.             'https://github.com/doctrine/dbal/pull/5595',
  70.             '%s is deprecated. Use introspectTable() instead.',
  71.             __METHOD__,
  72.         );
  73.         return $this->doListTableDetails($name);
  74.     }
  75.     /**
  76.      * {@inheritDoc}
  77.      */
  78.     public function listTableColumns($table$database null)
  79.     {
  80.         return $this->doListTableColumns($table$database);
  81.     }
  82.     /**
  83.      * {@inheritDoc}
  84.      */
  85.     public function listTableIndexes($table)
  86.     {
  87.         return $this->doListTableIndexes($table);
  88.     }
  89.     /**
  90.      * {@inheritDoc}
  91.      */
  92.     public function listTableForeignKeys($table$database null)
  93.     {
  94.         return $this->doListTableForeignKeys($table$database);
  95.     }
  96.     /**
  97.      * {@inheritDoc}
  98.      */
  99.     protected function _getPortableViewDefinition($view)
  100.     {
  101.         return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']);
  102.     }
  103.     /**
  104.      * {@inheritDoc}
  105.      */
  106.     protected function _getPortableTableDefinition($table)
  107.     {
  108.         return array_shift($table);
  109.     }
  110.     /**
  111.      * {@inheritDoc}
  112.      */
  113.     protected function _getPortableTableIndexesList($tableIndexes$tableName null)
  114.     {
  115.         foreach ($tableIndexes as $k => $v) {
  116.             $v array_change_key_case($vCASE_LOWER);
  117.             if ($v['key_name'] === 'PRIMARY') {
  118.                 $v['primary'] = true;
  119.             } else {
  120.                 $v['primary'] = false;
  121.             }
  122.             if (strpos($v['index_type'], 'FULLTEXT') !== false) {
  123.                 $v['flags'] = ['FULLTEXT'];
  124.             } elseif (strpos($v['index_type'], 'SPATIAL') !== false) {
  125.                 $v['flags'] = ['SPATIAL'];
  126.             }
  127.             // Ignore prohibited prefix `length` for spatial index
  128.             if (strpos($v['index_type'], 'SPATIAL') === false) {
  129.                 $v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null;
  130.             }
  131.             $tableIndexes[$k] = $v;
  132.         }
  133.         return parent::_getPortableTableIndexesList($tableIndexes$tableName);
  134.     }
  135.     /**
  136.      * {@inheritDoc}
  137.      */
  138.     protected function _getPortableDatabaseDefinition($database)
  139.     {
  140.         return $database['Database'];
  141.     }
  142.     /**
  143.      * {@inheritDoc}
  144.      */
  145.     protected function _getPortableTableColumnDefinition($tableColumn)
  146.     {
  147.         $tableColumn array_change_key_case($tableColumnCASE_LOWER);
  148.         $dbType strtolower($tableColumn['type']);
  149.         $dbType strtok($dbType'(), ');
  150.         assert(is_string($dbType));
  151.         $length $tableColumn['length'] ?? strtok('(), ');
  152.         $fixed null;
  153.         if (! isset($tableColumn['name'])) {
  154.             $tableColumn['name'] = '';
  155.         }
  156.         $scale     null;
  157.         $precision null;
  158.         $type $origType $this->_platform->getDoctrineTypeMapping($dbType);
  159.         // In cases where not connected to a database DESCRIBE $table does not return 'Comment'
  160.         if (isset($tableColumn['comment'])) {
  161.             $type                   $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
  162.             $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
  163.         }
  164.         switch ($dbType) {
  165.             case 'char':
  166.             case 'binary':
  167.                 $fixed true;
  168.                 break;
  169.             case 'float':
  170.             case 'double':
  171.             case 'real':
  172.             case 'numeric':
  173.             case 'decimal':
  174.                 if (
  175.                     preg_match(
  176.                         '([A-Za-z]+\(([0-9]+),([0-9]+)\))',
  177.                         $tableColumn['type'],
  178.                         $match,
  179.                     ) === 1
  180.                 ) {
  181.                     $precision $match[1];
  182.                     $scale     $match[2];
  183.                     $length    null;
  184.                 }
  185.                 break;
  186.             case 'tinytext':
  187.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT;
  188.                 break;
  189.             case 'text':
  190.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_TEXT;
  191.                 break;
  192.             case 'mediumtext':
  193.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT;
  194.                 break;
  195.             case 'tinyblob':
  196.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB;
  197.                 break;
  198.             case 'blob':
  199.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_BLOB;
  200.                 break;
  201.             case 'mediumblob':
  202.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB;
  203.                 break;
  204.             case 'tinyint':
  205.             case 'smallint':
  206.             case 'mediumint':
  207.             case 'int':
  208.             case 'integer':
  209.             case 'bigint':
  210.             case 'year':
  211.                 $length null;
  212.                 break;
  213.         }
  214.         if ($this->_platform instanceof MariaDb1027Platform) {
  215.             $columnDefault $this->getMariaDb1027ColumnDefault($this->_platform$tableColumn['default']);
  216.         } else {
  217.             $columnDefault $tableColumn['default'];
  218.         }
  219.         $options = [
  220.             'length'        => $length !== null ? (int) $length null,
  221.             'unsigned'      => strpos($tableColumn['type'], 'unsigned') !== false,
  222.             'fixed'         => (bool) $fixed,
  223.             'default'       => $columnDefault,
  224.             'notnull'       => $tableColumn['null'] !== 'YES',
  225.             'scale'         => null,
  226.             'precision'     => null,
  227.             'autoincrement' => strpos($tableColumn['extra'], 'auto_increment') !== false,
  228.             'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
  229.                 $tableColumn['comment']
  230.                 : null,
  231.         ];
  232.         if ($scale !== null && $precision !== null) {
  233.             $options['scale']     = (int) $scale;
  234.             $options['precision'] = (int) $precision;
  235.         }
  236.         $column = new Column($tableColumn['field'], Type::getType($type), $options);
  237.         if (isset($tableColumn['characterset'])) {
  238.             $column->setPlatformOption('charset'$tableColumn['characterset']);
  239.         }
  240.         if (isset($tableColumn['collation'])) {
  241.             $column->setPlatformOption('collation'$tableColumn['collation']);
  242.         }
  243.         if (isset($tableColumn['declarationMismatch'])) {
  244.             $column->setPlatformOption('declarationMismatch'$tableColumn['declarationMismatch']);
  245.         }
  246.         // Check underlying database type where doctrine type is inferred from DC2Type comment
  247.         // and set a flag if it is not as expected.
  248.         if ($type === 'json' && $origType !== $type && $this->expectedDbType($type$options) !== $dbType) {
  249.             $column->setPlatformOption('declarationMismatch'true);
  250.         }
  251.         return $column;
  252.     }
  253.     /**
  254.      * Returns the database data type for a given doctrine type and column
  255.      *
  256.      * Note that for data types that depend on length where length is not part of the column definition
  257.      * and therefore the $tableColumn['length'] will not be set, for example TEXT (which could be LONGTEXT,
  258.      * MEDIUMTEXT) or BLOB (LONGBLOB or TINYBLOB), the expectedDbType cannot be inferred exactly, merely
  259.      * the default type.
  260.      *
  261.      * This method is intended to be used to determine underlying database type where doctrine type is
  262.      * inferred from a DC2Type comment.
  263.      *
  264.      * @param mixed[] $tableColumn
  265.      */
  266.     private function expectedDbType(string $type, array $tableColumn): string
  267.     {
  268.         $_type          Type::getType($type);
  269.         $expectedDbType strtolower($_type->getSQLDeclaration($tableColumn$this->_platform));
  270.         $expectedDbType strtok($expectedDbType'(), ');
  271.         return $expectedDbType === false '' $expectedDbType;
  272.     }
  273.     /**
  274.      * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
  275.      *
  276.      * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted
  277.      *   to distinguish them from expressions (see MDEV-10134).
  278.      * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema
  279.      *   as current_timestamp(), currdate(), currtime()
  280.      * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have
  281.      *   null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053)
  282.      * - \' is always stored as '' in information_schema (normalized)
  283.      *
  284.      * @link https://mariadb.com/kb/en/library/information-schema-columns-table/
  285.      * @link https://jira.mariadb.org/browse/MDEV-13132
  286.      *
  287.      * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7
  288.      */
  289.     private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault): ?string
  290.     {
  291.         if ($columnDefault === 'NULL' || $columnDefault === null) {
  292.             return null;
  293.         }
  294.         if (preg_match('/^\'(.*)\'$/'$columnDefault$matches) === 1) {
  295.             return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
  296.         }
  297.         switch ($columnDefault) {
  298.             case 'current_timestamp()':
  299.                 return $platform->getCurrentTimestampSQL();
  300.             case 'curdate()':
  301.                 return $platform->getCurrentDateSQL();
  302.             case 'curtime()':
  303.                 return $platform->getCurrentTimeSQL();
  304.         }
  305.         return $columnDefault;
  306.     }
  307.     /**
  308.      * {@inheritDoc}
  309.      */
  310.     protected function _getPortableTableForeignKeysList($tableForeignKeys)
  311.     {
  312.         $list = [];
  313.         foreach ($tableForeignKeys as $value) {
  314.             $value array_change_key_case($valueCASE_LOWER);
  315.             if (! isset($list[$value['constraint_name']])) {
  316.                 if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') {
  317.                     $value['delete_rule'] = null;
  318.                 }
  319.                 if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') {
  320.                     $value['update_rule'] = null;
  321.                 }
  322.                 $list[$value['constraint_name']] = [
  323.                     'name' => $value['constraint_name'],
  324.                     'local' => [],
  325.                     'foreign' => [],
  326.                     'foreignTable' => $value['referenced_table_name'],
  327.                     'onDelete' => $value['delete_rule'],
  328.                     'onUpdate' => $value['update_rule'],
  329.                 ];
  330.             }
  331.             $list[$value['constraint_name']]['local'][]   = $value['column_name'];
  332.             $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name'];
  333.         }
  334.         return parent::_getPortableTableForeignKeysList($list);
  335.     }
  336.     /**
  337.      * {@inheritDoc}
  338.      */
  339.     protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint
  340.     {
  341.         return new ForeignKeyConstraint(
  342.             $tableForeignKey['local'],
  343.             $tableForeignKey['foreignTable'],
  344.             $tableForeignKey['foreign'],
  345.             $tableForeignKey['name'],
  346.             [
  347.                 'onDelete' => $tableForeignKey['onDelete'],
  348.                 'onUpdate' => $tableForeignKey['onUpdate'],
  349.             ],
  350.         );
  351.     }
  352.     public function createComparator(): Comparator
  353.     {
  354.         return new MySQL\Comparator(
  355.             $this->_platform,
  356.             new CachingCollationMetadataProvider(
  357.                 new ConnectionCollationMetadataProvider($this->_conn),
  358.             ),
  359.         );
  360.     }
  361.     protected function selectTableNames(string $databaseName): Result
  362.     {
  363.         $sql = <<<'SQL'
  364. SELECT TABLE_NAME
  365. FROM information_schema.TABLES
  366. WHERE TABLE_SCHEMA = ?
  367.   AND TABLE_TYPE = 'BASE TABLE'
  368. ORDER BY TABLE_NAME
  369. SQL;
  370.         return $this->_conn->executeQuery($sql, [$databaseName]);
  371.     }
  372.     protected function selectTableColumns(string $databaseName, ?string $tableName null): Result
  373.     {
  374.         // @todo 4.0 - call getColumnTypeSQLSnippet() instead
  375.         [$columnTypeSQL$joinCheckConstraintSQL] = $this->_platform->getColumnTypeSQLSnippets('c'$databaseName);
  376.         $sql 'SELECT';
  377.         if ($tableName === null) {
  378.             $sql .= ' c.TABLE_NAME,';
  379.         }
  380.         $sql .= <<<SQL
  381.        c.COLUMN_NAME        AS field,
  382.        $columnTypeSQL       AS type,
  383.        c.IS_NULLABLE        AS `null`,
  384.        c.COLUMN_KEY         AS `key`,
  385.        c.COLUMN_DEFAULT     AS `default`,
  386.        c.EXTRA,
  387.        c.COLUMN_COMMENT     AS comment,
  388.        c.CHARACTER_SET_NAME AS characterset,
  389.        c.COLLATION_NAME     AS collation
  390. FROM information_schema.COLUMNS c
  391.     INNER JOIN information_schema.TABLES t
  392.         ON t.TABLE_NAME = c.TABLE_NAME
  393.     $joinCheckConstraintSQL
  394. SQL;
  395.         // The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition
  396.         // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
  397.         // caused by https://bugs.mysql.com/bug.php?id=81347
  398.         $conditions = ['c.TABLE_SCHEMA = ?''t.TABLE_SCHEMA = ?'"t.TABLE_TYPE = 'BASE TABLE'"];
  399.         $params     = [$databaseName$databaseName];
  400.         if ($tableName !== null) {
  401.             $conditions[] = 't.TABLE_NAME = ?';
  402.             $params[]     = $tableName;
  403.         }
  404.         $sql .= ' WHERE ' implode(' AND '$conditions) . ' ORDER BY ORDINAL_POSITION';
  405.         return $this->_conn->executeQuery($sql$params);
  406.     }
  407.     protected function selectIndexColumns(string $databaseName, ?string $tableName null): Result
  408.     {
  409.         $sql 'SELECT';
  410.         if ($tableName === null) {
  411.             $sql .= ' TABLE_NAME,';
  412.         }
  413.         $sql .= <<<'SQL'
  414.         NON_UNIQUE  AS Non_Unique,
  415.         INDEX_NAME  AS Key_name,
  416.         COLUMN_NAME AS Column_Name,
  417.         SUB_PART    AS Sub_Part,
  418.         INDEX_TYPE  AS Index_Type
  419. FROM information_schema.STATISTICS
  420. SQL;
  421.         $conditions = ['TABLE_SCHEMA = ?'];
  422.         $params     = [$databaseName];
  423.         if ($tableName !== null) {
  424.             $conditions[] = 'TABLE_NAME = ?';
  425.             $params[]     = $tableName;
  426.         }
  427.         $sql .= ' WHERE ' implode(' AND '$conditions) . ' ORDER BY SEQ_IN_INDEX';
  428.         return $this->_conn->executeQuery($sql$params);
  429.     }
  430.     protected function selectForeignKeyColumns(string $databaseName, ?string $tableName null): Result
  431.     {
  432.         $sql 'SELECT DISTINCT';
  433.         if ($tableName === null) {
  434.             $sql .= ' k.TABLE_NAME,';
  435.         }
  436.         $sql .= <<<'SQL'
  437.             k.CONSTRAINT_NAME,
  438.             k.COLUMN_NAME,
  439.             k.REFERENCED_TABLE_NAME,
  440.             k.REFERENCED_COLUMN_NAME,
  441.             k.ORDINAL_POSITION /*!50116,
  442.             c.UPDATE_RULE,
  443.             c.DELETE_RULE */
  444. FROM information_schema.key_column_usage k /*!50116
  445. INNER JOIN information_schema.referential_constraints c
  446. ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME
  447. AND c.TABLE_NAME = k.TABLE_NAME */
  448. SQL;
  449.         $conditions = ['k.TABLE_SCHEMA = ?'];
  450.         $params     = [$databaseName];
  451.         if ($tableName !== null) {
  452.             $conditions[] = 'k.TABLE_NAME = ?';
  453.             $params[]     = $tableName;
  454.         }
  455.         $conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL';
  456.         $sql .= ' WHERE ' implode(' AND '$conditions)
  457.             // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition
  458.             // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
  459.             // caused by https://bugs.mysql.com/bug.php?id=81347.
  460.             // Use a string literal for the database name since the internal PDO SQL parser
  461.             // cannot recognize parameter placeholders inside conditional comments
  462.             ' /*!50116 AND c.CONSTRAINT_SCHEMA = ' $this->_conn->quote($databaseName) . ' */'
  463.             ' ORDER BY k.ORDINAL_POSITION';
  464.         return $this->_conn->executeQuery($sql$params);
  465.     }
  466.     /**
  467.      * {@inheritDoc}
  468.      */
  469.     protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName null): array
  470.     {
  471.         $sql = <<<'SQL'
  472.     SELECT t.TABLE_NAME,
  473.            t.ENGINE,
  474.            t.AUTO_INCREMENT,
  475.            t.TABLE_COMMENT,
  476.            t.CREATE_OPTIONS,
  477.            t.TABLE_COLLATION,
  478.            ccsa.CHARACTER_SET_NAME
  479.       FROM information_schema.TABLES t
  480.         INNER JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY ccsa
  481.             ON ccsa.COLLATION_NAME = t.TABLE_COLLATION
  482. SQL;
  483.         $conditions = ['t.TABLE_SCHEMA = ?'];
  484.         $params     = [$databaseName];
  485.         if ($tableName !== null) {
  486.             $conditions[] = 't.TABLE_NAME = ?';
  487.             $params[]     = $tableName;
  488.         }
  489.         $conditions[] = "t.TABLE_TYPE = 'BASE TABLE'";
  490.         $sql .= ' WHERE ' implode(' AND '$conditions);
  491.         /** @var array<string,array<string,mixed>> $metadata */
  492.         $metadata $this->_conn->executeQuery($sql$params)
  493.             ->fetchAllAssociativeIndexed();
  494.         $tableOptions = [];
  495.         foreach ($metadata as $table => $data) {
  496.             $data array_change_key_case($dataCASE_LOWER);
  497.             $tableOptions[$table] = [
  498.                 'engine'         => $data['engine'],
  499.                 'collation'      => $data['table_collation'],
  500.                 'charset'        => $data['character_set_name'],
  501.                 'autoincrement'  => $data['auto_increment'],
  502.                 'comment'        => $data['table_comment'],
  503.                 'create_options' => $this->parseCreateOptions($data['create_options']),
  504.             ];
  505.         }
  506.         return $tableOptions;
  507.     }
  508.     /** @return string[]|true[] */
  509.     private function parseCreateOptions(?string $string): array
  510.     {
  511.         $options = [];
  512.         if ($string === null || $string === '') {
  513.             return $options;
  514.         }
  515.         foreach (explode(' '$string) as $pair) {
  516.             $parts explode('='$pair2);
  517.             $options[$parts[0]] = $parts[1] ?? true;
  518.         }
  519.         return $options;
  520.     }
  521. }