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

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