Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move the installer into the core #4888

Merged
merged 17 commits into from Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions UPGRADE.md
Expand Up @@ -390,3 +390,10 @@ The public folder is now called `public` by default. It can be renamed in the `c
The `Contao\CoreBundle\Image\Studio\Figure::getLinkAttributes()` method will now return an
`Contao\CoreBundle\String\HtmlAttributes` object instead of an array. Use `iterator_to_array()` to transform it
back to an array representation. If you are just using array access, nothing needs to be changed.

### Install Tool

The ability to run execute migrations has been removed from the Install Tool, use the `contao:migrate` command or the
m-vo marked this conversation as resolved.
Show resolved Hide resolved
Contao Manager instead.

The `sqlCompileCommands` hook hasd been removed, use a custom schema provider instead.
m-vo marked this conversation as resolved.
Show resolved Hide resolved
20 changes: 9 additions & 11 deletions core-bundle/src/Command/MigrateCommand.php
Expand Up @@ -15,9 +15,10 @@
use Contao\CoreBundle\Doctrine\Backup\BackupManager;
use Contao\CoreBundle\Doctrine\Schema\MysqlInnodbRowSizeCalculator;
use Contao\CoreBundle\Doctrine\Schema\SchemaProvider;
use Contao\CoreBundle\Migration\CommandCompiler;
use Contao\CoreBundle\Migration\MigrationCollection;
use Contao\CoreBundle\Migration\MigrationResult;
use Contao\InstallationBundle\Database\Installer;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Table;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidOptionException;
Expand All @@ -34,11 +35,12 @@ class MigrateCommand extends Command
private SymfonyStyle|null $io = null;

public function __construct(
private CommandCompiler $commandCompiler,
private Connection $connection,
private MigrationCollection $migrations,
private BackupManager $backupManager,
private SchemaProvider $schemaProvider,
private MysqlInnodbRowSizeCalculator $rowSizeCalculator,
private Installer|null $installer = null,
) {
parent::__construct();
}
Expand Down Expand Up @@ -253,12 +255,6 @@ private function executeMigrations(bool &$dryRun, bool $asJson, string $specifie

private function executeSchemaDiff(bool $dryRun, bool $asJson, bool $withDeletesOption, string $specifiedHash = null): bool
{
if (null === $this->installer) {
$this->io->error('Service "contao_installation.database.installer" not found. The installation bundle needs to be installed in order to execute schema diff migrations.');

return false;
}

if ($schemaWarnings = $this->compileSchemaWarnings()) {
$this->io->warning(implode("\n\n", $schemaWarnings));

Expand All @@ -270,9 +266,11 @@ private function executeSchemaDiff(bool $dryRun, bool $asJson, bool $withDeletes
$commandsByHash = [];

while (true) {
$this->installer->compileCommands();
$commands = [];

$commands = $this->installer->getCommands(false);
foreach ($this->commandCompiler->compileCommands() as $command) {
$commands[md5($command)] = $command;
}

$hasNewCommands = \count(array_filter(
array_keys($commands),
Expand Down Expand Up @@ -341,7 +339,7 @@ private function executeSchemaDiff(bool $dryRun, bool $asJson, bool $withDeletes
}

try {
$this->installer->execCommand($hash);
$this->connection->executeQuery($commands[$hash]);

++$count;
$commandExecuted = true;
Expand Down
166 changes: 166 additions & 0 deletions core-bundle/src/Migration/CommandCompiler.php
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Migration;

use Contao\CoreBundle\Doctrine\Schema\SchemaProvider;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Schema;

class CommandCompiler
{
/**
* @internal Do not inherit from this class; decorate the "contao.migration.command_compiler" service instead
*/
public function __construct(private readonly Connection $connection, private readonly SchemaProvider $schemaProvider)
{
}

/**
* @return list<string>
*/
public function compileCommands(): array
{
// Get a list of SQL commands from the schema diff
$schemaManager = $this->connection->createSchemaManager();
$fromSchema = $schemaManager->createSchema();
$toSchema = $this->schemaProvider->createSchema();

$diffCommands = $schemaManager
->createComparator()
->compareSchemas($fromSchema, $toSchema)
->toSql($this->connection->getDatabasePlatform())
;

// Get a list of SQL commands that adjust the engine and collation options
$engineAndCollationCommands = $this->compileEngineAndCollationCommands($fromSchema, $toSchema);

return array_unique([...$diffCommands, ...$engineAndCollationCommands]);
ausi marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Checks engine and collation and adds the ALTER TABLE queries.
*
* @return list<string>
*/
private function compileEngineAndCollationCommands(Schema $fromSchema, Schema $toSchema): array
{
$tables = $toSchema->getTables();
$dynamic = $this->hasDynamicRowFormat();

$commands = [];

foreach ($tables as $table) {
$tableName = $table->getName();
$deleteIndexes = false;

if (!str_starts_with($tableName, 'tl_')) {
continue;
}

$tableOptions = $this->connection->fetchAssociative(
'SHOW TABLE STATUS WHERE Name = ? AND Engine IS NOT NULL AND Create_options IS NOT NULL AND Collation IS NOT NULL',
[$tableName]
);

if (false === $tableOptions) {
continue;
}

$engine = $table->hasOption('engine') ? $table->getOption('engine') : '';
$innodb = 'innodb' === strtolower($engine);

if (strtolower($tableOptions['Engine']) !== strtolower($engine)) {
if ($innodb && $dynamic) {
$command = 'ALTER TABLE '.$tableName.' ENGINE = '.$engine.' ROW_FORMAT = DYNAMIC';

if (false !== stripos($tableOptions['Create_options'], 'key_block_size=')) {
$command .= ' KEY_BLOCK_SIZE = 0';
}
} else {
$command = 'ALTER TABLE '.$tableName.' ENGINE = '.$engine;
}

$deleteIndexes = true;
$commands[] = $command;
} elseif ($innodb && $dynamic) {
if (false === stripos($tableOptions['Create_options'], 'row_format=dynamic')) {
$command = 'ALTER TABLE '.$tableName.' ENGINE = '.$engine.' ROW_FORMAT = DYNAMIC';

if (false !== stripos($tableOptions['Create_options'], 'key_block_size=')) {
$command .= ' KEY_BLOCK_SIZE = 0';
}

$commands[] = $command;
}
}

$collate = '';
$charset = $table->hasOption('charset') ? $table->getOption('charset') : '';

if ($table->hasOption('collation')) {
$collate = $table->getOption('collation');
} elseif ($table->hasOption('collate')) {
$collate = $table->getOption('collate');
}

if ($tableOptions['Collation'] !== $collate && '' !== $charset) {
$command = 'ALTER TABLE '.$tableName.' CONVERT TO CHARACTER SET '.$charset.' COLLATE '.$collate;
$deleteIndexes = true;
$commands[] = $command;
}

// Delete the indexes if the engine changes in case the existing
// indexes are too long. The migration then needs to be run multiple
// times to re-create the indexes with the correct length.
if ($deleteIndexes) {
if (!$fromSchema->hasTable($tableName)) {
continue;
}

$platform = $this->connection->getDatabasePlatform();

foreach ($fromSchema->getTable($tableName)->getIndexes() as $index) {
$indexName = $index->getName();

if ('primary' === strtolower($indexName)) {
continue;
}

$commands[] = $platform->getDropIndexSQL($indexName, $tableName);
}
}
}

return $commands;
}

private function hasDynamicRowFormat(): bool
{
$filePerTable = $this->connection->fetchAssociative("SHOW VARIABLES LIKE 'innodb_file_per_table'");

// Dynamic rows require innodb_file_per_table to be enabled
if (!\in_array(strtolower((string) $filePerTable['Value']), ['1', 'on'], true)) {
return false;
}

$fileFormat = $this->connection->fetchAssociative("SHOW VARIABLES LIKE 'innodb_file_format'");

// MySQL 8 and MariaDB 10.3 no longer have the "innodb_file_format" setting
if (false === $fileFormat || '' === $fileFormat['Value']) {
return true;
}

// Dynamic rows require the Barracuda file format in MySQL <8 and MariaDB <10.3
return 'barracuda' === strtolower((string) $fileFormat['Value']);
}
}
3 changes: 2 additions & 1 deletion core-bundle/src/Resources/config/commands.yaml
Expand Up @@ -86,11 +86,12 @@ services:
contao.command.migrate:
class: Contao\CoreBundle\Command\MigrateCommand
arguments:
- '@contao.migration.command_compiler'
- '@database_connection'
- '@contao.migration.collection'
- '@contao.doctrine.backup_manager'
- '@contao.doctrine.schema_provider'
- '@contao.doctrine.schema.mysql_innodb_row_size_calculator'
- '@?contao_installation.database.installer'

contao.command.resize_images:
class: Contao\CoreBundle\Command\ResizeImagesCommand
Expand Down
7 changes: 7 additions & 0 deletions core-bundle/src/Resources/config/migrations.yaml
Expand Up @@ -7,6 +7,13 @@ services:
arguments:
- ~

contao.migration.command_compiler:
class: Contao\CoreBundle\Migration\CommandCompiler
public: true
arguments:
- '@database_connection'
- '@contao.doctrine.schema_provider'

contao.migration.version_400.version_400_update:
class: Contao\CoreBundle\Migration\Version400\Version400Update
arguments:
Expand Down