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

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\Types\Type;
  7. use function array_change_key_case;
  8. use function array_shift;
  9. use function assert;
  10. use function explode;
  11. use function is_string;
  12. use function preg_match;
  13. use function strpos;
  14. use function strtok;
  15. use function strtolower;
  16. use function strtr;
  17. use const CASE_LOWER;
  18. /**
  19.  * Schema manager for the MySQL RDBMS.
  20.  *
  21.  * @extends AbstractSchemaManager<AbstractMySQLPlatform>
  22.  */
  23. class MySQLSchemaManager extends AbstractSchemaManager
  24. {
  25.     /**
  26.      * @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences
  27.      */
  28.     private const MARIADB_ESCAPE_SEQUENCES = [
  29.         '\\0' => "\0",
  30.         "\\'" => "'",
  31.         '\\"' => '"',
  32.         '\\b' => "\b",
  33.         '\\n' => "\n",
  34.         '\\r' => "\r",
  35.         '\\t' => "\t",
  36.         '\\Z' => "\x1a",
  37.         '\\\\' => '\\',
  38.         '\\%' => '%',
  39.         '\\_' => '_',
  40.         // Internally, MariaDB escapes single quotes using the standard syntax
  41.         "''" => "'",
  42.     ];
  43.     /**
  44.      * {@inheritdoc}
  45.      */
  46.     protected function _getPortableViewDefinition($view)
  47.     {
  48.         return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']);
  49.     }
  50.     /**
  51.      * {@inheritdoc}
  52.      */
  53.     protected function _getPortableTableDefinition($table)
  54.     {
  55.         return array_shift($table);
  56.     }
  57.     /**
  58.      * {@inheritdoc}
  59.      */
  60.     protected function _getPortableUserDefinition($user)
  61.     {
  62.         return [
  63.             'user' => $user['User'],
  64.             'password' => $user['Password'],
  65.         ];
  66.     }
  67.     /**
  68.      * {@inheritdoc}
  69.      */
  70.     protected function _getPortableTableIndexesList($tableIndexes$tableName null)
  71.     {
  72.         foreach ($tableIndexes as $k => $v) {
  73.             $v array_change_key_case($vCASE_LOWER);
  74.             if ($v['key_name'] === 'PRIMARY') {
  75.                 $v['primary'] = true;
  76.             } else {
  77.                 $v['primary'] = false;
  78.             }
  79.             if (strpos($v['index_type'], 'FULLTEXT') !== false) {
  80.                 $v['flags'] = ['FULLTEXT'];
  81.             } elseif (strpos($v['index_type'], 'SPATIAL') !== false) {
  82.                 $v['flags'] = ['SPATIAL'];
  83.             }
  84.             // Ignore prohibited prefix `length` for spatial index
  85.             if (strpos($v['index_type'], 'SPATIAL') === false) {
  86.                 $v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null;
  87.             }
  88.             $tableIndexes[$k] = $v;
  89.         }
  90.         return parent::_getPortableTableIndexesList($tableIndexes$tableName);
  91.     }
  92.     /**
  93.      * {@inheritdoc}
  94.      */
  95.     protected function _getPortableDatabaseDefinition($database)
  96.     {
  97.         return $database['Database'];
  98.     }
  99.     /**
  100.      * {@inheritdoc}
  101.      */
  102.     protected function _getPortableTableColumnDefinition($tableColumn)
  103.     {
  104.         $tableColumn array_change_key_case($tableColumnCASE_LOWER);
  105.         $dbType strtolower($tableColumn['type']);
  106.         $dbType strtok($dbType'(), ');
  107.         assert(is_string($dbType));
  108.         $length $tableColumn['length'] ?? strtok('(), ');
  109.         $fixed null;
  110.         if (! isset($tableColumn['name'])) {
  111.             $tableColumn['name'] = '';
  112.         }
  113.         $scale     null;
  114.         $precision null;
  115.         $type $this->_platform->getDoctrineTypeMapping($dbType);
  116.         // In cases where not connected to a database DESCRIBE $table does not return 'Comment'
  117.         if (isset($tableColumn['comment'])) {
  118.             $type                   $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
  119.             $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
  120.         }
  121.         switch ($dbType) {
  122.             case 'char':
  123.             case 'binary':
  124.                 $fixed true;
  125.                 break;
  126.             case 'float':
  127.             case 'double':
  128.             case 'real':
  129.             case 'numeric':
  130.             case 'decimal':
  131.                 if (
  132.                     preg_match(
  133.                         '([A-Za-z]+\(([0-9]+),([0-9]+)\))',
  134.                         $tableColumn['type'],
  135.                         $match
  136.                     ) === 1
  137.                 ) {
  138.                     $precision $match[1];
  139.                     $scale     $match[2];
  140.                     $length    null;
  141.                 }
  142.                 break;
  143.             case 'tinytext':
  144.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT;
  145.                 break;
  146.             case 'text':
  147.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_TEXT;
  148.                 break;
  149.             case 'mediumtext':
  150.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT;
  151.                 break;
  152.             case 'tinyblob':
  153.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB;
  154.                 break;
  155.             case 'blob':
  156.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_BLOB;
  157.                 break;
  158.             case 'mediumblob':
  159.                 $length AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB;
  160.                 break;
  161.             case 'tinyint':
  162.             case 'smallint':
  163.             case 'mediumint':
  164.             case 'int':
  165.             case 'integer':
  166.             case 'bigint':
  167.             case 'year':
  168.                 $length null;
  169.                 break;
  170.         }
  171.         if ($this->_platform instanceof MariaDb1027Platform) {
  172.             $columnDefault $this->getMariaDb1027ColumnDefault($this->_platform$tableColumn['default']);
  173.         } else {
  174.             $columnDefault $tableColumn['default'];
  175.         }
  176.         $options = [
  177.             'length'        => $length !== null ? (int) $length null,
  178.             'unsigned'      => strpos($tableColumn['type'], 'unsigned') !== false,
  179.             'fixed'         => (bool) $fixed,
  180.             'default'       => $columnDefault,
  181.             'notnull'       => $tableColumn['null'] !== 'YES',
  182.             'scale'         => null,
  183.             'precision'     => null,
  184.             'autoincrement' => strpos($tableColumn['extra'], 'auto_increment') !== false,
  185.             'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
  186.                 $tableColumn['comment']
  187.                 : null,
  188.         ];
  189.         if ($scale !== null && $precision !== null) {
  190.             $options['scale']     = (int) $scale;
  191.             $options['precision'] = (int) $precision;
  192.         }
  193.         $column = new Column($tableColumn['field'], Type::getType($type), $options);
  194.         if (isset($tableColumn['characterset'])) {
  195.             $column->setPlatformOption('charset'$tableColumn['characterset']);
  196.         }
  197.         if (isset($tableColumn['collation'])) {
  198.             $column->setPlatformOption('collation'$tableColumn['collation']);
  199.         }
  200.         return $column;
  201.     }
  202.     /**
  203.      * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers.
  204.      *
  205.      * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted
  206.      *   to distinguish them from expressions (see MDEV-10134).
  207.      * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema
  208.      *   as current_timestamp(), currdate(), currtime()
  209.      * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have
  210.      *   null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053)
  211.      * - \' is always stored as '' in information_schema (normalized)
  212.      *
  213.      * @link https://mariadb.com/kb/en/library/information-schema-columns-table/
  214.      * @link https://jira.mariadb.org/browse/MDEV-13132
  215.      *
  216.      * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7
  217.      */
  218.     private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault): ?string
  219.     {
  220.         if ($columnDefault === 'NULL' || $columnDefault === null) {
  221.             return null;
  222.         }
  223.         if (preg_match('/^\'(.*)\'$/'$columnDefault$matches) === 1) {
  224.             return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES);
  225.         }
  226.         switch ($columnDefault) {
  227.             case 'current_timestamp()':
  228.                 return $platform->getCurrentTimestampSQL();
  229.             case 'curdate()':
  230.                 return $platform->getCurrentDateSQL();
  231.             case 'curtime()':
  232.                 return $platform->getCurrentTimeSQL();
  233.         }
  234.         return $columnDefault;
  235.     }
  236.     /**
  237.      * {@inheritdoc}
  238.      */
  239.     protected function _getPortableTableForeignKeysList($tableForeignKeys)
  240.     {
  241.         $list = [];
  242.         foreach ($tableForeignKeys as $value) {
  243.             $value array_change_key_case($valueCASE_LOWER);
  244.             if (! isset($list[$value['constraint_name']])) {
  245.                 if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') {
  246.                     $value['delete_rule'] = null;
  247.                 }
  248.                 if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') {
  249.                     $value['update_rule'] = null;
  250.                 }
  251.                 $list[$value['constraint_name']] = [
  252.                     'name' => $value['constraint_name'],
  253.                     'local' => [],
  254.                     'foreign' => [],
  255.                     'foreignTable' => $value['referenced_table_name'],
  256.                     'onDelete' => $value['delete_rule'],
  257.                     'onUpdate' => $value['update_rule'],
  258.                 ];
  259.             }
  260.             $list[$value['constraint_name']]['local'][]   = $value['column_name'];
  261.             $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name'];
  262.         }
  263.         $result = [];
  264.         foreach ($list as $constraint) {
  265.             $result[] = new ForeignKeyConstraint(
  266.                 $constraint['local'],
  267.                 $constraint['foreignTable'],
  268.                 $constraint['foreign'],
  269.                 $constraint['name'],
  270.                 [
  271.                     'onDelete' => $constraint['onDelete'],
  272.                     'onUpdate' => $constraint['onUpdate'],
  273.                 ]
  274.             );
  275.         }
  276.         return $result;
  277.     }
  278.     /**
  279.      * {@inheritdoc}
  280.      */
  281.     public function listTableDetails($name)
  282.     {
  283.         $table parent::listTableDetails($name);
  284.         $sql $this->_platform->getListTableMetadataSQL($name);
  285.         $tableOptions $this->_conn->fetchAssociative($sql);
  286.         if ($tableOptions === false) {
  287.             return $table;
  288.         }
  289.         $table->addOption('engine'$tableOptions['ENGINE']);
  290.         if ($tableOptions['TABLE_COLLATION'] !== null) {
  291.             $table->addOption('collation'$tableOptions['TABLE_COLLATION']);
  292.         }
  293.         $table->addOption('charset'$tableOptions['CHARACTER_SET_NAME']);
  294.         if ($tableOptions['AUTO_INCREMENT'] !== null) {
  295.             $table->addOption('autoincrement'$tableOptions['AUTO_INCREMENT']);
  296.         }
  297.         $table->addOption('comment'$tableOptions['TABLE_COMMENT']);
  298.         $table->addOption('create_options'$this->parseCreateOptions($tableOptions['CREATE_OPTIONS']));
  299.         return $table;
  300.     }
  301.     public function createComparator(): Comparator
  302.     {
  303.         return new MySQL\Comparator($this->getDatabasePlatform());
  304.     }
  305.     /**
  306.      * @return string[]|true[]
  307.      */
  308.     private function parseCreateOptions(?string $string): array
  309.     {
  310.         $options = [];
  311.         if ($string === null || $string === '') {
  312.             return $options;
  313.         }
  314.         foreach (explode(' '$string) as $pair) {
  315.             $parts explode('='$pair2);
  316.             $options[$parts[0]] = $parts[1] ?? true;
  317.         }
  318.         return $options;
  319.     }
  320. }