*/ class MysqlAdapter extends PdoAdapter { /** * @var string[] */ protected static $specificColumnTypes = [ self::PHINX_TYPE_ENUM, self::PHINX_TYPE_SET, self::PHINX_TYPE_YEAR, self::PHINX_TYPE_JSON, self::PHINX_TYPE_BINARYUUID, self::PHINX_TYPE_TINYBLOB, self::PHINX_TYPE_MEDIUMBLOB, self::PHINX_TYPE_LONGBLOB, self::PHINX_TYPE_MEDIUM_INTEGER, ]; /** * @var bool[] */ protected $signedColumnTypes = [ self::PHINX_TYPE_INTEGER => true, self::PHINX_TYPE_TINY_INTEGER => true, self::PHINX_TYPE_SMALL_INTEGER => true, self::PHINX_TYPE_MEDIUM_INTEGER => true, self::PHINX_TYPE_BIG_INTEGER => true, self::PHINX_TYPE_FLOAT => true, self::PHINX_TYPE_DECIMAL => true, self::PHINX_TYPE_DOUBLE => true, self::PHINX_TYPE_BOOLEAN => true, ]; // These constants roughly correspond to the maximum allowed value for each field, // except for the `_LONG` and `_BIG` variants, which are maxed at 32-bit // PHP_INT_MAX value. The `INT_REGULAR` field is just arbitrarily half of INT_BIG // as its actual value is its regular value is larger than PHP_INT_MAX. We do this // to keep consistent the type hints for getSqlType and Column::$limit being integers. public const TEXT_TINY = 255; public const TEXT_SMALL = 255; /* deprecated, alias of TEXT_TINY */ public const TEXT_REGULAR = 65535; public const TEXT_MEDIUM = 16777215; public const TEXT_LONG = 2147483647; // According to https://dev.mysql.com/doc/refman/5.0/en/blob.html BLOB sizes are the same as TEXT public const BLOB_TINY = 255; public const BLOB_SMALL = 255; /* deprecated, alias of BLOB_TINY */ public const BLOB_REGULAR = 65535; public const BLOB_MEDIUM = 16777215; public const BLOB_LONG = 2147483647; public const INT_TINY = 255; public const INT_SMALL = 65535; public const INT_MEDIUM = 16777215; public const INT_REGULAR = 1073741823; public const INT_BIG = 2147483647; public const INT_DISPLAY_TINY = 4; public const INT_DISPLAY_SMALL = 6; public const INT_DISPLAY_MEDIUM = 8; public const INT_DISPLAY_REGULAR = 11; public const INT_DISPLAY_BIG = 20; public const BIT = 64; public const TYPE_YEAR = 'year'; public const FIRST = 'FIRST'; /** * {@inheritDoc} * * @throws \RuntimeException * @throws \InvalidArgumentException * @return void */ public function connect(): void { if ($this->connection === null) { if (!class_exists('PDO') || !in_array('mysql', PDO::getAvailableDrivers(), true)) { // @codeCoverageIgnoreStart throw new RuntimeException('You need to enable the PDO_Mysql extension for Phinx to run properly.'); // @codeCoverageIgnoreEnd } $options = $this->getOptions(); $dsn = 'mysql:'; if (!empty($options['unix_socket'])) { // use socket connection $dsn .= 'unix_socket=' . $options['unix_socket']; } else { // use network connection $dsn .= 'host=' . $options['host']; if (!empty($options['port'])) { $dsn .= ';port=' . $options['port']; } } $dsn .= ';dbname=' . $options['name']; // charset support if (!empty($options['charset'])) { $dsn .= ';charset=' . $options['charset']; } $driverOptions = []; // use custom data fetch mode if (!empty($options['fetch_mode'])) { $driverOptions[PDO::ATTR_DEFAULT_FETCH_MODE] = constant('\PDO::FETCH_' . strtoupper($options['fetch_mode'])); } // pass \PDO::ATTR_PERSISTENT to driver options instead of useless setting it after instantiation if (isset($options['attr_persistent'])) { $driverOptions[PDO::ATTR_PERSISTENT] = $options['attr_persistent']; } // support arbitrary \PDO::MYSQL_ATTR_* driver options and pass them to PDO // https://php.net/manual/en/ref.pdo-mysql.php#pdo-mysql.constants foreach ($options as $key => $option) { if (strpos($key, 'mysql_attr_') === 0) { $pdoConstant = '\PDO::' . strtoupper($key); if (!defined($pdoConstant)) { throw new \UnexpectedValueException('Invalid PDO attribute: ' . $key . ' (' . $pdoConstant . ')'); } $driverOptions[constant($pdoConstant)] = $option; } } $db = $this->createPdoConnection($dsn, $options['user'] ?? null, $options['pass'] ?? null, $driverOptions); $this->setConnection($db); } } /** * @inheritDoc */ public function disconnect(): void { $this->connection = null; } /** * @inheritDoc */ public function hasTransactions(): bool { return true; } /** * @inheritDoc */ public function beginTransaction(): void { $this->execute('START TRANSACTION'); } /** * @inheritDoc */ public function commitTransaction(): void { $this->execute('COMMIT'); } /** * @inheritDoc */ public function rollbackTransaction(): void { $this->execute('ROLLBACK'); } /** * @inheritDoc */ public function quoteTableName(string $tableName): string { return str_replace('.', '`.`', $this->quoteColumnName($tableName)); } /** * @inheritDoc */ public function quoteColumnName(string $columnName): string { return '`' . str_replace('`', '``', $columnName) . '`'; } /** * @inheritDoc */ public function hasTable(string $tableName): bool { if ($this->hasCreatedTable($tableName)) { return true; } if (strpos($tableName, '.') !== false) { [$schema, $table] = explode('.', $tableName); $exists = $this->hasTableWithSchema($schema, $table); // Only break here on success, because it is possible for table names to contain a dot. if ($exists) { return true; } } $options = $this->getOptions(); return $this->hasTableWithSchema($options['name'], $tableName); } /** * @param string $schema The table schema * @param string $tableName The table name * @return bool */ protected function hasTableWithSchema(string $schema, string $tableName): bool { $result = $this->fetchRow(sprintf( "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s'", $schema, $tableName )); return !empty($result); } /** * @inheritDoc */ public function createTable(Table $table, array $columns = [], array $indexes = []): void { // This method is based on the MySQL docs here: https://dev.mysql.com/doc/refman/5.1/en/create-index.html $defaultOptions = [ 'engine' => 'InnoDB', 'collation' => 'utf8mb4_unicode_ci', ]; $options = array_merge( $defaultOptions, array_intersect_key($this->getOptions(), $defaultOptions), $table->getOptions() ); // Add the default primary key if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { $options['id'] = 'id'; } if (isset($options['id']) && is_string($options['id'])) { // Handle id => "field_name" to support AUTO_INCREMENT $column = new Column(); $column->setName($options['id']) ->setType('integer') ->setOptions([ 'signed' => $options['signed'] ?? !FeatureFlags::$unsignedPrimaryKeys, 'identity' => true, ]); if (isset($options['limit'])) { $column->setLimit($options['limit']); } array_unshift($columns, $column); if (isset($options['primary_key']) && (array)$options['id'] !== (array)$options['primary_key']) { throw new InvalidArgumentException('You cannot enable an auto incrementing ID field and a primary key'); } $options['primary_key'] = $options['id']; } // open: process table options like collation etc // process table engine (default to InnoDB) $optionsStr = 'ENGINE = InnoDB'; if (isset($options['engine'])) { $optionsStr = sprintf('ENGINE = %s', $options['engine']); } // process table collation if (isset($options['collation'])) { $charset = explode('_', $options['collation']); $optionsStr .= sprintf(' CHARACTER SET %s', $charset[0]); $optionsStr .= sprintf(' COLLATE %s', $options['collation']); } // set the table comment if (isset($options['comment'])) { $optionsStr .= sprintf(' COMMENT=%s ', $this->getConnection()->quote($options['comment'])); } // set the table row format if (isset($options['row_format'])) { $optionsStr .= sprintf(' ROW_FORMAT=%s ', $options['row_format']); } $sql = 'CREATE TABLE '; $sql .= $this->quoteTableName($table->getName()) . ' ('; foreach ($columns as $column) { $sql .= $this->quoteColumnName($column->getName()) . ' ' . $this->getColumnSqlDefinition($column) . ', '; } // set the primary key(s) if (isset($options['primary_key'])) { $sql = rtrim($sql); $sql .= ' PRIMARY KEY ('; if (is_string($options['primary_key'])) { // handle primary_key => 'id' $sql .= $this->quoteColumnName($options['primary_key']); } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') $sql .= implode(',', array_map([$this, 'quoteColumnName'], (array)$options['primary_key'])); } $sql .= ')'; } else { $sql = substr(rtrim($sql), 0, -1); // no primary keys } // set the indexes foreach ($indexes as $index) { $sql .= ', ' . $this->getIndexSqlDefinition($index); } $sql .= ') ' . $optionsStr; $sql = rtrim($sql); // execute the sql $this->execute($sql); $this->addCreatedTable($table->getName()); } /** * {@inheritDoc} * * @throws \InvalidArgumentException */ protected function getChangePrimaryKeyInstructions(Table $table, $newColumns): AlterInstructions { $instructions = new AlterInstructions(); // Drop the existing primary key $primaryKey = $this->getPrimaryKey($table->getName()); if (!empty($primaryKey['columns'])) { $instructions->addAlter('DROP PRIMARY KEY'); } // Add the primary key(s) if (!empty($newColumns)) { $sql = 'ADD PRIMARY KEY ('; if (is_string($newColumns)) { // handle primary_key => 'id' $sql .= $this->quoteColumnName($newColumns); } elseif (is_array($newColumns)) { // handle primary_key => array('tag_id', 'resource_id') $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns)); } else { throw new InvalidArgumentException(sprintf( 'Invalid value for primary key: %s', json_encode($newColumns) )); } $sql .= ')'; $instructions->addAlter($sql); } return $instructions; } /** * @inheritDoc */ protected function getChangeCommentInstructions(Table $table, ?string $newComment): AlterInstructions { $instructions = new AlterInstructions(); // passing 'null' is to remove table comment $newComment = $newComment ?? ''; $sql = sprintf(' COMMENT=%s ', $this->getConnection()->quote($newComment)); $instructions->addAlter($sql); return $instructions; } /** * @inheritDoc */ protected function getRenameTableInstructions(string $tableName, string $newTableName): AlterInstructions { $this->updateCreatedTableName($tableName, $newTableName); $sql = sprintf( 'RENAME TABLE %s TO %s', $this->quoteTableName($tableName), $this->quoteTableName($newTableName) ); return new AlterInstructions([], [$sql]); } /** * @inheritDoc */ protected function getDropTableInstructions(string $tableName): AlterInstructions { $this->removeCreatedTable($tableName); $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName)); return new AlterInstructions([], [$sql]); } /** * @inheritDoc */ public function truncateTable(string $tableName): void { $sql = sprintf( 'TRUNCATE TABLE %s', $this->quoteTableName($tableName) ); $this->execute($sql); } /** * @inheritDoc */ public function getColumns(string $tableName): array { $columns = []; $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName))); foreach ($rows as $columnInfo) { $phinxType = $this->getPhinxType($columnInfo['Type']); $column = new Column(); $column->setName($columnInfo['Field']) ->setNull($columnInfo['Null'] !== 'NO') ->setType($phinxType['name']) ->setSigned(strpos($columnInfo['Type'], 'unsigned') === false) ->setLimit($phinxType['limit']) ->setScale($phinxType['scale']); if ($columnInfo['Extra'] === 'auto_increment') { $column->setIdentity(true); } if (isset($phinxType['values'])) { $column->setValues($phinxType['values']); } $default = $columnInfo['Default']; if ( is_string($default) && in_array( $column->getType(), array_merge( static::PHINX_TYPES_GEOSPATIAL, [static::PHINX_TYPE_BLOB, static::PHINX_TYPE_JSON, static::PHINX_TYPE_TEXT] ) ) ) { // The default that comes back from MySQL for these types prefixes the collation type and // surrounds the value with escaped single quotes, for example "_utf8mbf4\'abc\'", and so // this converts that then down to the default value of "abc" to correspond to what the user // would have specified in a migration. $default = preg_replace("/^_(?:[a-zA-Z0-9]+?)\\\'(.*)\\\'$/", '\1', $default); } $column->setDefault($default); $columns[] = $column; } return $columns; } /** * @inheritDoc */ public function hasColumn(string $tableName, string $columnName): bool { $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName))); foreach ($rows as $column) { if (strcasecmp($column['Field'], $columnName) === 0) { return true; } } return false; } /** * @inheritDoc */ protected function getAddColumnInstructions(Table $table, Column $column): AlterInstructions { $alter = sprintf( 'ADD %s %s', $this->quoteColumnName($column->getName()), $this->getColumnSqlDefinition($column) ); $alter .= $this->afterClause($column); return new AlterInstructions([$alter]); } /** * Exposes the MySQL syntax to arrange a column `FIRST`. * * @param \Phinx\Db\Table\Column $column The column being altered. * @return string The appropriate SQL fragment. */ protected function afterClause(Column $column): string { $after = $column->getAfter(); if (empty($after)) { return ''; } if ($after === self::FIRST) { return ' FIRST'; } return ' AFTER ' . $this->quoteColumnName($after); } /** * {@inheritDoc} * * @throws \InvalidArgumentException */ protected function getRenameColumnInstructions(string $tableName, string $columnName, string $newColumnName): AlterInstructions { $rows = $this->fetchAll(sprintf('SHOW FULL COLUMNS FROM %s', $this->quoteTableName($tableName))); foreach ($rows as $row) { if (strcasecmp($row['Field'], $columnName) === 0) { $null = $row['Null'] === 'NO' ? 'NOT NULL' : 'NULL'; $comment = isset($row['Comment']) ? ' COMMENT ' . '\'' . addslashes($row['Comment']) . '\'' : ''; $extra = ' ' . strtoupper($row['Extra']); if (($row['Default'] !== null)) { $extra .= $this->getDefaultValueDefinition($row['Default']); } $definition = $row['Type'] . ' ' . $null . $extra . $comment; $alter = sprintf( 'CHANGE COLUMN %s %s %s', $this->quoteColumnName($columnName), $this->quoteColumnName($newColumnName), $definition ); return new AlterInstructions([$alter]); } } throw new InvalidArgumentException(sprintf( "The specified column doesn't exist: " . $columnName )); } /** * @inheritDoc */ protected function getChangeColumnInstructions(string $tableName, string $columnName, Column $newColumn): AlterInstructions { $alter = sprintf( 'CHANGE %s %s %s%s', $this->quoteColumnName($columnName), $this->quoteColumnName($newColumn->getName()), $this->getColumnSqlDefinition($newColumn), $this->afterClause($newColumn) ); return new AlterInstructions([$alter]); } /** * @inheritDoc */ protected function getDropColumnInstructions(string $tableName, string $columnName): AlterInstructions { $alter = sprintf('DROP COLUMN %s', $this->quoteColumnName($columnName)); return new AlterInstructions([$alter]); } /** * Get an array of indexes from a particular table. * * @param string $tableName Table name * @return array */ protected function getIndexes(string $tableName): array { $indexes = []; $rows = $this->fetchAll(sprintf('SHOW INDEXES FROM %s', $this->quoteTableName($tableName))); foreach ($rows as $row) { if (!isset($indexes[$row['Key_name']])) { $indexes[$row['Key_name']] = ['columns' => []]; } $indexes[$row['Key_name']]['columns'][] = strtolower($row['Column_name']); } return $indexes; } /** * @inheritDoc */ public function hasIndex(string $tableName, $columns): bool { if (is_string($columns)) { $columns = [$columns]; // str to array } $columns = array_map('strtolower', $columns); $indexes = $this->getIndexes($tableName); foreach ($indexes as $index) { if ($columns == $index['columns']) { return true; } } return false; } /** * @inheritDoc */ public function hasIndexByName(string $tableName, string $indexName): bool { $indexes = $this->getIndexes($tableName); foreach ($indexes as $name => $index) { if ($name === $indexName) { return true; } } return false; } /** * @inheritDoc */ protected function getAddIndexInstructions(Table $table, Index $index): AlterInstructions { $instructions = new AlterInstructions(); if ($index->getType() === Index::FULLTEXT) { // Must be executed separately // SQLSTATE[HY000]: General error: 1795 InnoDB presently supports one FULLTEXT index creation at a time $alter = sprintf( 'ALTER TABLE %s ADD %s', $this->quoteTableName($table->getName()), $this->getIndexSqlDefinition($index) ); $instructions->addPostStep($alter); } else { $alter = sprintf( 'ADD %s', $this->getIndexSqlDefinition($index) ); $instructions->addAlter($alter); } return $instructions; } /** * {@inheritDoc} * * @throws \InvalidArgumentException */ protected function getDropIndexByColumnsInstructions(string $tableName, $columns): AlterInstructions { if (is_string($columns)) { $columns = [$columns]; // str to array } $indexes = $this->getIndexes($tableName); $columns = array_map('strtolower', $columns); foreach ($indexes as $indexName => $index) { if ($columns == $index['columns']) { return new AlterInstructions([sprintf( 'DROP INDEX %s', $this->quoteColumnName($indexName) )]); } } throw new InvalidArgumentException(sprintf( "The specified index on columns '%s' does not exist", implode(',', $columns) )); } /** * {@inheritDoc} * * @throws \InvalidArgumentException */ protected function getDropIndexByNameInstructions(string $tableName, $indexName): AlterInstructions { $indexes = $this->getIndexes($tableName); foreach ($indexes as $name => $index) { if ($name === $indexName) { return new AlterInstructions([sprintf( 'DROP INDEX %s', $this->quoteColumnName($indexName) )]); } } throw new InvalidArgumentException(sprintf( "The specified index name '%s' does not exist", $indexName )); } /** * @inheritDoc */ public function hasPrimaryKey(string $tableName, $columns, ?string $constraint = null): bool { $primaryKey = $this->getPrimaryKey($tableName); if (empty($primaryKey['constraint'])) { return false; } if ($constraint) { return $primaryKey['constraint'] === $constraint; } else { if (is_string($columns)) { $columns = [$columns]; // str to array } $missingColumns = array_diff($columns, $primaryKey['columns']); return empty($missingColumns); } } /** * Get the primary key from a particular table. * * @param string $tableName Table name * @return array */ public function getPrimaryKey(string $tableName): array { $options = $this->getOptions(); $rows = $this->fetchAll(sprintf( "SELECT k.CONSTRAINT_NAME, k.COLUMN_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS t JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k USING(CONSTRAINT_NAME,TABLE_SCHEMA,TABLE_NAME) WHERE t.CONSTRAINT_TYPE='PRIMARY KEY' AND t.TABLE_SCHEMA='%s' AND t.TABLE_NAME='%s'", $options['name'], $tableName )); $primaryKey = [ 'columns' => [], ]; foreach ($rows as $row) { $primaryKey['constraint'] = $row['CONSTRAINT_NAME']; $primaryKey['columns'][] = $row['COLUMN_NAME']; } return $primaryKey; } /** * @inheritDoc */ public function hasForeignKey(string $tableName, $columns, ?string $constraint = null): bool { if (is_string($columns)) { $columns = [$columns]; // str to array } $foreignKeys = $this->getForeignKeys($tableName); if ($constraint) { if (isset($foreignKeys[$constraint])) { return !empty($foreignKeys[$constraint]); } return false; } foreach ($foreignKeys as $key) { if ($columns == $key['columns']) { return true; } } return false; } /** * Get an array of foreign keys from a particular table. * * @param string $tableName Table name * @return array */ protected function getForeignKeys(string $tableName): array { if (strpos($tableName, '.') !== false) { [$schema, $tableName] = explode('.', $tableName); } $foreignKeys = []; $rows = $this->fetchAll(sprintf( "SELECT CONSTRAINT_NAME, CONCAT(TABLE_SCHEMA, '.', TABLE_NAME) AS TABLE_NAME, COLUMN_NAME, CONCAT(REFERENCED_TABLE_SCHEMA, '.', REFERENCED_TABLE_NAME) AS REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME IS NOT NULL AND TABLE_SCHEMA = %s AND TABLE_NAME = '%s' ORDER BY POSITION_IN_UNIQUE_CONSTRAINT", empty($schema) ? 'DATABASE()' : "'$schema'", $tableName )); foreach ($rows as $row) { $foreignKeys[$row['CONSTRAINT_NAME']]['table'] = $row['TABLE_NAME']; $foreignKeys[$row['CONSTRAINT_NAME']]['columns'][] = $row['COLUMN_NAME']; $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_table'] = $row['REFERENCED_TABLE_NAME']; $foreignKeys[$row['CONSTRAINT_NAME']]['referenced_columns'][] = $row['REFERENCED_COLUMN_NAME']; } return $foreignKeys; } /** * @inheritDoc */ protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreignKey): AlterInstructions { $alter = sprintf( 'ADD %s', $this->getForeignKeySqlDefinition($foreignKey) ); return new AlterInstructions([$alter]); } /** * @inheritDoc */ protected function getDropForeignKeyInstructions(string $tableName, string $constraint): AlterInstructions { $alter = sprintf( 'DROP FOREIGN KEY %s', $constraint ); return new AlterInstructions([$alter]); } /** * {@inheritDoc} * * @throws \InvalidArgumentException */ protected function getDropForeignKeyByColumnsInstructions(string $tableName, array $columns): AlterInstructions { $instructions = new AlterInstructions(); foreach ($columns as $column) { $rows = $this->fetchAll(sprintf( "SELECT CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL AND TABLE_NAME = '%s' AND COLUMN_NAME = '%s' ORDER BY POSITION_IN_UNIQUE_CONSTRAINT", $tableName, $column )); foreach ($rows as $row) { $instructions->merge($this->getDropForeignKeyInstructions($tableName, $row['CONSTRAINT_NAME'])); } } if (empty($instructions->getAlterParts())) { throw new InvalidArgumentException(sprintf( "Not foreign key on columns '%s' exist", implode(',', $columns) )); } return $instructions; } /** * {@inheritDoc} * * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException */ public function getSqlType($type, ?int $limit = null): array { switch ($type) { case static::PHINX_TYPE_FLOAT: case static::PHINX_TYPE_DOUBLE: case static::PHINX_TYPE_DECIMAL: case static::PHINX_TYPE_DATE: case static::PHINX_TYPE_ENUM: case static::PHINX_TYPE_SET: case static::PHINX_TYPE_JSON: // Geospatial database types case static::PHINX_TYPE_GEOMETRY: case static::PHINX_TYPE_POINT: case static::PHINX_TYPE_LINESTRING: case static::PHINX_TYPE_POLYGON: return ['name' => $type]; case static::PHINX_TYPE_DATETIME: case static::PHINX_TYPE_TIMESTAMP: case static::PHINX_TYPE_TIME: return ['name' => $type, 'limit' => $limit]; case static::PHINX_TYPE_STRING: return ['name' => 'varchar', 'limit' => $limit ?: 255]; case static::PHINX_TYPE_CHAR: return ['name' => 'char', 'limit' => $limit ?: 255]; case static::PHINX_TYPE_TEXT: if ($limit) { $sizes = [ // Order matters! Size must always be tested from longest to shortest! 'longtext' => static::TEXT_LONG, 'mediumtext' => static::TEXT_MEDIUM, 'text' => static::TEXT_REGULAR, 'tinytext' => static::TEXT_SMALL, ]; foreach ($sizes as $name => $length) { if ($limit >= $length) { return ['name' => $name]; } } } return ['name' => 'text']; case static::PHINX_TYPE_BINARY: if ($limit === null) { $limit = 255; } if ($limit > 255) { return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit); } return ['name' => 'binary', 'limit' => $limit]; case static::PHINX_TYPE_BINARYUUID: return ['name' => 'binary', 'limit' => 16]; case static::PHINX_TYPE_VARBINARY: if ($limit === null) { $limit = 255; } if ($limit > 255) { return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit); } return ['name' => 'varbinary', 'limit' => $limit]; case static::PHINX_TYPE_BLOB: if ($limit !== null) { // Rework this part as the choosen types were always UNDER the required length $sizes = [ 'tinyblob' => static::BLOB_SMALL, 'blob' => static::BLOB_REGULAR, 'mediumblob' => static::BLOB_MEDIUM, ]; foreach ($sizes as $name => $length) { if ($limit <= $length) { return ['name' => $name]; } } // For more length requirement, the longblob is used return ['name' => 'longblob']; } // If not limit is provided, fallback on blob return ['name' => 'blob']; case static::PHINX_TYPE_TINYBLOB: // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_TINY); case static::PHINX_TYPE_MEDIUMBLOB: // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_MEDIUM); case static::PHINX_TYPE_LONGBLOB: // Automatically reprocess blob type to ensure that correct blob subtype is selected given provided limit return $this->getSqlType(static::PHINX_TYPE_BLOB, $limit ?: static::BLOB_LONG); case static::PHINX_TYPE_BIT: return ['name' => 'bit', 'limit' => $limit ?: 64]; case static::PHINX_TYPE_BIG_INTEGER: if ($limit === static::INT_BIG) { $limit = static::INT_DISPLAY_BIG; } return ['name' => 'bigint', 'limit' => $limit ?: 20]; case static::PHINX_TYPE_MEDIUM_INTEGER: if ($limit === static::INT_MEDIUM) { $limit = static::INT_DISPLAY_MEDIUM; } return ['name' => 'mediumint', 'limit' => $limit ?: 8]; case static::PHINX_TYPE_SMALL_INTEGER: if ($limit === static::INT_SMALL) { $limit = static::INT_DISPLAY_SMALL; } return ['name' => 'smallint', 'limit' => $limit ?: 6]; case static::PHINX_TYPE_TINY_INTEGER: if ($limit === static::INT_TINY) { $limit = static::INT_DISPLAY_TINY; } return ['name' => 'tinyint', 'limit' => $limit ?: 4]; case static::PHINX_TYPE_INTEGER: if ($limit && $limit >= static::INT_TINY) { $sizes = [ // Order matters! Size must always be tested from longest to shortest! 'bigint' => static::INT_BIG, 'int' => static::INT_REGULAR, 'mediumint' => static::INT_MEDIUM, 'smallint' => static::INT_SMALL, 'tinyint' => static::INT_TINY, ]; $limits = [ 'tinyint' => static::INT_DISPLAY_TINY, 'smallint' => static::INT_DISPLAY_SMALL, 'mediumint' => static::INT_DISPLAY_MEDIUM, 'int' => static::INT_DISPLAY_REGULAR, 'bigint' => static::INT_DISPLAY_BIG, ]; foreach ($sizes as $name => $length) { if ($limit >= $length) { $def = ['name' => $name]; if (isset($limits[$name])) { $def['limit'] = $limits[$name]; } return $def; } } } elseif (!$limit) { $limit = static::INT_DISPLAY_REGULAR; } return ['name' => 'int', 'limit' => $limit]; case static::PHINX_TYPE_BOOLEAN: return ['name' => 'tinyint', 'limit' => 1]; case static::PHINX_TYPE_UUID: return ['name' => 'char', 'limit' => 36]; case static::PHINX_TYPE_YEAR: if (!$limit || in_array($limit, [2, 4])) { $limit = 4; } return ['name' => 'year', 'limit' => $limit]; default: throw new UnsupportedColumnTypeException('Column type "' . $type . '" is not supported by MySQL.'); } } /** * Returns Phinx type by SQL type * * @internal param string $sqlType SQL type * @param string $sqlTypeDef SQL Type definition * @throws \Phinx\Db\Adapter\UnsupportedColumnTypeException * @return array Phinx type */ public function getPhinxType($sqlTypeDef) { $matches = []; if (!preg_match('/^([\w]+)(\(([\d]+)*(,([\d]+))*\))*(.+)*$/', $sqlTypeDef, $matches)) { throw new UnsupportedColumnTypeException('Column type "' . $sqlTypeDef . '" is not supported by MySQL.'); } $limit = null; $scale = null; $type = $matches[1]; if (count($matches) > 2) { $limit = $matches[3] ? (int)$matches[3] : null; } if (count($matches) > 4) { $scale = (int)$matches[5]; } if ($type === 'tinyint' && $limit === 1) { $type = static::PHINX_TYPE_BOOLEAN; $limit = null; } switch ($type) { case 'varchar': $type = static::PHINX_TYPE_STRING; if ($limit === 255) { $limit = null; } break; case 'char': $type = static::PHINX_TYPE_CHAR; if ($limit === 255) { $limit = null; } if ($limit === 36) { $type = static::PHINX_TYPE_UUID; } break; case 'tinyint': $type = static::PHINX_TYPE_TINY_INTEGER; break; case 'smallint': $type = static::PHINX_TYPE_SMALL_INTEGER; break; case 'mediumint': $type = static::PHINX_TYPE_MEDIUM_INTEGER; break; case 'int': $type = static::PHINX_TYPE_INTEGER; break; case 'bigint': $type = static::PHINX_TYPE_BIG_INTEGER; break; case 'bit': $type = static::PHINX_TYPE_BIT; if ($limit === 64) { $limit = null; } break; case 'blob': $type = static::PHINX_TYPE_BLOB; $limit = static::BLOB_REGULAR; break; case 'tinyblob': $type = static::PHINX_TYPE_TINYBLOB; $limit = static::BLOB_TINY; break; case 'mediumblob': $type = static::PHINX_TYPE_MEDIUMBLOB; $limit = static::BLOB_MEDIUM; break; case 'longblob': $type = static::PHINX_TYPE_LONGBLOB; $limit = static::BLOB_LONG; break; case 'tinytext': $type = static::PHINX_TYPE_TEXT; $limit = static::TEXT_TINY; break; case 'mediumtext': $type = static::PHINX_TYPE_TEXT; $limit = static::TEXT_MEDIUM; break; case 'longtext': $type = static::PHINX_TYPE_TEXT; $limit = static::TEXT_LONG; break; case 'binary': if ($limit === null) { $limit = 255; } if ($limit > 255) { $type = static::PHINX_TYPE_BLOB; break; } if ($limit === 16) { $type = static::PHINX_TYPE_BINARYUUID; } break; } try { // Call this to check if parsed type is supported. $this->getSqlType($type, $limit); } catch (UnsupportedColumnTypeException $e) { $type = Literal::from($type); } $phinxType = [ 'name' => $type, 'limit' => $limit, 'scale' => $scale, ]; if ($type === static::PHINX_TYPE_ENUM || $type === static::PHINX_TYPE_SET) { $values = trim($matches[6], '()'); $phinxType['values'] = []; $opened = false; $escaped = false; $wasEscaped = false; $value = ''; $valuesLength = strlen($values); for ($i = 0; $i < $valuesLength; $i++) { $char = $values[$i]; if ($char === "'" && !$opened) { $opened = true; } elseif ( !$escaped && ($i + 1) < $valuesLength && ( $char === "'" && $values[$i + 1] === "'" || $char === '\\' && $values[$i + 1] === '\\' ) ) { $escaped = true; } elseif ($char === "'" && $opened && !$escaped) { $phinxType['values'][] = $value; $value = ''; $opened = false; } elseif (($char === "'" || $char === '\\') && $opened && $escaped) { $value .= $char; $escaped = false; $wasEscaped = true; } elseif ($opened) { if ($values[$i - 1] === '\\' && !$wasEscaped) { if ($char === 'n') { $char = "\n"; } elseif ($char === 'r') { $char = "\r"; } elseif ($char === 't') { $char = "\t"; } if ($values[$i] !== $char) { $value = substr($value, 0, strlen($value) - 1); } } $value .= $char; $wasEscaped = false; } } } return $phinxType; } /** * @inheritDoc */ public function createDatabase(string $name, array $options = []): void { $charset = $options['charset'] ?? 'utf8'; if (isset($options['collation'])) { $this->execute(sprintf( 'CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s` COLLATE `%s`', $name, $charset, $options['collation'] )); } else { $this->execute(sprintf('CREATE DATABASE `%s` DEFAULT CHARACTER SET `%s`', $name, $charset)); } } /** * @inheritDoc */ public function hasDatabase(string $name): bool { $rows = $this->fetchAll( sprintf( 'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = \'%s\'', $name ) ); foreach ($rows as $row) { if (!empty($row)) { return true; } } return false; } /** * @inheritDoc */ public function dropDatabase(string $name): void { $this->execute(sprintf('DROP DATABASE IF EXISTS `%s`', $name)); $this->createdTables = []; } /** * Gets the MySQL Column Definition for a Column object. * * @param \Phinx\Db\Table\Column $column Column * @return string */ protected function getColumnSqlDefinition(Column $column): string { if ($column->getType() instanceof Literal) { $def = (string)$column->getType(); } else { $sqlType = $this->getSqlType($column->getType(), $column->getLimit()); $def = strtoupper($sqlType['name']); } if ($column->getPrecision() && $column->getScale()) { $def .= '(' . $column->getPrecision() . ',' . $column->getScale() . ')'; } elseif (isset($sqlType['limit'])) { $def .= '(' . $sqlType['limit'] . ')'; } $values = $column->getValues(); if ($values && is_array($values)) { $def .= '(' . implode(', ', array_map(function ($value) { // we special case NULL as it's not actually allowed an enum value, // and we want MySQL to issue an error on the create statement, but // quote coerces it to an empty string, which will not error return $value === null ? 'NULL' : $this->getConnection()->quote($value); }, $values)) . ')'; } $def .= $column->getEncoding() ? ' CHARACTER SET ' . $column->getEncoding() : ''; $def .= $column->getCollation() ? ' COLLATE ' . $column->getCollation() : ''; $def .= !$column->isSigned() && isset($this->signedColumnTypes[$column->getType()]) ? ' unsigned' : ''; $def .= $column->isNull() ? ' NULL' : ' NOT NULL'; if ( version_compare($this->getAttribute(\PDO::ATTR_SERVER_VERSION), '8', '>=') && in_array($column->getType(), static::PHINX_TYPES_GEOSPATIAL) && !is_null($column->getSrid()) ) { $def .= " SRID {$column->getSrid()}"; } $def .= $column->isIdentity() ? ' AUTO_INCREMENT' : ''; $default = $column->getDefault(); // MySQL 8 supports setting default for the following tested types, but only if they are "cast as expressions" if ( version_compare($this->getAttribute(\PDO::ATTR_SERVER_VERSION), '8', '>=') && is_string($default) && in_array( $column->getType(), array_merge( static::PHINX_TYPES_GEOSPATIAL, [static::PHINX_TYPE_BLOB, static::PHINX_TYPE_JSON, static::PHINX_TYPE_TEXT] ) ) ) { $default = Literal::from('(' . $this->getConnection()->quote($column->getDefault()) . ')'); } $def .= $this->getDefaultValueDefinition($default, $column->getType()); if ($column->getComment()) { $def .= ' COMMENT ' . $this->getConnection()->quote($column->getComment()); } if ($column->getUpdate()) { $def .= ' ON UPDATE ' . $column->getUpdate(); } return $def; } /** * Gets the MySQL Index Definition for an Index object. * * @param \Phinx\Db\Table\Index $index Index * @return string */ protected function getIndexSqlDefinition(Index $index): string { $def = ''; $limit = ''; if ($index->getType() === Index::UNIQUE) { $def .= ' UNIQUE'; } if ($index->getType() === Index::FULLTEXT) { $def .= ' FULLTEXT'; } $def .= ' KEY'; if (is_string($index->getName())) { $def .= ' `' . $index->getName() . '`'; } $columnNames = $index->getColumns(); $order = $index->getOrder() ?? []; $columnNames = array_map(function ($columnName) use ($order) { $ret = '`' . $columnName . '`'; if (isset($order[$columnName])) { $ret .= ' ' . $order[$columnName]; } return $ret; }, $columnNames); if (!is_array($index->getLimit())) { if ($index->getLimit()) { $limit = '(' . $index->getLimit() . ')'; } $def .= ' (' . implode(',', $columnNames) . $limit . ')'; } else { $columns = $index->getColumns(); $limits = $index->getLimit(); $def .= ' ('; foreach ($columns as $column) { $limit = !isset($limits[$column]) || $limits[$column] <= 0 ? '' : '(' . $limits[$column] . ')'; $columnSort = isset($order[$column]) ?? ''; $def .= '`' . $column . '`' . $limit . ' ' . $columnSort . ', '; } $def = rtrim($def, ', '); $def .= ' )'; } return $def; } /** * Gets the MySQL Foreign Key Definition for an ForeignKey object. * * @param \Phinx\Db\Table\ForeignKey $foreignKey Foreign key * @return string */ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string { $def = ''; if ($foreignKey->getConstraint()) { $def .= ' CONSTRAINT ' . $this->quoteColumnName($foreignKey->getConstraint()); } $columnNames = []; foreach ($foreignKey->getColumns() as $column) { $columnNames[] = $this->quoteColumnName($column); } $def .= ' FOREIGN KEY (' . implode(',', $columnNames) . ')'; $refColumnNames = []; foreach ($foreignKey->getReferencedColumns() as $column) { $refColumnNames[] = $this->quoteColumnName($column); } $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()->getName()) . ' (' . implode(',', $refColumnNames) . ')'; if ($foreignKey->getOnDelete()) { $def .= ' ON DELETE ' . $foreignKey->getOnDelete(); } if ($foreignKey->getOnUpdate()) { $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate(); } return $def; } /** * Describes a database table. This is a MySQL adapter specific method. * * @param string $tableName Table name * @return array */ public function describeTable(string $tableName): array { $options = $this->getOptions(); // mysql specific $sql = sprintf( "SELECT * FROM information_schema.tables WHERE table_schema = '%s' AND table_name = '%s'", $options['name'], $tableName ); $table = $this->fetchRow($sql); return $table !== false ? $table : []; } /** * Returns MySQL column types (inherited and MySQL specified). * * @return string[] */ public function getColumnTypes(): array { return array_merge(parent::getColumnTypes(), static::$specificColumnTypes); } /** * @inheritDoc */ public function getDecoratedConnection(): Connection { $options = $this->getOptions(); $options = [ 'username' => $options['user'] ?? null, 'password' => $options['pass'] ?? null, 'database' => $options['name'], 'quoteIdentifiers' => true, ] + $options; $driver = new MysqlDriver($options); $driver->setConnection($this->connection); return new Connection(['driver' => $driver] + $options); } }