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

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::trigger(
  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 $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.         return $column;
  244.     }
  245.     /**
  246.      * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
  247.      *
  248.      * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted
  249.      *   to distinguish them from expressions (see MDEV-10134).
  250.      * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema
  251.      *   as current_timestamp(), currdate(), currtime()
  252.      * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have
  253.      *   null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053)
  254.      * - \' is always stored as '' in information_schema (normalized)
  255.      *
  256.      * @link https://mariadb.com/kb/en/library/information-schema-columns-table/
  257.      * @link https://jira.mariadb.org/browse/MDEV-13132
  258.      *
  259.      * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7
  260.      */
  261.     private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault): ?string
  262.     {
  263.         if ($columnDefault === 'NULL' || $columnDefault === null) {
  264.             return null;
  265.         }
  266.         if (preg_match('/^\'(.*)\'$/'$columnDefault$matches) === 1) {
  267.             return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
  268.         }
  269.         switch ($columnDefault) {
  270.             case 'current_timestamp()':
  271.                 return $platform->getCurrentTimestampSQL();
  272.             case 'curdate()':
  273.                 return $platform->getCurrentDateSQL();
  274.             case 'curtime()':
  275.                 return $platform->getCurrentTimeSQL();
  276.         }
  277.         return $columnDefault;
  278.     }
  279.     /**
  280.      * {@inheritdoc}
  281.      */
  282.     protected function _getPortableTableForeignKeysList($tableForeignKeys)
  283.     {
  284.         $list = [];
  285.         foreach ($tableForeignKeys as $value) {
  286.             $value array_change_key_case($valueCASE_LOWER);
  287.             if (! isset($list[$value['constraint_name']])) {
  288.                 if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') {
  289.                     $value['delete_rule'] = null;
  290.                 }
  291.                 if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') {
  292.                     $value['update_rule'] = null;
  293.                 }
  294.                 $list[$value['constraint_name']] = [
  295.                     'name' => $value['constraint_name'],
  296.                     'local' => [],
  297.                     'foreign' => [],
  298.                     'foreignTable' => $value['referenced_table_name'],
  299.                     'onDelete' => $value['delete_rule'],
  300.                     'onUpdate' => $value['update_rule'],
  301.                 ];
  302.             }
  303.             $list[$value['constraint_name']]['local'][]   = $value['column_name'];
  304.             $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name'];
  305.         }
  306.         return parent::_getPortableTableForeignKeysList($list);
  307.     }
  308.     /**
  309.      * {@inheritDoc}
  310.      */
  311.     protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint
  312.     {
  313.         return new ForeignKeyConstraint(
  314.             $tableForeignKey['local'],
  315.             $tableForeignKey['foreignTable'],
  316.             $tableForeignKey['foreign'],
  317.             $tableForeignKey['name'],
  318.             [
  319.                 'onDelete' => $tableForeignKey['onDelete'],
  320.                 'onUpdate' => $tableForeignKey['onUpdate'],
  321.             ],
  322.         );
  323.     }
  324.     public function createComparator(): Comparator
  325.     {
  326.         return new MySQL\Comparator(
  327.             $this->_platform,
  328.             new CachingCollationMetadataProvider(
  329.                 new ConnectionCollationMetadataProvider($this->_conn),
  330.             ),
  331.         );
  332.     }
  333.     protected function selectTableNames(string $databaseName): Result
  334.     {
  335.         $sql = <<<'SQL'
  336. SELECT TABLE_NAME
  337. FROM information_schema.TABLES
  338. WHERE TABLE_SCHEMA = ?
  339.   AND TABLE_TYPE = 'BASE TABLE'
  340. ORDER BY TABLE_NAME
  341. SQL;
  342.         return $this->_conn->executeQuery($sql, [$databaseName]);
  343.     }
  344.     protected function selectTableColumns(string $databaseName, ?string $tableName null): Result
  345.     {
  346.         $sql 'SELECT';
  347.         if ($tableName === null) {
  348.             $sql .= ' c.TABLE_NAME,';
  349.         }
  350.         $sql .= <<<'SQL'
  351.        c.COLUMN_NAME        AS field,
  352.        c.COLUMN_TYPE        AS type,
  353.        c.IS_NULLABLE        AS `null`,
  354.        c.COLUMN_KEY         AS `key`,
  355.        c.COLUMN_DEFAULT     AS `default`,
  356.        c.EXTRA,
  357.        c.COLUMN_COMMENT     AS comment,
  358.        c.CHARACTER_SET_NAME AS characterset,
  359.        c.COLLATION_NAME     AS collation
  360. FROM information_schema.COLUMNS c
  361.     INNER JOIN information_schema.TABLES t
  362.         ON t.TABLE_NAME = c.TABLE_NAME
  363. SQL;
  364.         // The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition
  365.         // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
  366.         // caused by https://bugs.mysql.com/bug.php?id=81347
  367.         $conditions = ['c.TABLE_SCHEMA = ?''t.TABLE_SCHEMA = ?'"t.TABLE_TYPE = 'BASE TABLE'"];
  368.         $params     = [$databaseName$databaseName];
  369.         if ($tableName !== null) {
  370.             $conditions[] = 't.TABLE_NAME = ?';
  371.             $params[]     = $tableName;
  372.         }
  373.         $sql .= ' WHERE ' implode(' AND '$conditions) . ' ORDER BY ORDINAL_POSITION';
  374.         return $this->_conn->executeQuery($sql$params);
  375.     }
  376.     protected function selectIndexColumns(string $databaseName, ?string $tableName null): Result
  377.     {
  378.         $sql 'SELECT';
  379.         if ($tableName === null) {
  380.             $sql .= ' TABLE_NAME,';
  381.         }
  382.         $sql .= <<<'SQL'
  383.         NON_UNIQUE  AS Non_Unique,
  384.         INDEX_NAME  AS Key_name,
  385.         COLUMN_NAME AS Column_Name,
  386.         SUB_PART    AS Sub_Part,
  387.         INDEX_TYPE  AS Index_Type
  388. FROM information_schema.STATISTICS
  389. SQL;
  390.         $conditions = ['TABLE_SCHEMA = ?'];
  391.         $params     = [$databaseName];
  392.         if ($tableName !== null) {
  393.             $conditions[] = 'TABLE_NAME = ?';
  394.             $params[]     = $tableName;
  395.         }
  396.         $sql .= ' WHERE ' implode(' AND '$conditions) . ' ORDER BY SEQ_IN_INDEX';
  397.         return $this->_conn->executeQuery($sql$params);
  398.     }
  399.     protected function selectForeignKeyColumns(string $databaseName, ?string $tableName null): Result
  400.     {
  401.         $sql 'SELECT DISTINCT';
  402.         if ($tableName === null) {
  403.             $sql .= ' k.TABLE_NAME,';
  404.         }
  405.         $sql .= <<<'SQL'
  406.             k.CONSTRAINT_NAME,
  407.             k.COLUMN_NAME,
  408.             k.REFERENCED_TABLE_NAME,
  409.             k.REFERENCED_COLUMN_NAME,
  410.             k.ORDINAL_POSITION /*!50116,
  411.             c.UPDATE_RULE,
  412.             c.DELETE_RULE */
  413. FROM information_schema.key_column_usage k /*!50116
  414. INNER JOIN information_schema.referential_constraints c
  415. ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME
  416. AND c.TABLE_NAME = k.TABLE_NAME */
  417. SQL;
  418.         $conditions = ['k.TABLE_SCHEMA = ?'];
  419.         $params     = [$databaseName];
  420.         if ($tableName !== null) {
  421.             $conditions[] = 'k.TABLE_NAME = ?';
  422.             $params[]     = $tableName;
  423.         }
  424.         $conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL';
  425.         $sql .= ' WHERE ' implode(' AND '$conditions)
  426.             // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition
  427.             // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions
  428.             // caused by https://bugs.mysql.com/bug.php?id=81347.
  429.             // Use a string literal for the database name since the internal PDO SQL parser
  430.             // cannot recognize parameter placeholders inside conditional comments
  431.             ' /*!50116 AND c.CONSTRAINT_SCHEMA = ' $this->_conn->quote($databaseName) . ' */'
  432.             ' ORDER BY k.ORDINAL_POSITION';
  433.         return $this->_conn->executeQuery($sql$params);
  434.     }
  435.     /**
  436.      * {@inheritDoc}
  437.      */
  438.     protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName null): array
  439.     {
  440.         $sql = <<<'SQL'
  441.     SELECT t.TABLE_NAME,
  442.            t.ENGINE,
  443.            t.AUTO_INCREMENT,
  444.            t.TABLE_COMMENT,
  445.            t.CREATE_OPTIONS,
  446.            t.TABLE_COLLATION,
  447.            ccsa.CHARACTER_SET_NAME
  448.       FROM information_schema.TABLES t
  449.         INNER JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY ccsa
  450.             ON ccsa.COLLATION_NAME = t.TABLE_COLLATION
  451. SQL;
  452.         $conditions = ['t.TABLE_SCHEMA = ?'];
  453.         $params     = [$databaseName];
  454.         if ($tableName !== null) {
  455.             $conditions[] = 't.TABLE_NAME = ?';
  456.             $params[]     = $tableName;
  457.         }
  458.         $conditions[] = "t.TABLE_TYPE = 'BASE TABLE'";
  459.         $sql .= ' WHERE ' implode(' AND '$conditions);
  460.         /** @var array<string,array<string,mixed>> $metadata */
  461.         $metadata $this->_conn->executeQuery($sql$params)
  462.             ->fetchAllAssociativeIndexed();
  463.         $tableOptions = [];
  464.         foreach ($metadata as $table => $data) {
  465.             $data array_change_key_case($dataCASE_LOWER);
  466.             $tableOptions[$table] = [
  467.                 'engine'         => $data['engine'],
  468.                 'collation'      => $data['table_collation'],
  469.                 'charset'        => $data['character_set_name'],
  470.                 'autoincrement'  => $data['auto_increment'],
  471.                 'comment'        => $data['table_comment'],
  472.                 'create_options' => $this->parseCreateOptions($data['create_options']),
  473.             ];
  474.         }
  475.         return $tableOptions;
  476.     }
  477.     /** @return string[]|true[] */
  478.     private function parseCreateOptions(?string $string): array
  479.     {
  480.         $options = [];
  481.         if ($string === null || $string === '') {
  482.             return $options;
  483.         }
  484.         foreach (explode(' '$string) as $pair) {
  485.             $parts explode('='$pair2);
  486.             $options[$parts[0]] = $parts[1] ?? true;
  487.         }
  488.         return $options;
  489.     }
  490. }