propelorm/Propel2

View on GitHub
src/Propel/Generator/Platform/MysqlPlatform.php

Summary

Maintainability
F
4 days
Test Coverage
<?php

/**
 * MIT License. This file is part of the Propel package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Propel\Generator\Platform;

use PDO;
use Propel\Generator\Config\GeneratorConfigInterface;
use Propel\Generator\Exception\EngineException;
use Propel\Generator\Model\Column;
use Propel\Generator\Model\Database;
use Propel\Generator\Model\Diff\ColumnDiff;
use Propel\Generator\Model\Diff\DatabaseDiff;
use Propel\Generator\Model\Domain;
use Propel\Generator\Model\ForeignKey;
use Propel\Generator\Model\Index;
use Propel\Generator\Model\PropelTypes;
use Propel\Generator\Model\Table;
use Propel\Generator\Model\Unique;
use Propel\Generator\Platform\Util\MysqlUuidMigrationBuilder;

/**
 * MySql PlatformInterface implementation.
 *
 * @author Hans Lellelid <hans@xmpl.org> (Propel)
 * @author Martin Poeschl <mpoeschl@marmot.at> (Torque)
 */
class MysqlPlatform extends DefaultPlatform
{
    /**
     * @var string
     */
    protected $tableEngineKeyword = 'ENGINE';

    /**
     * @var string
     */
    protected $defaultTableEngine = 'InnoDB';

    /**
     * @var string|null
     */
    protected $serverVersion;

    /**
     * @var bool
     */
    protected $useUuidNativeType = false;

