OpenXE/classes/Components/SchemaCreator/Adapter/Driver/MysqlDriver.php
2021-05-21 08:49:41 +02:00

547 lines
21 KiB
PHP

<?php
declare(strict_types=1);
namespace Xentral\Components\SchemaCreator\Adapter\Driver;
use Exception;
use Xentral\Components\Database\Exception\EscapingException;
use Xentral\Components\SchemaCreator\Collection\ColumnCollection;
use Xentral\Components\SchemaCreator\Collection\IndexCollection;
use Xentral\Components\SchemaCreator\Collection\TableOptionCollection;
use Xentral\Components\SchemaCreator\Exception\SchemaCreatorInvalidArgumentException;
use Xentral\Components\SchemaCreator\Index\Constraint;
use Xentral\Components\SchemaCreator\Index\Index;
use Xentral\Components\SchemaCreator\Index\Primary;
use Xentral\Components\SchemaCreator\Index\Unique;
use Xentral\Components\SchemaCreator\Interfaces\PrimaryKeyInterface;
use Xentral\Components\SchemaCreator\Interfaces\ColumnInterface;
use Xentral\Components\SchemaCreator\Interfaces\IndexInterface;
use Xentral\Components\SchemaCreator\Interfaces\DriverInterface;
use Xentral\Components\SchemaCreator\LineGenerator\Common\ColumnLineGenerator;
use Xentral\Components\SchemaCreator\LineGenerator\Common\ConstraintLineGenerator;
use Xentral\Components\SchemaCreator\LineGenerator\Common\IndexLineGenerator;
use Xentral\Components\SchemaCreator\LineGenerator\Common\TableOptionsGenerator;
use Xentral\Components\SchemaCreator\Option\TableOption;
use Xentral\Components\SchemaCreator\Schema\TableSchema;
final class MysqlDriver implements DriverInterface
{
/** @var ColumnLineGenerator $columnGenerator */
private $columnGenerator;
/** @var IndexLineGenerator $indexGenerator */
private $indexGenerator;
/** @var TableOptionsGenerator $tableOptionsGenerator */
private $tableOptionsGenerator;
/** @var ConstraintLineGenerator $constraintGenerator */
private $constraintGenerator;
/**
* @param ColumnLineGenerator $columnGenerator
* @param IndexLineGenerator $indexGenerator
* @param TableOptionsGenerator $tableOptionsGenerator
* @param ConstraintLineGenerator $constraintLineGenerator
*/
public function __construct(
ColumnLineGenerator $columnGenerator,
IndexLineGenerator $indexGenerator,
TableOptionsGenerator $tableOptionsGenerator,
ConstraintLineGenerator $constraintLineGenerator
) {
$this->columnGenerator = $columnGenerator;
$this->indexGenerator = $indexGenerator;
$this->tableOptionsGenerator = $tableOptionsGenerator;
$this->constraintGenerator = $constraintLineGenerator;
}
/**
* @param ColumnCollection $columnCollection
* @param IndexCollection $indexCollection
*
* @throws Exception
* @return string
*/
private function generateTableColumns(ColumnCollection $columnCollection, IndexCollection $indexCollection): string
{
$columnDefinitions = [];
$hasAutoIncrement = $columnCollection->hasAutoIncrement();
$autoIncrementColumn = $columnCollection->getAutoIncrementColumn();
foreach ($columnCollection as $column) {
$columnDefinitions[] = $this->columnGenerator->generateLine($column);
$isFieldAsAutoIncrement = $hasAutoIncrement === true && $autoIncrementColumn->getField(
) === $column->getField();
if ($indexCollection !== null && $isFieldAsAutoIncrement === true && $indexCollection->hasPrimaryKey(
) === false) {
$primaryRef = $column->getField();
$indexCollection->add(new Primary([$primaryRef]));
$columnCollection->getIterator()->rewind();
}
}
return implode(",\n", $columnDefinitions);
}
/**
* @param IndexCollection $indexCollection
* @param ColumnCollection $targetColumns
*
* @throws EscapingException
*
* @return string
*/
private function generateTableIndexes(IndexCollection $indexCollection, ColumnCollection $targetColumns): string
{
$asIndexes = [];
foreach ($indexCollection as $tableIndex) {
/** @var IndexInterface $tableIndex */
if ($this->hasIndexReferencesInSchema($tableIndex->getReferences(), $targetColumns)) {
$asIndexes [] = $this->indexGenerator->generateLine($tableIndex);
}
}
return implode(",\n", $asIndexes);
}
/**
* @param TableOptionCollection $tableOptionCollection
*
* @throws EscapingException
*
* @return string
*/
private function generateTableOptions(TableOptionCollection $tableOptionCollection): string
{
$tableOptions = [];
foreach ($tableOptionCollection as $tableOption) {
/** @var TableOption $tableOption */
$tableOptions[$tableOption->getOption()] = $tableOption->getValue();
}
return $this->tableOptionsGenerator->generate($tableOptions);
}
/**
* @inheritDoc
*/
public function loadFromTable(string $table): TableSchema
{
$tableSchema = new TableSchema($table);
if ($columns = $this->columnGenerator->fetchColumnsFromDb($table)) {
foreach ($columns as $column) {
$type = strtolower($column['type']);
$type = $type === 'int' ? 'integer' : $type; //Cannot use 'Int' as class name as it is reserved
$type = $type === 'float' ? 'double' : $type; //Cannot use 'Float' as class name as it is reserved
$classType = '\\Xentral\\Components\\SchemaCreator\\Type\\' . ucfirst($type);
if (!class_exists($classType)) {
throw new SchemaCreatorInvalidArgumentException(sprintf('%s cannot be found', $classType));
}
if (!method_exists($classType, 'fromDBColumn')) {
throw new SchemaCreatorInvalidArgumentException(
sprintf('Method %s::fromDBColumn not found', $classType)
);
}
if (is_numeric($column['default']) && $this->hasImplemented($classType, 'NumericTypeInterface')) {
$column['default'] = (int)$column['default'];
}
if ($this->hasImplemented($classType, 'EnumAndSetInterface')) {
$column['references'] = explode(',', $column['references']);
}
$callback = call_user_func($classType . '::fromDBColumn', $column);
$tableSchema->addColumn($callback);
}
}
if ($indexes = $this->indexGenerator->fetchIndexesFromDb($table)) {
$constraints = $this->constraintGenerator->fetchConstraintsFromDb($table);
foreach ($indexes as $index) {
if ($index['name'] === PrimaryKeyInterface::INDEX_NAME) {
$tableSchema->addIndex(new Primary($index['columns']));
} elseif ( ($constraintKey = array_search($index['name'], array_column($constraints, 'name'), true)) !== false) {
$tableIndex = new Constraint(
$constraints[$constraintKey]['name'],
$constraints[$constraintKey]['columns'],
$constraints[$constraintKey]['reference_table'],
$constraints[$constraintKey]['reference_columns']
);
$tableSchema->addIndex($tableIndex);
} else {
$tableIndex = $index['unique'] === true ? new Unique($index['columns'], $index['name']) : new Index(
$index['columns'], $index['name']
);
$tableSchema->addIndex($tableIndex);
}
}
}
if ($options = $this->tableOptionsGenerator->fetchOptionsFromDb($table)) {
$tableSchema->addOption(TableOption::fromEngine($options['engine']));
$tableSchema->addOption(TableOption::fromCharset($options['table_charset']));
$tableSchema->addOption(TableOption::fromCollation($options['collation']));
$tableSchema->addOption(TableOption::fromComment($options['comment']));
}
return $tableSchema;
}
/**
* @param string $class
* @param string $needleInterface
*
* @return bool
*/
private function hasImplemented(string $class, string $needleInterface): bool
{
if ($interfaces = class_implements($class)) {
foreach ($interfaces as $interface) {
$interface_exploded = explode('\\', $interface);
$name = array_pop($interface_exploded);
if ($name === $needleInterface) {
return true;
}
}
}
return false;
}
/**
* @param array $references
* @param ColumnCollection $targetColumns
*
* @return bool
*/
private function hasIndexReferencesInSchema(array $references, ColumnCollection $targetColumns): bool
{
$columns = [];
foreach ($targetColumns as $configuredColumns) {
$columns[] = $configuredColumns->getField();
}
foreach ($references as $reference) {
if (!in_array(sprintf('%s', $reference), $columns, true)) {
throw new SchemaCreatorInvalidArgumentException(
sprintf('Column name `%s` missing in the table', $reference)
);
}
}
return true;
}
/**
* @param array $targetSchema
* @param array $currentSchema
*
* @return bool
*/
private function checkColumnNeedsUpdate(array $targetSchema, array $currentSchema): bool
{
$diff = array_diff_assoc($targetSchema, $currentSchema);
return count($diff) > 0;
}
/**
* @param TableSchema $currentSchema
* @param TableSchema $targetSchema
*
* @throws EscapingException
*
* @return string
*/
private function generateColumnsDiff(TableSchema $currentSchema, TableSchema $targetSchema): string
{
$alterColumnParts = [];
$targetColumns = $targetSchema->getColumns();
$currentColumns = $currentSchema->getColumns();
$currentIndexes = $currentSchema->getIndexes();
$hasAutoIncrement = $currentColumns->hasAutoIncrement();
$autoIncrementColumn = $currentColumns->getAutoIncrementColumn();
foreach ($currentColumns as $schemaKey => $currentColumn) {
$isFieldAsAutoIncrement = $hasAutoIncrement === true && $autoIncrementColumn->getField(
) === $currentColumn->getField();
if ($currentIndexes !== null && $isFieldAsAutoIncrement === true && $currentIndexes->hasPrimaryKey(
) === false) {
$primaryReference = $currentColumn->getField();
$currentIndexes->add(new Primary([$primaryReference]));
}
}
$targetFields = $targetColumns->getFields();
$currentFields = $currentColumns->getFields();
$newFields = array_diff($targetFields, $currentFields);
foreach ($targetColumns as $schemaKey => $schemaColumn) {
/** @var ColumnInterface $schemaColumn */
$field = $schemaColumn->getField();
if (in_array($field, $newFields, true)) {
$alterColumnParts[] = sprintf('ADD %s', $this->columnGenerator->generateLine($schemaColumn));
} else {
$needsUpdate = $this->checkColumnNeedsUpdate(
$this->columnGenerator->toArray($schemaColumn),
$this->columnGenerator->toArray($currentSchema->getColumnByName($field))
);
if ($needsUpdate) {
$alterColumnParts[] = sprintf('MODIFY %s', $this->columnGenerator->generateLine($schemaColumn));
}
}
}
return implode(', ', $alterColumnParts);
}
/**
* @param IndexInterface $targetIndex
* @param IndexInterface $currentIndex
*
* @return bool
*/
private function checkIndexNeedsUpdate(IndexInterface $targetIndex, IndexInterface $currentIndex): bool
{
$targetReferences = $targetIndex->getReferences();
$currentReferences = $currentIndex->getReferences();
if ($targetIndex->isUnique() !== $currentIndex->isUnique()) {
return true;
}
$diffColumns = array_diff($targetReferences, $currentReferences);
return count($diffColumns) > 0;
}
/**
* @param TableSchema $currentSchema
* @param TableSchema $targetSchema
*
* @throws Exception
* @return string
*/
private function generateIndexesDiff(TableSchema $currentSchema, TableSchema $targetSchema): string
{
$alterIndexParts = [];
$targetIndexes = $targetSchema->getIndexes();
$currentIndexes = $currentSchema->getIndexes();
$targetColumns = $targetSchema->getColumns();
// FIX MISSING PRIMARY KEY HERE
$this->generateTableColumns($targetColumns, $targetIndexes);
$targetIndexNames = $targetIndexes->getIndexNames();
$currentIndexNames = $currentIndexes->getIndexNames();
$newIndexes = array_diff($targetIndexNames, $currentIndexNames);
$removedIndexes = array_diff($currentIndexNames, $targetIndexNames);
if (count($removedIndexes) > 0) {
foreach ($removedIndexes as $removedIndexName) {
if ($removedIndexName !== 'PRIMARY' && !in_array($removedIndexName, $newIndexes, true)) {
foreach($currentIndexes as $currentIndex) {
if($currentIndex->getName() !== $removedIndexName) {
continue;
}
if($currentIndex->getType() === 'CONSTRAINT') {
$alterIndexParts[] = sprintf('DROP FOREIGN KEY %s', $this->indexGenerator->escape($removedIndexName));
break;
}
else {
$alterIndexParts[] = sprintf('DROP INDEX %s', $this->indexGenerator->escape($removedIndexName));
break;
}
}
}
}
}
foreach ($targetIndexes as $schemaKey => $schemaIndex) {
$indexName = $schemaIndex->getName();
$indexType = $schemaIndex->getType();
if (in_array($indexName, $newIndexes, true) &&
$this->hasIndexReferencesInSchema($schemaIndex->getReferences(), $targetColumns) === true) {
$alterIndexParts[] = sprintf('ADD %s', $this->indexGenerator->generateLine($schemaIndex));
} else {
$needsUpdate = $this->checkIndexNeedsUpdate($schemaIndex, $currentSchema->getIndexByName($indexName));
if ($needsUpdate) {
if ($schemaIndex instanceof PrimaryKeyInterface) {
$references = $schemaIndex->getReferences();
// CHECK IF one of the reference has AutoIncrement
$currentPrimary = $currentSchema->getIndexByName('PRIMARY');
if (null !== $currentPrimary) {
$currentReferences = $currentPrimary->getReferences();
$columnModified = null;
foreach ($currentReferences as $columnName) {
$column = $currentSchema->getColumnByName($columnName);
$options = $column->getOptions();
if (array_key_exists('extra', $options) && in_array(
$options['extra'],
['ai', 'AUTO_INCREMENT']
)) {
$columnModified = sprintf(
'MODIFY %s',
$this->columnGenerator->generateLine($column)
);
$columnModifiedWithoutAi = str_replace('AUTO_INCREMENT', '', $columnModified);
$alterIndexParts[] = trim($columnModifiedWithoutAi);
break;
}
}
$alterIndexParts[] = 'DROP PRIMARY KEY';
}
$reference = implode(
',',
array_map(
function ($reference) {
return $this->indexGenerator->escape($reference);
},
$references
)
);
$alterIndexParts[] = sprintf('ADD PRIMARY KEY (%s)', $reference);
} else {
if($indexType === 'CONSTRAINT') {
$alterIndexParts[] = sprintf('DROP FOREIGN KEY %s', $this->indexGenerator->escape($indexName));
}
else {
$alterIndexParts[] = sprintf('DROP INDEX %s', $this->indexGenerator->escape($indexName));
}
$alterIndexParts[] = sprintf('ADD %s', $this->indexGenerator->generateLine($schemaIndex));
}
}
}
}
return implode(', ', $alterIndexParts);
}
/**
* @param TableSchema $currentSchema
* @param TableSchema $targetSchema
*
* @throws Exception
* @return string
*/
private function generateOptionsDiff(TableSchema $currentSchema, TableSchema $targetSchema): string
{
$targetOptions = $targetSchema->getOptions();
if ($targetOptions->getIterator()->count() === 0) {
return '';
}
$currentOptions = $currentSchema->getOptions();
$sqlOptions = [];
$charset = null;
foreach ($targetOptions as $option) {
$currentTableOption = $currentOptions->getTableOption($option->getOption());
if ($currentTableOption === null || $currentTableOption->getValue() !== $option->getValue()) {
if ($option->getOption() === 'collation' && ($charsetOption = $currentOptions->getTableOption(
'charset'
))) {
$charset = $charsetOption->getValue();
}
$sqlOptions[] = $this->tableOptionsGenerator->alterOptions($option, $charset);
}
}
return implode(' ', $sqlOptions);
}
/**
* @inheritDoc
*/
public function getTableDefinition(TableSchema $schema): string
{
$name = $schema->getTable();
$columns = $this->generateTableColumns(
$schema->getColumns(),
$schema->getIndexes()
);
$keys = $this->generateTableIndexes($schema->getIndexes(), $schema->getColumns());
$columns = rtrim($columns, ',');
if (!empty($keys)) {
$keys = ',' . PHP_EOL . $keys;
}
$definition = sprintf("(\n%s\n)", $columns . $keys);
$defFooter = $this->generateTableOptions($schema->getOptions());
$definition .= $defFooter;
return sprintf('CREATE TABLE IF NOT EXISTS %s %s', $this->indexGenerator->escape($name), $definition);
}
/**
* @param string $table
*
* @throws EscapingException
*
* @return string
*/
private function generateAlterTable(string $table): string
{
return sprintf('ALTER TABLE %s ', $this->indexGenerator->escape($table));
}
/**
* @inheritDoc
*/
public function generateTableSchemaDiff(TableSchema $currentSchema, TableSchema $targetSchema): string
{
$sql = '';
$alterColumns = $this->generateColumnsDiff($currentSchema, $targetSchema);
$alterIndexes = $this->generateIndexesDiff($currentSchema, $targetSchema);
$alterOptions = $this->generateOptionsDiff($currentSchema, $targetSchema);
if (empty($alterColumns) && empty($alterIndexes) && empty($alterOptions)) {
return $sql;
}
if (!empty($alterColumns)) {
$sql .= $alterColumns . ', ';
}
if (!empty($alterIndexes)) {
$sql .= $alterIndexes . ', ';
}
$table = $targetSchema->getTable();
$alterTable = $this->generateAlterTable($table);
if (!empty($alterOptions)) {
$alterTable .= $alterOptions;
if (!empty($sql)) {
$alterTable .= ', ';
}
}
$sql = substr_replace($sql, ';', -2);
return $alterTable . $sql;
}
}