nextcloud/lib/private/DB/MigrationService.php
Ferdinand Thiessen 67eef32b07
fix(DB): support up to 63 character long table and index names
We do not support Oracle 11 anymore but at least Oracle 12c (12.2).
So the limitation is gone (Oracle now supports up to 128 character long
names).

Instead we are now limited by MySQL (64 characters) and PostgreSQL (63
characters).

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
2026-01-18 14:50:34 +01:00

810 lines
28 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2017 ownCloud GmbH
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\DB;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Type;
use OC\App\InfoParser;
use OC\Migration\SimpleOutput;
use OCP\App\IAppManager;
use OCP\AppFramework\App;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Migration\IMigrationStep;
use OCP\Migration\IOutput;
use OCP\Server;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MigrationService {
private bool $migrationTableCreated;
private array $migrations;
private string $migrationsPath;
private string $migrationsNamespace;
private IOutput $output;
private LoggerInterface $logger;
private Connection $connection;
private string $appName;
private bool $checkOracle;
/**
* @throws \Exception
*/
public function __construct(
string $appName,
Connection $connection,
?IOutput $output = null,
?LoggerInterface $logger = null,
) {
$this->appName = $appName;
$this->checkOracle = false;
$this->connection = $connection;
if ($logger === null) {
$this->logger = Server::get(LoggerInterface::class);
} else {
$this->logger = $logger;
}
if ($output === null) {
$this->output = new SimpleOutput($this->logger, $appName);
} else {
$this->output = $output;
}
if ($appName === 'core') {
$this->migrationsPath = \OC::$SERVERROOT . '/core/Migrations';
$this->migrationsNamespace = 'OC\\Core\\Migrations';
$this->checkOracle = true;
} else {
$appManager = Server::get(IAppManager::class);
$appPath = $appManager->getAppPath($appName);
$namespace = App::buildAppNamespace($appName);
$this->migrationsPath = "$appPath/lib/Migration";
$this->migrationsNamespace = $namespace . '\\Migration';
$infoParser = new InfoParser();
$info = $infoParser->parse($appPath . '/appinfo/info.xml');
if (!isset($info['dependencies']['database'])) {
$this->checkOracle = true;
} else {
$this->checkOracle = false;
foreach ($info['dependencies']['database'] as $database) {
if (\is_string($database) && $database === 'oci') {
$this->checkOracle = true;
} elseif (\is_array($database) && isset($database['@value']) && $database['@value'] === 'oci') {
$this->checkOracle = true;
}
}
}
}
$this->migrationTableCreated = false;
}
/**
* Returns the name of the app for which this migration is executed
*/
public function getApp(): string {
return $this->appName;
}
/**
* @codeCoverageIgnore - this will implicitly tested on installation
*/
private function createMigrationTable(): bool {
if ($this->migrationTableCreated) {
return false;
}
if ($this->connection->tableExists('migrations') && \OCP\Server::get(IConfig::class)->getAppValue('core', 'vendor', '') !== 'owncloud') {
$this->migrationTableCreated = true;
return false;
}
$schema = new SchemaWrapper($this->connection);
/**
* We drop the table when it has different columns or the definition does not
* match. E.g. ownCloud uses a length of 177 for app and 14 for version.
*/
try {
$table = $schema->getTable('migrations');
$columns = $table->getColumns();
if (count($columns) === 2) {
try {
$column = $table->getColumn('app');
$schemaMismatch = $column->getLength() !== 255;
if (!$schemaMismatch) {
$column = $table->getColumn('version');
$schemaMismatch = $column->getLength() !== 255;
}
} catch (SchemaException $e) {
// One of the columns is missing
$schemaMismatch = true;
}
if (!$schemaMismatch) {
// Table exists and schema matches: return back!
$this->migrationTableCreated = true;
return false;
}
}
// Drop the table, when it didn't match our expectations.
$this->connection->dropTable('migrations');
// Recreate the schema after the table was dropped.
$schema = new SchemaWrapper($this->connection);
} catch (SchemaException $e) {
// Table not found, no need to panic, we will create it.
}
$table = $schema->createTable('migrations');
$table->addColumn('app', Types::STRING, ['length' => 255]);
$table->addColumn('version', Types::STRING, ['length' => 255]);
$table->setPrimaryKey(['app', 'version']);
$this->connection->migrateToSchema($schema->getWrappedSchema());
$this->migrationTableCreated = true;
return true;
}
/**
* Returns all versions which have already been applied
*
* @return list<string>
* @codeCoverageIgnore - no need to test this
*/
public function getMigratedVersions() {
$this->createMigrationTable();
$qb = $this->connection->getQueryBuilder();
$qb->select('version')
->from('migrations')
->where($qb->expr()->eq('app', $qb->createNamedParameter($this->getApp())))
->orderBy('version');
$result = $qb->executeQuery();
$rows = $result->fetchAll(\PDO::FETCH_COLUMN);
$result->closeCursor();
usort($rows, $this->sortMigrations(...));
return $rows;
}
/**
* Returns all versions which are available in the migration folder
* @return list<string>
*/
public function getAvailableVersions(): array {
$this->ensureMigrationsAreLoaded();
$versions = array_map('strval', array_keys($this->migrations));
usort($versions, $this->sortMigrations(...));
return $versions;
}
protected function sortMigrations(string $a, string $b): int {
preg_match('/(\d+)Date(\d+)/', basename($a), $matchA);
preg_match('/(\d+)Date(\d+)/', basename($b), $matchB);
if (!empty($matchA) && !empty($matchB)) {
$versionA = (int)$matchA[1];
$versionB = (int)$matchB[1];
if ($versionA !== $versionB) {
return ($versionA < $versionB) ? -1 : 1;
}
return strnatcmp($matchA[2], $matchB[2]);
}
return strnatcmp(basename($a), basename($b));
}
/**
* @return array<string, string>
*/
protected function findMigrations(): array {
$directory = realpath($this->migrationsPath);
if ($directory === false || !file_exists($directory) || !is_dir($directory)) {
return [];
}
$iterator = new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY
),
'#^.+\\/Version[^\\/]{1,255}\\.php$#i',
\RegexIterator::GET_MATCH);
$files = array_keys(iterator_to_array($iterator));
usort($files, $this->sortMigrations(...));
$migrations = [];
foreach ($files as $file) {
$className = basename($file, '.php');
$version = (string)substr($className, 7);
if ($version === '0') {
throw new \InvalidArgumentException(
"Cannot load a migrations with the name '$version' because it is a reserved number"
);
}
$migrations[$version] = sprintf('%s\\%s', $this->migrationsNamespace, $className);
}
return $migrations;
}
/**
* @param string $to
* @return string[]
*/
private function getMigrationsToExecute($to) {
$knownMigrations = $this->getMigratedVersions();
$availableMigrations = $this->getAvailableVersions();
$toBeExecuted = [];
foreach ($availableMigrations as $v) {
if ($to !== 'latest' && ($this->sortMigrations($v, $to) > 0)) {
continue;
}
if ($this->shallBeExecuted($v, $knownMigrations)) {
$toBeExecuted[] = $v;
}
}
return $toBeExecuted;
}
/**
* @param string $m
* @param string[] $knownMigrations
* @return bool
*/
private function shallBeExecuted($m, $knownMigrations) {
if (in_array($m, $knownMigrations)) {
return false;
}
return true;
}
/**
* @param string $version
*/
private function markAsExecuted($version): void {
$this->connection->insertIfNotExist('*PREFIX*migrations', [
'app' => $this->appName,
'version' => $version
]);
}
/**
* Returns the name of the table which holds the already applied versions
*
* @return string
*/
public function getMigrationsTableName() {
return $this->connection->getPrefix() . 'migrations';
}
/**
* Returns the namespace of the version classes
*
* @return string
*/
public function getMigrationsNamespace() {
return $this->migrationsNamespace;
}
/**
* Returns the directory which holds the versions
*
* @return string
*/
public function getMigrationsDirectory() {
return $this->migrationsPath;
}
/**
* Return the explicit version for the aliases; current, next, prev, latest
*
* @return mixed|null|string
*/
public function getMigration(string $alias) {
switch ($alias) {
case 'current':
return $this->getCurrentVersion();
case 'next':
return $this->getRelativeVersion($this->getCurrentVersion(), 1);
case 'prev':
return $this->getRelativeVersion($this->getCurrentVersion(), -1);
case 'latest':
$this->ensureMigrationsAreLoaded();
$migrations = $this->getAvailableVersions();
return @end($migrations);
}
return '0';
}
private function getRelativeVersion(string $version, int $delta): ?string {
$this->ensureMigrationsAreLoaded();
$versions = $this->getAvailableVersions();
array_unshift($versions, '0');
/** @var int|false $offset */
$offset = array_search($version, $versions, true);
if ($offset === false || !isset($versions[$offset + $delta])) {
// Unknown version or delta out of bounds.
return null;
}
return (string)$versions[$offset + $delta];
}
private function getCurrentVersion(): string {
$m = $this->getMigratedVersions();
if (count($m) === 0) {
return '0';
}
return @end($m);
}
/**
* @throws \InvalidArgumentException
*/
private function getClass(string $version): string {
$this->ensureMigrationsAreLoaded();
if (isset($this->migrations[$version])) {
return $this->migrations[$version];
}
throw new \InvalidArgumentException("Version $version is unknown.");
}
/**
* Allows to set an IOutput implementation which is used for logging progress and messages
*/
public function setOutput(IOutput $output): void {
$this->output = $output;
}
/**
* Applies all not yet applied versions up to $to
* @throws \InvalidArgumentException
*/
public function migrate(string $to = 'latest', bool $schemaOnly = false): void {
if ($schemaOnly) {
$this->output->debug('Migrating schema only');
$this->migrateSchemaOnly($to);
return;
}
// read known migrations
$toBeExecuted = $this->getMigrationsToExecute($to);
foreach ($toBeExecuted as $version) {
try {
$this->executeStep($version, $schemaOnly);
} catch (\Exception $e) {
// The exception itself does not contain the name of the migration,
// so we wrap it here, to make debugging easier.
throw new \Exception('Database error when running migration ' . $version . ' for app ' . $this->getApp() . PHP_EOL . $e->getMessage(), 0, $e);
}
}
}
/**
* Applies all not yet applied versions up to $to
* @throws \InvalidArgumentException
*/
public function migrateSchemaOnly(string $to = 'latest'): void {
// read known migrations
$toBeExecuted = $this->getMigrationsToExecute($to);
if (empty($toBeExecuted)) {
return;
}
$toSchema = null;
foreach ($toBeExecuted as $version) {
$this->output->debug('- Reading ' . $version);
$instance = $this->createInstance($version);
$toSchema = $instance->changeSchema($this->output, function () use ($toSchema): ISchemaWrapper {
return $toSchema ?: new SchemaWrapper($this->connection);
}, ['tablePrefix' => $this->connection->getPrefix()]) ?: $toSchema;
}
if ($toSchema instanceof SchemaWrapper) {
$this->output->debug('- Checking target database schema');
$targetSchema = $toSchema->getWrappedSchema();
$beforeSchema = $this->connection->createSchema();
$this->ensureUniqueNamesConstraints($targetSchema, true);
$this->ensureNamingConstraints($beforeSchema, $targetSchema, \strlen($this->connection->getPrefix()));
if ($this->checkOracle) {
$this->ensureOracleConstraints($beforeSchema, $targetSchema);
}
$this->output->debug('- Migrate database schema');
$this->connection->migrateToSchema($targetSchema);
$toSchema->performDropTableCalls();
}
$this->output->debug('- Mark migrations as executed');
foreach ($toBeExecuted as $version) {
$this->markAsExecuted($version);
}
}
/**
* Get the human readable descriptions for the migration steps to run
*
* @param string $to
* @return string[] [$name => $description]
*/
public function describeMigrationStep($to = 'latest') {
$toBeExecuted = $this->getMigrationsToExecute($to);
$description = [];
foreach ($toBeExecuted as $version) {
$migration = $this->createInstance($version);
if ($migration->name()) {
$description[$migration->name()] = $migration->description();
}
}
return $description;
}
/**
* @param string $version
* @return IMigrationStep
* @throws \InvalidArgumentException
*/
public function createInstance($version) {
/** @psalm-var class-string<IMigrationStep> $class */
$class = $this->getClass($version);
try {
$s = \OCP\Server::get($class);
} catch (NotFoundExceptionInterface) {
if (class_exists($class)) {
$s = new $class();
} else {
throw new \InvalidArgumentException("Migration step '$class' is unknown");
}
}
if (!$s instanceof IMigrationStep) {
throw new \InvalidArgumentException('Not a valid migration');
}
return $s;
}
/**
* Executes one explicit version
*
* @param string $version
* @param bool $schemaOnly
* @throws \InvalidArgumentException
*/
public function executeStep($version, $schemaOnly = false): void {
$instance = $this->createInstance($version);
if (!$schemaOnly) {
$instance->preSchemaChange($this->output, function (): ISchemaWrapper {
return new SchemaWrapper($this->connection);
}, ['tablePrefix' => $this->connection->getPrefix()]);
}
$toSchema = $instance->changeSchema($this->output, function (): ISchemaWrapper {
return new SchemaWrapper($this->connection);
}, ['tablePrefix' => $this->connection->getPrefix()]);
if ($toSchema instanceof SchemaWrapper) {
$targetSchema = $toSchema->getWrappedSchema();
$sourceSchema = $this->connection->createSchema();
$this->ensureUniqueNamesConstraints($targetSchema, $schemaOnly);
$this->ensureNamingConstraints($sourceSchema, $targetSchema, \strlen($this->connection->getPrefix()));
if ($this->checkOracle) {
$this->ensureOracleConstraints($sourceSchema, $targetSchema);
}
$this->connection->migrateToSchema($targetSchema);
$toSchema->performDropTableCalls();
}
if (!$schemaOnly) {
$instance->postSchemaChange($this->output, function (): ISchemaWrapper {
return new SchemaWrapper($this->connection);
}, ['tablePrefix' => $this->connection->getPrefix()]);
}
$this->markAsExecuted($version);
}
/**
* Enforces some naming conventions to make sure tables can be used on all supported database engines.
*
* Naming constraints:
* - Tables names must be 63 chars or shorter (including its prefix (default 'oc_'))
* - Column names must be 63 chars or shorter
* - Index names must be 63 chars or shorter
* - Sequence names must be 63 chars or shorter
* - Primary key names must be set to 63 chars or shorter - or the table name must be <= 58 characters (63 - 5 for '_pKey' suffix) including the table name prefix
*
* This is based on the identifier limits set by our supported database engines:
* - MySQL and MariaDB support 64 characters
* - Oracle supports 128 characters (since 12c)
* - PostgreSQL support 63
* - SQLite does not have any limits
*
* @see https://github.com/nextcloud/documentation/blob/master/developer_manual/basics/storage/database.rst
*
* @throws \Doctrine\DBAL\Exception
*/
public function ensureNamingConstraints(Schema $sourceSchema, Schema $targetSchema, int $prefixLength): void {
$MAX_NAME_LENGTH = 63;
$sequences = $targetSchema->getSequences();
foreach ($targetSchema->getTables() as $table) {
try {
$sourceTable = $sourceSchema->getTable($table->getName());
} catch (SchemaException $e) {
// we only validate new tables
if (\strlen($table->getName()) + $prefixLength > $MAX_NAME_LENGTH) {
throw new \InvalidArgumentException('Table name "' . $table->getName() . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH);
}
$sourceTable = null;
}
foreach ($table->getColumns() as $thing) {
// If the table doesn't exist OR if the column doesn't exist in the table
if ((!$sourceTable instanceof Table || !$sourceTable->hasColumn($thing->getName()))
&& \strlen($thing->getName()) > $MAX_NAME_LENGTH
) {
throw new \InvalidArgumentException('Column name "' . $table->getName() . '"."' . $thing->getName() . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH);
}
}
foreach ($table->getIndexes() as $thing) {
if ((!$sourceTable instanceof Table || !$sourceTable->hasIndex($thing->getName()))
&& \strlen($thing->getName()) > $MAX_NAME_LENGTH
) {
throw new \InvalidArgumentException('Index name "' . $table->getName() . '"."' . $thing->getName() . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH);
}
}
foreach ($table->getForeignKeys() as $thing) {
if ((!$sourceTable instanceof Table || !$sourceTable->hasForeignKey($thing->getName()))
&& \strlen($thing->getName()) > $MAX_NAME_LENGTH
) {
throw new \InvalidArgumentException('Foreign key name "' . $table->getName() . '"."' . $thing->getName() . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH);
}
}
$primaryKey = $table->getPrimaryKey();
// only check if there is a primary key
// and there was non in the old table or there was no old table
if ($primaryKey !== null && ($sourceTable === null || $sourceTable->getPrimaryKey() === null)) {
$indexName = strtolower($primaryKey->getName());
$isUsingDefaultName = $indexName === 'primary';
// This is the default name when using postgres - we use this for length comparison
// as this is the longest default names for the DB engines provided by doctrine
$defaultName = strtolower($table->getName() . '_pkey');
if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_POSTGRES) {
$isUsingDefaultName = $defaultName === $indexName;
if ($isUsingDefaultName) {
$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
return $sequence->getName() !== $sequenceName;
});
}
} elseif ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
$isUsingDefaultName = strtolower($table->getName() . '_seq') === $indexName;
}
if (!$isUsingDefaultName && \strlen($indexName) > $MAX_NAME_LENGTH) {
throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH);
}
if ($isUsingDefaultName && \strlen($defaultName) + $prefixLength > $MAX_NAME_LENGTH) {
throw new \InvalidArgumentException('Primary index name on "' . $table->getName() . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH);
}
}
}
foreach ($sequences as $sequence) {
if (!$sourceSchema->hasSequence($sequence->getName())
&& \strlen($sequence->getName()) > $MAX_NAME_LENGTH
) {
throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" exceeds the maximum length of ' . $MAX_NAME_LENGTH);
}
}
}
/**
* Enforces some data conventions to make sure tables can be used on Oracle SQL.
*
* Data constraints:
* - Tables need a primary key (Not specific to Oracle, but required for performant clustering support)
* - Columns with "NotNull" can not have empty string as default value
* - Columns with "NotNull" can not have number 0 as default value
* - Columns with type "bool" (which is in fact integer of length 1) can not be "NotNull" as it can not store 0/false
* - Columns with type "string" can not be longer than 4.000 characters, use "text" instead
*
* @see https://github.com/nextcloud/documentation/blob/master/developer_manual/basics/storage/database.rst
* @throws \Doctrine\DBAL\Exception
*/
public function ensureOracleConstraints(Schema $sourceSchema, Schema $targetSchema): void {
$sequences = $targetSchema->getSequences();
foreach ($targetSchema->getTables() as $table) {
try {
$sourceTable = $sourceSchema->getTable($table->getName());
} catch (SchemaException $e) {
$sourceTable = null;
}
foreach ($table->getColumns() as $column) {
// If the table doesn't exist OR if the column doesn't exist in the table
if (!$sourceTable instanceof Table || !$sourceTable->hasColumn($column->getName())) {
if ($column->getNotnull() && $column->getDefault() === ''
&& $sourceTable instanceof Table && !$sourceTable->hasColumn($column->getName())) {
// null and empty string are the same on Oracle SQL
throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $column->getName() . '" is NotNull, but has empty string or null as default.');
}
if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE
&& $column->getNotnull()
&& Type::lookupName($column->getType()) === Types::BOOLEAN
) {
// Oracle doesn't support boolean column with non-null value
// to still allow lighter DB schemas on other providers we force it to not null
// see https://github.com/nextcloud/server/pull/55156
$column->setNotnull(false);
}
$sourceColumn = null;
} else {
$sourceColumn = $sourceTable->getColumn($column->getName());
}
// If the column was just created OR the length changed OR the type changed
// we will NOT detect invalid length if the column is not modified
if (($sourceColumn === null || $sourceColumn->getLength() !== $column->getLength() || Type::lookupName($sourceColumn->getType()) !== Types::STRING)
&& $column->getLength() > 4000 && Type::lookupName($column->getType()) === Types::STRING) {
throw new \InvalidArgumentException('Column "' . $table->getName() . '"."' . $column->getName() . '" is type String, but exceeding the 4.000 length limit.');
}
}
$primaryKey = $table->getPrimaryKey();
if ($primaryKey instanceof Index && (!$sourceTable instanceof Table || $sourceTable->getPrimaryKey() === null)) {
$indexName = strtolower($primaryKey->getName());
$isUsingDefaultName = $indexName === 'primary';
if ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_POSTGRES) {
$defaultName = $table->getName() . '_pkey';
$isUsingDefaultName = strtolower($defaultName) === $indexName;
if ($isUsingDefaultName) {
$sequenceName = $table->getName() . '_' . implode('_', $primaryKey->getColumns()) . '_seq';
$sequences = array_filter($sequences, function (Sequence $sequence) use ($sequenceName) {
return $sequence->getName() !== $sequenceName;
});
}
} elseif ($this->connection->getDatabaseProvider() === IDBConnection::PLATFORM_ORACLE) {
$defaultName = $table->getName() . '_seq';
$isUsingDefaultName = strtolower($defaultName) === $indexName;
}
} elseif (!$primaryKey instanceof Index && !$sourceTable instanceof Table) {
/** @var LoggerInterface $logger */
$logger = \OCP\Server::get(LoggerInterface::class);
$logger->error('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups. This will throw an exception and not be installable in a future version of Nextcloud.');
// throw new \InvalidArgumentException('Table "' . $table->getName() . '" has no primary key and therefor will not behave sane in clustered setups.');
}
}
}
/**
* Ensure naming constraints
*
* Naming constraints:
* - Index, sequence and primary key names must be unique within a Postgres Schema
*
* Only on installation we want to break hard, so that all developers notice
* the bugs when installing the app on any database or CI, and can work on
* fixing their migrations before releasing a version incompatible with Postgres.
*
* In case of updates we might be running on production instances and the
* administrators being faced with the error would not know how to resolve it
* anyway. This can also happen with instances, that had the issue before the
* current update, so we don't want to make their life more complicated
* than needed.
*
* @param Schema $targetSchema
* @param bool $isInstalling
*/
public function ensureUniqueNamesConstraints(Schema $targetSchema, bool $isInstalling): void {
$constraintNames = [];
$sequences = $targetSchema->getSequences();
foreach ($targetSchema->getTables() as $table) {
foreach ($table->getIndexes() as $thing) {
$indexName = strtolower($thing->getName());
if ($indexName === 'primary' || $thing->isPrimary()) {
continue;
}
if (isset($constraintNames[$thing->getName()])) {
if ($isInstalling) {
throw new \InvalidArgumentException('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$this->logErrorOrWarning('Index name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$constraintNames[$thing->getName()] = $table->getName();
}
foreach ($table->getForeignKeys() as $thing) {
if (isset($constraintNames[$thing->getName()])) {
if ($isInstalling) {
throw new \InvalidArgumentException('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$this->logErrorOrWarning('Foreign key name "' . $thing->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$constraintNames[$thing->getName()] = $table->getName();
}
$primaryKey = $table->getPrimaryKey();
if ($primaryKey instanceof Index) {
$indexName = strtolower($primaryKey->getName());
if ($indexName === 'primary') {
continue;
}
if (isset($constraintNames[$indexName])) {
if ($isInstalling) {
throw new \InvalidArgumentException('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$this->logErrorOrWarning('Primary index name "' . $indexName . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$constraintNames[$indexName] = $table->getName();
}
}
foreach ($sequences as $sequence) {
if (isset($constraintNames[$sequence->getName()])) {
if ($isInstalling) {
throw new \InvalidArgumentException('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$this->logErrorOrWarning('Sequence name "' . $sequence->getName() . '" for table "' . $table->getName() . '" collides with the constraint on table "' . $constraintNames[$thing->getName()] . '".');
}
$constraintNames[$sequence->getName()] = 'sequence';
}
}
protected function logErrorOrWarning(string $log): void {
if ($this->output instanceof SimpleOutput) {
$this->output->warning($log);
} else {
$this->logger->error($log);
}
}
private function ensureMigrationsAreLoaded(): void {
if (empty($this->migrations)) {
$this->migrations = $this->findMigrations();
}
}
}