    /**
     * Initializes db specific domain mapping.
     *
     * @return void
     */
    protected function initializeTypeMap(): void
    {
        parent::initializeTypeMap();
        $this->setSchemaDomainMapping(new Domain(PropelTypes::BOOLEAN, 'TINYINT', 1));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::NUMERIC, 'DECIMAL'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::LONGVARCHAR, 'TEXT'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::BINARY, 'BINARY'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::VARBINARY, 'MEDIUMBLOB'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::LONGVARBINARY, 'LONGBLOB'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::CLOB, 'LONGTEXT'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::OBJECT, 'MEDIUMBLOB'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::PHP_ARRAY, 'TEXT'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::ENUM, 'TINYINT'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::SET, 'INT'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::REAL, 'DOUBLE'));
        $this->setSchemaDomainMapping(new Domain(PropelTypes::UUID_BINARY, 'BINARY', 16));

        $this->setUuidTypeMapping();
    }

    /**
     * @param \Propel\Generator\Config\GeneratorConfigInterface $generatorConfig
     *
     * @return void
     */
    public function setGeneratorConfig(GeneratorConfigInterface $generatorConfig): void
    {
        parent::setGeneratorConfig($generatorConfig);

        $mysqlConfig = $generatorConfig->get()['database']['adapters']['mysql'];

        $defaultTableEngine = $mysqlConfig['tableType'];
        if ($defaultTableEngine) {
            $this->defaultTableEngine = $defaultTableEngine;
        }

        $tableEngineKeyword = $mysqlConfig['tableEngineKeyword'];
        if ($tableEngineKeyword) {
            $this->tableEngineKeyword = $tableEngineKeyword;
        }

        $uuidColumnType = $mysqlConfig['uuidColumnType'];
        if ($uuidColumnType) {
            $enable = strtolower($uuidColumnType) === 'native';
            $this->setUuidNativeType($enable);
        }
    }

    /**
     * @param bool $enable
     *
     * @return void
     */
    public function setUuidNativeType(bool $enable): void
    {
        $this->useUuidNativeType = $enable;
        $this->setUuidTypeMapping();
    }

    /**
     * Set column type for UUIDs according to MysqlPlatform::useUuidNativeType.
     *
     * Currently, only MariaDB has a native UUID type.
     *
     * @return void
     */
    protected function setUuidTypeMapping(): void
    {
        $domain = ($this->useUuidNativeType)
            ? new Domain(PropelTypes::UUID, 'UUID')
            : $this->schemaDomainMap[PropelTypes::UUID_BINARY];

        $this->schemaDomainMap[PropelTypes::UUID] = $domain;
    }

    /**
     * @param string $tableEngineKeyword
     *
     * @return void
     */
    public function setTableEngineKeyword(string $tableEngineKeyword): void
    {
        $this->tableEngineKeyword = $tableEngineKeyword;
    }

    /**
     * @return string
     */
    public function getTableEngineKeyword(): string
    {
        return $this->tableEngineKeyword;
    }

    /**
     * @param string $defaultTableEngine
     *
     * @return void
     */
    public function setDefaultTableEngine(string $defaultTableEngine): void
    {
        $this->defaultTableEngine = $defaultTableEngine;
    }

    /**
     * @return string
     */
    public function getDefaultTableEngine(): string
    {
        return $this->defaultTableEngine;
    }

    /**
     * @return string
     */
    public function getAutoIncrement(): string
    {
        return 'AUTO_INCREMENT';
    }

    /**
     * @return int
     */
    public function getMaxColumnNameLength(): int
    {
        return 64;
    }

    /**
     * @return bool
     */
    public function supportsNativeDeleteTrigger(): bool
    {
        return strtolower($this->getDefaultTableEngine()) === 'innodb';
    }

    /**
     * @return bool
     */
    public function supportsIndexSize(): bool
    {
        return true;
    }

    /**
     * @param \Propel\Generator\Model\Table $table
     *
     * @return bool
     */
    public function supportsForeignKeys(Table $table): bool
    {
        $vendorSpecific = $table->getVendorInfoForType('mysql');
        if ($vendorSpecific->hasParameter('Type')) {
            $mysqlTableType = $vendorSpecific->getParameter('Type');
        } elseif ($vendorSpecific->hasParameter('Engine')) {
            $mysqlTableType = $vendorSpecific->getParameter('Engine');
        } else {
            $mysqlTableType = $this->getDefaultTableEngine();
        }

        return strtolower($mysqlTableType) === 'innodb';
    }

    /**
     * @param \Propel\Generator\Model\Database $database
     *
     * @return string
     */
    public function getAddTablesDDL(Database $database): string
    {
        $ret = '';
        foreach ($database->getTablesForSql() as $table) {
            $ret .= $this->getCommentBlockDDL($table->getName());
            $ret .= $this->getDropTableDDL($table);
            $ret .= $this->getAddTableDDL($table);
        }
        if ($ret) {
            $ret = $this->getBeginDDL() . $ret . $this->getEndDDL();
        }

        return $ret;
    }

    /**
     * @return string
     */
    public function getBeginDDL(): string
    {
        return "
# This is a fix for InnoDB in MySQL >= 4.1.x
# It \"suspends judgement\" for fkey relationships until are tables are set.
SET FOREIGN_KEY_CHECKS = 0;
";
    }

    /**
     * @return string
     */
    public function getEndDDL(): string
    {
        return "
# This restores the fkey checks, after having unset them earlier
SET FOREIGN_KEY_CHECKS = 1;
";
    }

    /**
     * Returns the SQL for the primary key of a Table object
     *
     * @param \Propel\Generator\Model\Table $table
     *
     * @return string
     */
    public function getPrimaryKeyDDL(Table $table): string
    {
        if ($table->hasPrimaryKey()) {
            $keys = $table->getPrimaryKey();

            //MySQL throws an 'Incorrect table definition; there can be only one auto column and it must be defined as a key'
            //if the primary key consists of multiple columns and if the first is not the autoIncrement one. So
            //this push the autoIncrement column to the first position if its not already.
            $autoIncrement = $table->getAutoIncrementPrimaryKey();
            if ($autoIncrement && $keys[0] != $autoIncrement) {
                $idx = array_search($autoIncrement, $keys);
                if ($idx !== false) {
                    unset($keys[$idx]);
                    array_unshift($keys, $autoIncrement);
                }
            }

            return 'PRIMARY KEY (' . $this->getColumnListDDL($keys) . ')';
        }

        return '';
    }

    /**
     * @param \Propel\Generator\Model\Table $table
     *
     * @return string
     */
    public function getAddTableDDL(Table $table): string
    {
        $lines = [];

        foreach ($table->getColumns() as $column) {
            $lines[] = $this->getColumnDDL($column);
        }

        if ($table->hasPrimaryKey()) {
            $lines[] = $this->getPrimaryKeyDDL($table);
        }

        foreach ($table->getUnices() as $unique) {
            $lines[] = $this->getUniqueDDL($unique);
        }

        foreach ($table->getIndices() as $index) {
            $lines[] = $this->getIndexDDL($index);
        }

        if ($this->supportsForeignKeys($table)) {
            foreach ($table->getForeignKeys() as $foreignKey) {
                if ($foreignKey->isSkipSql() || $foreignKey->isPolymorphic()) {
                    continue;
                }
                $lines[] = str_replace("
    ", "
        ", $this->getForeignKeyDDL($foreignKey));
            }
        }

        $vendorSpecific = $table->getVendorInfoForType('mysql');
        if ($vendorSpecific->hasParameter('Type')) {
            $mysqlTableType = $vendorSpecific->getParameter('Type');
        } elseif ($vendorSpecific->hasParameter('Engine')) {
            $mysqlTableType = $vendorSpecific->getParameter('Engine');
        } else {
            $mysqlTableType = $this->getDefaultTableEngine();
        }

        $tableOptions = $this->getTableOptions($table);

        if ($table->getDescription()) {
            $tableOptions[] = 'COMMENT=' . $this->quote($table->getDescription());
        }

        $tableOptions = $tableOptions ? ' ' . implode(' ', $tableOptions) : '';
        $sep = ",
    ";

        $pattern = "
CREATE TABLE %s
(
    %s
) %s=%s%s;
";

        return sprintf(
            $pattern,
            $this->quoteIdentifier($table->getName()),
            implode($sep, $lines),
            $this->getTableEngineKeyword(),
            $mysqlTableType,
            $tableOptions,
        );
    }

    /**
     * @param \Propel\Generator\Model\Table $table
     *
     * @return array<string>
     */
    protected function getTableOptions(Table $table): array
    {
        $vi = $table->getVendorInfoForType('mysql');
        $tableOptions = [];
        // List of supported table options
        // see http://dev.mysql.com/doc/refman/5.5/en/create-table.html
        $supportedOptions = [
            'AutoIncrement' => 'AUTO_INCREMENT',
            'AvgRowLength' => 'AVG_ROW_LENGTH',
            'Charset' => 'CHARACTER SET',
            'Checksum' => 'CHECKSUM',
            'Collate' => 'COLLATE',
            'Connection' => 'CONNECTION',
            'DataDirectory' => 'DATA DIRECTORY',
            'Delay_key_write' => 'DELAY_KEY_WRITE',
            'DelayKeyWrite' => 'DELAY_KEY_WRITE',
            'IndexDirectory' => 'INDEX DIRECTORY',
            'InsertMethod' => 'INSERT_METHOD',
            'KeyBlockSize' => 'KEY_BLOCK_SIZE',
            'MaxRows' => 'MAX_ROWS',
            'MinRows' => 'MIN_ROWS',
            'Pack_Keys' => 'PACK_KEYS',
            'PackKeys' => 'PACK_KEYS',
            'RowFormat' => 'ROW_FORMAT',
            'Union' => 'UNION',
        ];

        $noQuotedValue = array_flip([
            'InsertMethod',
            'Pack_Keys',
            'PackKeys',
            'RowFormat',
        ]);

        foreach ($supportedOptions as $name => $sqlName) {
            $parameterValue = null;

            if ($vi->hasParameter($name)) {
                $parameterValue = $vi->getParameter($name);
            } elseif ($vi->hasParameter($sqlName)) {
                $parameterValue = $vi->getParameter($sqlName);
            }

            // if we have a param value, then parse it out
            if ($parameterValue !== null) {
                // if the value is numeric or is parameter is in $noQuotedValue, then there is no need for quotes
                if (!is_numeric($parameterValue) && !isset($noQuotedValue[$name])) {
                    $parameterValue = $this->quote($parameterValue);
                }

                $tableOptions[] = sprintf('%s=%s', $sqlName, $parameterValue);
            }
        }

        return $tableOptions;
    }

    /**
     * @param \Propel\Generator\Model\Table $table
     *
     * @return string
     */
    public function getDropTableDDL(Table $table): string
    {
        return "
DROP TABLE IF EXISTS " . $this->quoteIdentifier($table->getName()) . ";
";
    }

    /**
     * @param \Propel\Generator\Model\Column $col
     *
     * @throws \Propel\Generator\Exception\EngineException
     *
     * @return string
     */
    public function getColumnDDL(Column $col): string
    {
        $domain = $col->getDomain();
        $sqlType = $domain->getSqlType();
        $notNullString = $this->getNullString($col->isNotNull());
        $defaultSetting = $this->getColumnDefaultValueDDL($col);

        // Special handling of TIMESTAMP/DATETIME types ...
        // See: http://propel.phpdb.org/trac/ticket/538
        if ($sqlType === 'DATETIME') {
            $def = $domain->getDefaultValue();
            if ($def && $def->isExpression()) {
                // DATETIME values can only have constant expressions
                $sqlType = 'TIMESTAMP';
            }
        } elseif ($sqlType === 'DATE') {
            $def = $domain->getDefaultValue();
            if ($def && $def->isExpression()) {
                throw new EngineException('DATE columns cannot have default *expressions* in MySQL.');
            }
        } elseif ($sqlType === 'TEXT' || $sqlType === 'BLOB') {
            if ($domain->getDefaultValue()) {
                throw new EngineException('BLOB and TEXT columns cannot have DEFAULT values. in MySQL.');
            }
        }

        $ddl = [$this->quoteIdentifier($col->getName())];
        $ddl[] = $this->getSqlTypeExpression($col);

        $colinfo = $col->getVendorInfoForType($this->getDatabaseType());
        if ($colinfo->hasParameter('Unsigned')) {
            $unsigned = $colinfo->getParameter('Unsigned');
            switch (strtoupper($unsigned)) {
                case 'FALSE':
                    break;
                case 'TRUE':
                    $ddl[] = 'UNSIGNED';

                    break;
                default:
                    throw new EngineException('Unexpected value "' . $unsigned . '" for MySQL vendor column parameter "Unsigned", expecting "true" or "false".');
            }
        }

        if ($colinfo->hasParameter('Charset')) {
            $ddl[] = 'CHARACTER SET ' . $this->quote($colinfo->getParameter('Charset'));
        }
        if ($colinfo->hasParameter('Collation')) {
            $ddl[] = 'COLLATE ' . $this->quote($colinfo->getParameter('Collation'));
        } elseif ($colinfo->hasParameter('Collate')) {
            $ddl[] = 'COLLATE ' . $this->quote($colinfo->getParameter('Collate'));
        }

        if ($sqlType === 'TIMESTAMP') {
            if ($notNullString === '') {
                $notNullString = 'NULL';
            }
            if ($defaultSetting === '' && $notNullString === 'NOT NULL') {
                $defaultSetting = 'DEFAULT CURRENT_TIMESTAMP';
            }
            $ddl[] = $notNullString;
            if ($defaultSetting) {
                $ddl[] = $defaultSetting;
            }
        } else {
            if ($defaultSetting) {
                $ddl[] = $defaultSetting;
            }
            if ($notNullString) {
                $ddl[] = $notNullString;
            }
        }

        $autoIncrement = $col->getAutoIncrementString();
        if ($autoIncrement) {
            $ddl[] = $autoIncrement;
        }

        if ($col->getDescription()) {
            $ddl[] = 'COMMENT ' . $this->quote($col->getDescription());
        }

        return implode(' ', $ddl);
    }

    /**
     * Returns the SQL type as a string.
     *
     * @see Domain::getSqlType()
     *
     * @param \Propel\Generator\Model\Column $column
     *
     * @return string
     */
    public function getSqlTypeExpression(Column $column): string
    {
        $sqlType = $column->getSqlType();
        $hasSize = $this->hasSize($sqlType) && $column->isDefaultSqlType($this);

        return (!$hasSize) ? $sqlType : $sqlType . $column->getSizeDefinition();
    }

    /**
     * @param \Propel\Generator\Model\Column $fromColumn
     * @param \Propel\Generator\Model\Column $toColumn
     *
     * @return string
     */
    protected function getChangeColumnToUuidBinaryType(Column $fromColumn, Column $toColumn): string
    {
        return MysqlUuidMigrationBuilder::create($this)->buildMigration($fromColumn, $toColumn, true);
    }

    /**
     * @param \Propel\Generator\Model\Column $fromColumn
     * @param \Propel\Generator\Model\Column $toColumn
     *
     * @return string
     */
    protected function getChangeColumnFromUuidBinaryType(Column $fromColumn, Column $toColumn): string
    {
        return MysqlUuidMigrationBuilder::create($this)->buildMigration($fromColumn, $toColumn, false);
    }

    /**
     * Creates a comma-separated list of column names for the index.
     * For MySQL unique indexes there is the option of specifying size, so we cannot simply use
     * the getColumnsList() method.
     *
     * @param \Propel\Generator\Model\Index $index
     *
     * @return string
     */
    protected function getIndexColumnListDDL(Index $index): string
    {
        $list = [];
        foreach ($index->getColumns() as $col) {
            $size = $index->hasColumnSize($col) ? '(' . $index->getColumnSize($col) . ')' : '';
            $list[] = $this->quoteIdentifier($col) . $size;
        }

        return implode(', ', $list);
    }

    /**
     * Builds the DDL SQL to drop the primary key of a table.
     *
     * @param \Propel\Generator\Model\Table $table
     *
     * @return string
     */
    public function getDropPrimaryKeyDDL(Table $table): string
    {
        if (!$table->hasPrimaryKey()) {
            return '';
        }

        $tableName = $this->quoteIdentifier($table->getName());

        return "\nALTER TABLE $tableName DROP PRIMARY KEY;\n";
    }

    /**
     * Builds the DDL SQL to add an Index.
     *
     * @param \Propel\Generator\Model\Index $index
     *
     * @return string
     */
    public function getAddIndexDDL(Index $index): string
    {
        $pattern = "
CREATE %sINDEX %s ON %s (%s);
";

        return sprintf(
            $pattern,
            $this->getIndexType($index),
            $this->quoteIdentifier($index->getName()),
            $this->quoteIdentifier($index->getTable()->getName()),
            $this->getIndexColumnListDDL($index),
        );
    }

    /**
     * Builds the DDL SQL to drop an Index.
     *
     * @param \Propel\Generator\Model\Index $index
     *
     * @return string
     */
    public function getDropIndexDDL(Index $index): string
    {
        $pattern = "
DROP INDEX %s ON %s;
";

        return sprintf(
            $pattern,
            $this->quoteIdentifier($index->getName()),
            $this->quoteIdentifier($index->getTable()->getName()),
        );
    }

    /**
     * Builds the DDL SQL for an Index object.
     *
     * @param \Propel\Generator\Model\Index $index
     *
     * @return string
     */
    public function getIndexDDL(Index $index): string
    {
        return sprintf(
            '%sINDEX %s (%s)',
            $this->getIndexType($index),
            $this->quoteIdentifier($index->getName()),
            $this->getIndexColumnListDDL($index),
        );
    }

    /**
     * @param \Propel\Generator\Model\Index $index
     *
     * @return string
     */
    protected function getIndexType(Index $index): string
    {
        $type = '';
        $vendorInfo = $index->getVendorInfoForType($this->getDatabaseType());
        if ($vendorInfo->getParameter('Index_type')) {
            $type = $vendorInfo->getParameter('Index_type') . ' ';
        } elseif ($index->isUnique()) {
            $type = 'UNIQUE ';
        }

        return $type;
    }

    /**
     * @param \Propel\Generator\Model\Unique $unique
     *
     * @return string
     */
    public function getUniqueDDL(Unique $unique): string
    {
        return sprintf(
            'UNIQUE INDEX %s (%s)',
            $this->quoteIdentifier($unique->getName()),
            $this->getIndexColumnListDDL($unique),
        );
    }

    /**
     * @param \Propel\Generator\Model\ForeignKey $fk
     *
     * @return string
     */
    public function getAddForeignKeyDDL(ForeignKey $fk): string
    {
        if ($this->supportsForeignKeys($fk->getTable())) {
            return parent::getAddForeignKeyDDL($fk);
        }

        return '';
    }

    /**
     * Builds the DDL SQL for a ForeignKey object.
     *
     * @param \Propel\Generator\Model\ForeignKey $fk
     *
     * @return string
     */
    public function getForeignKeyDDL(ForeignKey $fk): string
    {
        if ($this->supportsForeignKeys($fk->getTable())) {
            return parent::getForeignKeyDDL($fk);
        }

        return '';
    }

    /**
     * @param \Propel\Generator\Model\ForeignKey $fk
     *
     * @return string|null
     */
    public function getDropForeignKeyDDL(ForeignKey $fk): ?string
    {
        if (!$this->supportsForeignKeys($fk->getTable())) {
            return '';
        }
        if ($fk->isSkipSql() || $fk->isPolymorphic()) {
            return null;
        }
        $pattern = "
ALTER TABLE %s DROP FOREIGN KEY %s;
";

        return sprintf(
            $pattern,
            $this->quoteIdentifier($fk->getTable()->getName()),
            $this->quoteIdentifier($fk->getName()),
        );
    }

    /**
     * @param string $comment
     *
     * @return string
     */
    public function getCommentBlockDDL(string $comment): string
    {
        $pattern = "
-- ---------------------------------------------------------------------
-- %s
-- ---------------------------------------------------------------------
";

        return sprintf($pattern, $comment);
    }

    /**
     * Builds the DDL SQL to modify a database
     * based on a DatabaseDiff instance
     *
     * @param \Propel\Generator\Model\Diff\DatabaseDiff $databaseDiff
     *
     * @return string
     */
    public function getModifyDatabaseDDL(DatabaseDiff $databaseDiff): string
    {
        $ret = '';

        foreach ($databaseDiff->getRemovedTables() as $table) {
            $ret .= $this->getDropTableDDL($table);
        }

        foreach ($databaseDiff->getRenamedTables() as $fromTableName => $toTableName) {
            $ret .= $this->getRenameTableDDL($fromTableName, $toTableName);
        }

        foreach ($databaseDiff->getModifiedTables() as $tableDiff) {
            $ret .= $this->getModifyTableDDL($tableDiff);
        }

        foreach ($databaseDiff->getAddedTables() as $table) {
            $ret .= $this->getAddTableDDL($table);
        }

        if ($ret) {
            $ret = $this->getBeginDDL() . $ret . $this->getEndDDL();
        }

        return $ret;
    }

    /**
     * Builds the DDL SQL to rename a table
     *
     * @param string $fromTableName
     * @param string $toTableName
     *
     * @return string
     */
    public function getRenameTableDDL(string $fromTableName, string $toTableName): string
    {
        $pattern = "
RENAME TABLE %s TO %s;
";

        return sprintf(
            $pattern,
            $this->quoteIdentifier($fromTableName),
            $this->quoteIdentifier($toTableName),
        );
    }

    /**
     * Builds the DDL SQL to remove a column
     *
     * @param \Propel\Generator\Model\Column $column
     *
     * @return string
     */
    public function getRemoveColumnDDL(Column $column): string
    {
        $pattern = "
ALTER TABLE %s DROP %s;
";

        return sprintf(
            $pattern,
            $this->quoteIdentifier($column->getTable()->getName()),
            $this->quoteIdentifier($column->getName()),
        );
    }

    /**
     * Builds the DDL SQL to rename a column
     *
     * @param \Propel\Generator\Model\Column $fromColumn
     * @param \Propel\Generator\Model\Column $toColumn
     *
     * @return string
     */
    public function getRenameColumnDDL(Column $fromColumn, Column $toColumn): string
    {
        return $this->getChangeColumnDDL($fromColumn, $toColumn);
    }

    /**
     * Builds the DDL SQL to modify a column
     *
     * @param \Propel\Generator\Model\Diff\ColumnDiff $columnDiff
     *
     * @return string
     */
    public function getModifyColumnDDL(ColumnDiff $columnDiff): string
    {
        $fromColumn = $columnDiff->getFromColumn();
        $toColumn = $columnDiff->getToColumn();

        if ($fromColumn->isTextType() && $toColumn->isUuidBinaryType()) {
            return $this->getChangeColumnToUuidBinaryType($fromColumn, $toColumn);
        }

        // binary column from database does not know it is a UUID column
        $fromBinaryColumn = in_array($fromColumn->getType(), [PropelTypes::BINARY, PropelTypes::UUID_BINARY], true);
        if ($fromBinaryColumn && $toColumn->isTextType() && $toColumn->isContent('UUID')) {
            return $this->getChangeColumnFromUuidBinaryType($fromColumn, $toColumn);
        }

        return $this->getChangeColumnDDL($fromColumn, $toColumn);
    }

    /**
     * Builds the DDL SQL to change a column
     *
     * @param \Propel\Generator\Model\Column $fromColumn
     * @param \Propel\Generator\Model\Column $toColumn
     *
     * @return string
     */
    public function getChangeColumnDDL(Column $fromColumn, Column $toColumn): string
    {
        $tableName = $this->quoteIdentifier($fromColumn->getTable()->getName());
        $columnName = $this->quoteIdentifier($fromColumn->getName());
        $columnDefinition = $this->getColumnDDL($toColumn);
        $pattern = "\nALTER TABLE %s CHANGE %s %s;\n";

        return sprintf($pattern, $tableName, $columnName, $columnDefinition);
    }

    /**
     * Builds the DDL SQL to modify a list of columns
     *
     * @param array<\Propel\Generator\Model\Diff\ColumnDiff> $columnDiffs
     *
     * @return string
     */
    public function getModifyColumnsDDL(array $columnDiffs): string
    {
        $modifyColumnStatements = array_map([$this, 'getModifyColumnDDL'], $columnDiffs);

        return implode('', $modifyColumnStatements);
    }

    /**
     * Builds the DDL SQL to add a column
     *
     * @param \Propel\Generator\Model\Column $column
     *
     * @return string
     */
    public function getAddColumnDDL(Column $column): string
    {
        $pattern = "
ALTER TABLE %s ADD %s %s;
";
        $tableColumns = $column->getTable()->getColumns();

        // Default to add first if no column is found before the current one
        $insertPositionDDL = 'FIRST';
        foreach ($tableColumns as $i => $tableColumn) {
            // We found the column, use the one before it if it's not the first
            if ($tableColumn->getName() == $column->getName()) {
                // We have a column that is not the first one
                if ($i > 0) {
                    $insertPositionDDL = 'AFTER ' . $this->quoteIdentifier($tableColumns[$i - 1]->getName());
                }

                break;
            }
        }

        return sprintf(
            $pattern,
            $this->quoteIdentifier($column->getTable()->getName()),
            $this->getColumnDDL($column),
            $insertPositionDDL,
        );
    }

    /**
     * Builds the DDL SQL to add a list of columns
     *
     * @param array<\Propel\Generator\Model\Column> $columns
     *
     * @return string
     */
    public function getAddColumnsDDL(array $columns): string
    {
        $lines = '';
        foreach ($columns as $column) {
            $lines .= $this->getAddColumnDDL($column);
        }

        return $lines;
    }

    /**
     * @see Platform::supportsSchemas()
     *
     * @return bool
     */
    public function supportsSchemas(): bool
    {
        return true;
    }

    /**
     * @param string $sqlType
     *
     * @return bool
     */
    public function hasSize(string $sqlType): bool
    {
        return !in_array($sqlType, [
            'MEDIUMTEXT',
            'LONGTEXT',
            'BLOB',
            'MEDIUMBLOB',
            'LONGBLOB',
        ], true);
    }

    /**
     * @return array<int>
     */
    public function getDefaultTypeSizes(): array
    {
        return [
            'char' => 1,
            'tinyint' => 4,
            'smallint' => 6,
            'int' => 11,
            'bigint' => 20,
            'decimal' => 10,
        ];
    }

    /**
     * Escape the string for RDBMS.
     *
     * @param string $text
     *
     * @return string
     */
    public function disconnectedEscapeText(string $text): string
    {
        return addslashes($text);
    }

    /**
     * {@inheritDoc}
     *
     * MySQL documentation says that identifiers cannot contain '.'. Thus it
     * should be safe to split the string by '.' and quote each part individually
     * to allow for a <schema>.<table> or <table>.<column> syntax.
     *
     * @param string $text the identifier
     *
     * @return string the quoted identifier
     */
    public function doQuoting(string $text): string
    {
        return '`' . strtr($text, ['.' => '`.`']) . '`';
    }

    /**
     * @param \Propel\Generator\Model\Column $column
     * @param string $identifier
     * @param string $columnValueAccessor
     * @param string $tab
     *
     * @return string
     */
    public function getColumnBindingPHP(Column $column, string $identifier, string $columnValueAccessor, string $tab = '            '): string
    {
        // FIXME - This is a temporary hack to get around apparent bugs w/ PDO+MYSQL
        // See http://pecl.php.net/bugs/bug.php?id=9919
        if ($column->getPDOType() === PDO::PARAM_BOOL) {
            return sprintf(
                "
%s\$stmt->bindValue(%s, (int) %s, PDO::PARAM_INT);",
                $tab,
                $identifier,
                $columnValueAccessor,
            );
        }

        return parent::getColumnBindingPHP($column, $identifier, $columnValueAccessor, $tab);
    }

    /**
     * Get the default On Delete behavior for foreign keys when not explicity set.
     *
     * @return string
     */
    public function getDefaultForeignKeyOnDeleteBehavior(): string
    {
        $majorVersion = $this->getMajorServerVersionNumber();

        return ($majorVersion && $majorVersion >= 8 && !$this->isMariaDB()) ? ForeignKey::NOACTION : ForeignKey::RESTRICT;
    }

    /**
     * Get the default On Update behavior for foreign keys when not explicity set.
     *
     * @return string
     */
    public function getDefaultForeignKeyOnUpdateBehavior(): string
    {
        $majorVersion = $this->getMajorServerVersionNumber();

        return ($majorVersion && $majorVersion >= 8 && !$this->isMariaDB()) ? ForeignKey::NOACTION : ForeignKey::RESTRICT;
    }

    /**
     * Get the server version of the platform
     *
     * @return string|null
     */
    protected function getServerVersion(): ?string
    {
        if (!$this->serverVersion && $this->con) {
            $this->serverVersion = $this->con->getAttribute(PDO::ATTR_SERVER_VERSION);
        }

        return $this->serverVersion;
    }

    /**
     * Get the extracted major server version number
     *
     * @return int|null
     */
    protected function getMajorServerVersionNumber(): ?int
    {
        $serverVersion = $this->getServerVersion();
        if (!$serverVersion) {
            return null;
        }
        $dotPos = strpos($serverVersion, '.');
        if ($dotPos === false) {
            return null;
        }

        return (int)substr($serverVersion, 0, $dotPos - 1);
    }

    /**
     * Whether the platform is running on a MariaDB server
     *
     * @return bool
     */
    protected function isMariaDB(): bool
    {
        $serverVersion = $this->getServerVersion() ?? '';

        return (stripos($serverVersion, 'mariadb') !== false);
    }
}