From a81759b0f17087abe2a0dd5fd69c1c48a5467afc Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 2 Feb 2023 16:11:59 +0200 Subject: [PATCH] dbal ref --- .../Database/DBAL/Driver/PDO/MySQL/Driver.php | 109 ------ .../DBAL/Factories/MysqlConnectionFactory.php | 134 ------- .../DBAL/Platforms/MariaDb1027Platform.php | 55 --- .../DBAL/Platforms/MySQL57Platform.php | 38 -- .../DBAL/Platforms/MySQL80Platform.php | 38 -- .../Database/DBAL/Platforms/MySQLPlatform.php | 38 -- .../Utils/Database/DBAL/Schema/Comparator.php | 353 ------------------ .../DBAL/Schema/MySQLSchemaManager.php | 230 ------------ .../DBAL/Traits/Platforms/MySQLPlatform.php | 298 --------------- .../DBAL/Traits/Schema/Comparator.php | 102 ----- .../{DBAL => Dbal}/ConnectionFactory.php | 2 +- .../ConnectionFactoryFactory.php | 2 +- .../Dbal/Factories/MysqlConnectionFactory.php | 85 +++++ .../Factories/PostgresqlConnectionFactory.php | 58 ++- .../{DBAL => Dbal}/Types/LongtextType.php | 2 +- .../{DBAL => Dbal}/Types/MediumtextType.php | 2 +- .../Espo/Core/Utils/Database/Helper.php | 2 +- .../Core/Utils/Database/Schema/Builder.php | 4 + .../Core/Utils/Database/Schema/Column.php | 14 + .../MysqlColumnPreparator.php | 26 +- .../Utils/Database/Schema/DiffModifier.php | 311 +++++++++++++++ .../Utils/Database/Schema/SchemaManager.php | 117 +++--- .../Espo/Resources/metadata/app/database.json | 8 +- composer.json | 2 +- composer.lock | 40 +- tests/integration/Core/Tester.php | 13 +- .../Utils/Database/AutoIncrementFieldTest.php | 24 +- .../Espo/Core/Utils/Database/Base.php | 2 +- .../Espo/Core/Utils/Database/HelperTest.php | 54 +-- .../Core/Utils/Database/TextFieldTest.php | 2 + .../Core/Utils/Database/VarcharFieldTest.php | 14 + 31 files changed, 610 insertions(+), 1569 deletions(-) delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Driver/PDO/MySQL/Driver.php delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Factories/MysqlConnectionFactory.php delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Platforms/MariaDb1027Platform.php delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Platforms/MySQL57Platform.php delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Platforms/MySQL80Platform.php delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Platforms/MySQLPlatform.php delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Schema/Comparator.php delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Schema/MySQLSchemaManager.php delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Traits/Platforms/MySQLPlatform.php delete mode 100644 application/Espo/Core/Utils/Database/DBAL/Traits/Schema/Comparator.php rename application/Espo/Core/Utils/Database/{DBAL => Dbal}/ConnectionFactory.php (97%) rename application/Espo/Core/Utils/Database/{DBAL => Dbal}/ConnectionFactoryFactory.php (98%) create mode 100644 application/Espo/Core/Utils/Database/Dbal/Factories/MysqlConnectionFactory.php rename application/Espo/Core/Utils/Database/{DBAL => Dbal}/Factories/PostgresqlConnectionFactory.php (66%) rename application/Espo/Core/Utils/Database/{DBAL => Dbal}/Types/LongtextType.php (97%) rename application/Espo/Core/Utils/Database/{DBAL => Dbal}/Types/MediumtextType.php (97%) create mode 100644 application/Espo/Core/Utils/Database/Schema/DiffModifier.php diff --git a/application/Espo/Core/Utils/Database/DBAL/Driver/PDO/MySQL/Driver.php b/application/Espo/Core/Utils/Database/DBAL/Driver/PDO/MySQL/Driver.php deleted file mode 100644 index 7f87c93c13..0000000000 --- a/application/Espo/Core/Utils/Database/DBAL/Driver/PDO/MySQL/Driver.php +++ /dev/null @@ -1,109 +0,0 @@ -constructPdoDsn($params), - $params['user'] ?? '', - $params['password'] ?? '', - $driverOptions - ); - - return new Connection($pdo); - } - - /** - * Constructs the MySQL PDO DSN. - * - * @param mixed[] $params - * @return string The DSN. - */ - protected function constructPdoDsn(array $params) - { - $dsn = 'mysql:'; - if (isset($params['host']) && $params['host'] !== '') { - $dsn .= 'host=' . $params['host'] . ';'; - } - - if (isset($params['port'])) { - $dsn .= 'port=' . $params['port'] . ';'; - } - - if (isset($params['dbname'])) { - $dsn .= 'dbname=' . $params['dbname'] . ';'; - } - - if (isset($params['unix_socket'])) { - $dsn .= 'unix_socket=' . $params['unix_socket'] . ';'; - } - - if (isset($params['charset'])) { - $dsn .= 'charset=' . $params['charset'] . ';'; - } - - return $dsn; - } - - // Espo: requires for the issue https://github.com/doctrine/dbal/issues/4496 - public function getSchemaManager(MySQLDriverConnection $conn, AbstractPlatform $platform) - { - assert($platform instanceof MySQLPlatform); - return new MySQLSchemaManager($conn, $platform); - } - // End: espo -} diff --git a/application/Espo/Core/Utils/Database/DBAL/Factories/MysqlConnectionFactory.php b/application/Espo/Core/Utils/Database/DBAL/Factories/MysqlConnectionFactory.php deleted file mode 100644 index 16a3d5e90c..0000000000 --- a/application/Espo/Core/Utils/Database/DBAL/Factories/MysqlConnectionFactory.php +++ /dev/null @@ -1,134 +0,0 @@ -> */ - private $driverClassNameMap = [ - 'mysqli' => 'Doctrine\\DBAL\\Driver\\Mysqli\\Driver', // @todo Revise usage. - 'pdo_mysql' => PDOMySQLDriver::class, - ]; - - /** @var array> */ - private $customPlatformClassNameMap = [ - 'MariaDb1027Platform' => 'Espo\\Core\\Utils\\Database\\DBAL\\Platforms\\MariaDb1027Platform', - 'MySQL57Platform' => 'Espo\\Core\\Utils\\Database\\DBAL\\Platforms\\MySQL57Platform', - 'MySQL80Platform' => 'Espo\\Core\\Utils\\Database\\DBAL\\Platforms\\MySQL80Platform', - 'MySQLPlatform' => 'Espo\\Core\\Utils\\Database\\DBAL\\Platforms\\MySQLPlatform', - ]; - - public function __construct( - private PDO $pdo, - private Config $config - ) {} - - /** - * @throws DBALException - */ - public function create(DatabaseParams $databaseParams): Connection - { - $driver = $this->createDriver(); - - $version = $this->getFullDatabaseVersion() ?? ''; - $platform = $driver->createDatabasePlatformForVersion($version); - - $params = [ - 'platform' => $this->handlePlatform($platform), - 'host' => $databaseParams->getHost(), - 'port' => $databaseParams->getPort(), - 'dbname' => $databaseParams->getName(), - 'charset' => $databaseParams->getCharset(), - 'user' => $databaseParams->getUsername(), - 'password' => $databaseParams->getPassword(), - 'driverOptions' => PdoOptions::getOptionsFromDatabaseParams($databaseParams), - ]; - - return new Connection($params, $driver); - } - - private function createDriver(): Driver - { - $driverName = $this->config->get('database.driver') ?? 'pdo_mysql'; - - $driverClass = $this->driverClassNameMap[$driverName] ?? null; - - if (!$driverClass) { - throw new RuntimeException('Unknown database driver.'); - } - - return new $driverClass(); - } - - private function handlePlatform(AbstractPlatform $platform): AbstractPlatform - { - $reflect = new ReflectionClass($platform); - - $platformClass = $reflect->getShortName(); - - if (isset($this->customPlatformClassNameMap[$platformClass])) { - $className = $this->customPlatformClassNameMap[$platformClass]; - - return new $className(); - } - - return $platform; - } - - private function getFullDatabaseVersion(): ?string - { - $sth = $this->pdo->prepare("select version()"); - - $sth->execute(); - - /** @var string|null|false $result */ - $result = $sth->fetchColumn(); - - if ($result === false || $result === null) { - return null; - } - - return $result; - } -} diff --git a/application/Espo/Core/Utils/Database/DBAL/Platforms/MariaDb1027Platform.php b/application/Espo/Core/Utils/Database/DBAL/Platforms/MariaDb1027Platform.php deleted file mode 100644 index fe233ac919..0000000000 --- a/application/Espo/Core/Utils/Database/DBAL/Platforms/MariaDb1027Platform.php +++ /dev/null @@ -1,55 +0,0 @@ -doctrineTypeMapping['json'] = Types::JSON; - } -} diff --git a/application/Espo/Core/Utils/Database/DBAL/Platforms/MySQL57Platform.php b/application/Espo/Core/Utils/Database/DBAL/Platforms/MySQL57Platform.php deleted file mode 100644 index 924e179052..0000000000 --- a/application/Espo/Core/Utils/Database/DBAL/Platforms/MySQL57Platform.php +++ /dev/null @@ -1,38 +0,0 @@ -toArray(); - $properties2 = $column2->toArray(); - - $changedProperties = []; - - if (get_class($properties1['type']) !== get_class($properties2['type'])) { - // Espo - $this->espoFixTypeDiff($changedProperties, $column1, $column2); - // Espo: end - } - - foreach (['notnull', 'unsigned', 'autoincrement'] as $property) { - if ($properties1[$property] === $properties2[$property]) { - continue; - } - - $changedProperties[] = $property; - } - - // Null values need to be checked additionally as they tell whether to create or drop a default value. - // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation. - if ( - ($properties1['default'] === null) !== ($properties2['default'] === null) - || $properties1['default'] != $properties2['default'] - ) { - $changedProperties[] = 'default'; - } - - if ( - ($properties1['type'] instanceof Types\StringType && ! $properties1['type'] instanceof Types\GuidType) || - $properties1['type'] instanceof Types\BinaryType - ) { - // check if value of length is set at all, default value assumed otherwise. - $length1 = $properties1['length'] ?? 255; - $length2 = $properties2['length'] ?? 255; - - // Espo: column length can only be increased - /*if ($length1 !== $length2) { - $changedProperties[] = 'length'; - }*/ - if ($length2 > $length1) { - $changedProperties[] = 'length'; - } - if ($length2 < $length1) { - $column2->setLength($length1); - } - // Espo: end - - if ($properties1['fixed'] !== $properties2['fixed']) { - $changedProperties[] = 'fixed'; - } - } elseif ($properties1['type'] instanceof Types\DecimalType) { - if (($properties1['precision'] ?? 10) !== ($properties2['precision'] ?? 10)) { - $changedProperties[] = 'precision'; - } - - if ($properties1['scale'] !== $properties2['scale']) { - $changedProperties[] = 'scale'; - } - } - - // A null value and an empty string are actually equal for a comment so they should not trigger a change. - if ( - $properties1['comment'] !== $properties2['comment'] && - ! ($properties1['comment'] === null && $properties2['comment'] === '') && - ! ($properties2['comment'] === null && $properties1['comment'] === '') - ) { - $changedProperties[] = 'comment'; - } - - $customOptions1 = $column1->getCustomSchemaOptions(); - $customOptions2 = $column2->getCustomSchemaOptions(); - - foreach (array_merge(array_keys($customOptions1), array_keys($customOptions2)) as $key) { - if (! array_key_exists($key, $properties1) || ! array_key_exists($key, $properties2)) { - $changedProperties[] = $key; - } elseif ($properties1[$key] !== $properties2[$key]) { - $changedProperties[] = $key; - } - } - - $platformOptions1 = $column1->getPlatformOptions(); - $platformOptions2 = $column2->getPlatformOptions(); - - foreach (array_keys(array_intersect_key($platformOptions1, $platformOptions2)) as $key) { - if ($properties1[$key] === $properties2[$key]) { - continue; - } - - // Espo: skip collation changes - if ($key == 'collation') { - $column2->setPlatformOption('collation', $platformOptions1['collation']); - continue; - } - // Espo: end - - $changedProperties[] = $key; - } - - return array_unique($changedProperties); - } - - public function diffTable(Table $fromTable, Table $toTable) - { - $changes = 0; - $tableDifferences = new TableDiff($fromTable->getName()); - $tableDifferences->fromTable = $fromTable; - - $fromTableColumns = $fromTable->getColumns(); - $toTableColumns = $toTable->getColumns(); - - /* See if all the columns in "from" table exist in "to" table */ - foreach ($toTableColumns as $columnName => $column) { - if ($fromTable->hasColumn($columnName)) { - continue; - } - - $tableDifferences->addedColumns[$columnName] = $column; - $changes++; - } - - /* See if there are any removed columns in "to" table */ - foreach ($fromTableColumns as $columnName => $column) { - // See if column is removed in "to" table. - if (! $toTable->hasColumn($columnName)) { - $tableDifferences->removedColumns[$columnName] = $column; - $changes++; - continue; - } - - // See if column has changed properties in "to" table. - $changedProperties = $this->diffColumn($column, $toTable->getColumn($columnName)); - - if (count($changedProperties) === 0) { - continue; - } - - $columnDiff = new ColumnDiff($column->getName(), $toTable->getColumn($columnName), $changedProperties); - - $columnDiff->fromColumn = $column; - $tableDifferences->changedColumns[$column->getName()] = $columnDiff; - $changes++; - } - - $this->detectColumnRenamings($tableDifferences); - - $fromTableIndexes = $fromTable->getIndexes(); - $toTableIndexes = $toTable->getIndexes(); - - /* See if all the indexes in "from" table exist in "to" table */ - foreach ($toTableIndexes as $indexName => $index) { - if (($index->isPrimary() && $fromTable->hasPrimaryKey()) || $fromTable->hasIndex($indexName)) { - continue; - } - - $tableDifferences->addedIndexes[$indexName] = $index; - $changes++; - } - - /* See if there are any removed indexes in "to" table */ - foreach ($fromTableIndexes as $indexName => $index) { - // See if index is removed in "to" table. - if ( - ($index->isPrimary() && ! $toTable->hasPrimaryKey()) || - ! $index->isPrimary() && ! $toTable->hasIndex($indexName) - ) { - $tableDifferences->removedIndexes[$indexName] = $index; - $changes++; - continue; - } - - // See if index has changed in "to" table. - $toTableIndex = $index->isPrimary() ? $toTable->getPrimaryKey() : $toTable->getIndex($indexName); - assert($toTableIndex instanceof Index); - - if (! $this->diffIndex($index, $toTableIndex)) { - continue; - } - - $tableDifferences->changedIndexes[$indexName] = $toTableIndex; - $changes++; - } - - $this->detectIndexRenamings($tableDifferences); - - $fromForeignKeys = $fromTable->getForeignKeys(); - $toForeignKeys = $toTable->getForeignKeys(); - - foreach ($fromForeignKeys as $fromKey => $fromConstraint) { - foreach ($toForeignKeys as $toKey => $toConstraint) { - if ($this->diffForeignKey($fromConstraint, $toConstraint) === false) { - unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]); - } else { - if (strtolower($fromConstraint->getName()) === strtolower($toConstraint->getName())) { - $tableDifferences->changedForeignKeys[] = $toConstraint; - $changes++; - unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]); - } - } - } - } - - foreach ($fromForeignKeys as $fromConstraint) { - $tableDifferences->removedForeignKeys[] = $fromConstraint; - $changes++; - } - - foreach ($toForeignKeys as $toConstraint) { - $tableDifferences->addedForeignKeys[] = $toConstraint; - $changes++; - } - - return $changes > 0 ? $tableDifferences : false; - } - - /** - * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop - * however ambiguities between different possibilities should not lead to renaming at all. - * - * @return void - */ - private function detectColumnRenamings(TableDiff $tableDifferences) - { - $renameCandidates = []; - foreach ($tableDifferences->addedColumns as $addedColumnName => $addedColumn) { - foreach ($tableDifferences->removedColumns as $removedColumn) { - if (count($this->diffColumn($addedColumn, $removedColumn)) !== 0) { - continue; - } - - $renameCandidates[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName]; - } - } - - foreach ($renameCandidates as $candidateColumns) { - if (count($candidateColumns) !== 1) { - continue; - } - - [$removedColumn, $addedColumn] = $candidateColumns[0]; - $removedColumnName = strtolower($removedColumn->getName()); - $addedColumnName = strtolower($addedColumn->getName()); - - if (isset($tableDifferences->renamedColumns[$removedColumnName])) { - continue; - } - - $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn; - unset( - $tableDifferences->addedColumns[$addedColumnName], - $tableDifferences->removedColumns[$removedColumnName] - ); - } - } - - /** - * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop - * however ambiguities between different possibilities should not lead to renaming at all. - * - * @return void - */ - private function detectIndexRenamings(TableDiff $tableDifferences) - { - $renameCandidates = []; - - // Gather possible rename candidates by comparing each added and removed index based on semantics. - foreach ($tableDifferences->addedIndexes as $addedIndexName => $addedIndex) { - foreach ($tableDifferences->removedIndexes as $removedIndex) { - if ($this->diffIndex($addedIndex, $removedIndex)) { - continue; - } - - $renameCandidates[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName]; - } - } - - foreach ($renameCandidates as $candidateIndexes) { - // If the current rename candidate contains exactly one semantically equal index, - // we can safely rename it. - // Otherwise it is unclear if a rename action is really intended, - // therefore we let those ambiguous indexes be added/dropped. - if (count($candidateIndexes) !== 1) { - continue; - } - - [$removedIndex, $addedIndex] = $candidateIndexes[0]; - - $removedIndexName = strtolower($removedIndex->getName()); - $addedIndexName = strtolower($addedIndex->getName()); - - if (isset($tableDifferences->renamedIndexes[$removedIndexName])) { - continue; - } - - // Espo: skip index renaming - //$tableDifferences->renamedIndexes[$removedIndexName] = $addedIndex; - // Espo: end - unset( - $tableDifferences->addedIndexes[$addedIndexName], - $tableDifferences->removedIndexes[$removedIndexName] - ); - } - } -} diff --git a/application/Espo/Core/Utils/Database/DBAL/Schema/MySQLSchemaManager.php b/application/Espo/Core/Utils/Database/DBAL/Schema/MySQLSchemaManager.php deleted file mode 100644 index 8d506255c2..0000000000 --- a/application/Espo/Core/Utils/Database/DBAL/Schema/MySQLSchemaManager.php +++ /dev/null @@ -1,230 +0,0 @@ - "\0", - "\\'" => "'", - '\\"' => '"', - '\\b' => "\b", - '\\n' => "\n", - '\\r' => "\r", - '\\t' => "\t", - '\\Z' => "\x1a", - '\\\\' => '\\', - '\\%' => '%', - '\\_' => '_', - - // Internally, MariaDB escapes single quotes using the standard syntax - "''" => "'", - ]; - - /** - * {@inheritdoc} - */ - protected function _getPortableTableColumnDefinition($tableColumn) - { - $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); - - $dbType = strtolower($tableColumn['type']); - $dbType = strtok($dbType, '(), '); - assert(is_string($dbType)); - - $length = $tableColumn['length'] ?? strtok('(), '); - - $fixed = null; - - if (! isset($tableColumn['name'])) { - $tableColumn['name'] = ''; - } - - $scale = null; - $precision = null; - - $type = $this->_platform->getDoctrineTypeMapping($dbType); - - // In cases where not connected to a database DESCRIBE $table does not return 'Comment' - if (isset($tableColumn['comment'])) { - $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); - $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); - } - - switch ($dbType) { - case 'char': - case 'binary': - $fixed = true; - break; - - case 'float': - case 'double': - case 'real': - case 'numeric': - case 'decimal': - if ( - preg_match( - '([A-Za-z]+\(([0-9]+),([0-9]+)\))', - $tableColumn['type'], - $match - ) === 1 - ) { - $precision = $match[1]; - $scale = $match[2]; - $length = null; - } - - break; - - case 'tinytext': - $length = MySQLPlatform::LENGTH_LIMIT_TINYTEXT; - break; - - case 'text': - $length = MySQLPlatform::LENGTH_LIMIT_TEXT; - break; - - case 'mediumtext': - $length = MySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT; - break; - - case 'tinyblob': - $length = MySQLPlatform::LENGTH_LIMIT_TINYBLOB; - break; - - case 'blob': - $length = MySQLPlatform::LENGTH_LIMIT_BLOB; - break; - - case 'mediumblob': - $length = MySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB; - break; - - case 'tinyint': - case 'smallint': - case 'mediumint': - case 'int': - case 'integer': - case 'bigint': - case 'year': - $length = null; - break; - } - - if ($this->_platform instanceof MariaDb1027Platform) { - $columnDefault = $this->getMariaDb1027ColumnDefault($this->_platform, $tableColumn['default']); - } else { - $columnDefault = $tableColumn['default']; - } - - $options = [ - 'length' => $length !== null ? (int) $length : null, - 'unsigned' => strpos($tableColumn['type'], 'unsigned') !== false, - 'fixed' => (bool) $fixed, - 'default' => $columnDefault, - 'notnull' => $tableColumn['null'] !== 'YES', - 'scale' => null, - 'precision' => null, - 'autoincrement' => strpos($tableColumn['extra'], 'auto_increment') !== false, - 'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== '' - ? $tableColumn['comment'] - : null, - ]; - - if ($scale !== null && $precision !== null) { - $options['scale'] = (int) $scale; - $options['precision'] = (int) $precision; - } - - $column = new Column($tableColumn['field'], Type::getType($type), $options); - - if (isset($tableColumn['characterset'])) { - $column->setPlatformOption('charset', $tableColumn['characterset']); - } - - if (isset($tableColumn['collation'])) { - $column->setPlatformOption('collation', $tableColumn['collation']); - } - - return $column; - } - - private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault): ?string - { - if ($columnDefault === 'NULL' || $columnDefault === null) { - return null; - } - - if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches) === 1) { - return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES); - } - - switch ($columnDefault) { - case 'current_timestamp()': - return $platform->getCurrentTimestampSQL(); - - case 'curdate()': - return $platform->getCurrentDateSQL(); - - case 'curtime()': - return $platform->getCurrentTimeSQL(); - } - - return $columnDefault; - } -} diff --git a/application/Espo/Core/Utils/Database/DBAL/Traits/Platforms/MySQLPlatform.php b/application/Espo/Core/Utils/Database/DBAL/Traits/Platforms/MySQLPlatform.php deleted file mode 100644 index 8595abdc1a..0000000000 --- a/application/Espo/Core/Utils/Database/DBAL/Traits/Platforms/MySQLPlatform.php +++ /dev/null @@ -1,298 +0,0 @@ -getNewName(); - - if ($newName !== false) { - $queryParts[] = 'RENAME TO ' . $newName->getQuotedName($this); - } - - foreach ($diff->renamedColumns as $oldColumnName => $column) { - if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { - continue; - } - - // Espo: handle remained autoincrement column - if ($column->getAutoincrement()) { - $oldColumnOptions = array_diff_key($column->toArray(), array_flip(['name', 'type', 'collation'])); - $diff->removedColumns[$oldColumnName] = new Column($oldColumnName, $column->getType(), $oldColumnOptions); - - $columnName = $column->getQuotedName($this); - $diff->addedColumns[$columnName] = $column; - continue; - } - // Espo: end - - $oldColumnName = new Identifier($oldColumnName); - $columnArray = $column->toArray(); - $columnArray['comment'] = $this->getColumnComment($column); - - // Espo: do not rename the column - /* $queryParts[] = 'CHANGE ' . $oldColumnName->getQuotedName($this) . ' ' - $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); */ - $queryParts[] = 'ADD ' - . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); - // Espo: end - } - - foreach ($diff->addedColumns as $column) { - if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { - continue; - } - - $columnArray = array_merge($column->toArray(), [ - 'comment' => $this->getColumnComment($column), - ]); - - // Espo: Unable to create autoindex column in existing table fix - if ($column->getAutoincrement()) { - $columnArray['unique'] = true; - - $columnName = $column->getQuotedName($this); - - foreach ($diff->addedIndexes as $indexName => $index) { - if ($index->getColumns() === [$columnName]) { - $columnArray['uniqueDeclaration'] = $index->getName() . "(" . $columnName . ")"; - unset($diff->addedIndexes[$indexName]); - } - } - } - // Espo: end - - $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); - } - - // Espo: remove autoincrement column - $autoincrementRemovedIndexes = []; - // Espo: end - - foreach ($diff->removedColumns as $column) { - if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { - continue; - } - - // Espo: remove autoincrement option - if ($column->getAutoincrement()) { - - $columnName = $column->getQuotedName($this); - - $changedColumn = clone $column; - $changedColumn->setNotNull(false); - $changedColumn->setAutoincrement(false); - - $changedProperties = array( - 'notnull', - 'autoincrement', - ); - - $diff->changedColumns[$columnName] = new ColumnDiff($columnName, $changedColumn, $changedProperties, $column); - - foreach ($diff->removedIndexes as $indexName => $index) { - if ($index->getColumns() === [$columnName]) { - $autoincrementRemovedIndexes[$indexName] = $index; - unset($diff->removedIndexes[$indexName]); - } - } - } - // Espo: end - - // Espo: No need to delete the column - //$queryParts[] = 'DROP ' . $column->getQuotedName($this); - // Espo: end - } - - foreach ($diff->changedColumns as $columnDiff) { - if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { - continue; - } - - $column = $columnDiff->column; - $columnArray = $column->toArray(); - - // Don't propagate default value changes for unsupported column types. - if ( - $columnDiff->hasChanged('default') && - count($columnDiff->changedProperties) === 1 && - ($columnArray['type'] instanceof TextType || $columnArray['type'] instanceof BlobType) - ) { - continue; - } - - // Espo: Unable to create autoindex column in existing table fix - if ($column->getAutoincrement()) { - $columnArray['unique'] = true; - - $columnName = $column->getQuotedName($this); - - foreach ($diff->addedIndexes as $indexName => $index) { - if ($index->getColumns() === [$columnName]) { - $columnArray['uniqueDeclaration'] = $index->getName() . "(" . $columnName . ")"; - unset($diff->addedIndexes[$indexName]); - } - } - } - // Espo: end - - $columnArray['comment'] = $this->getColumnComment($column); - $queryParts[] = 'CHANGE ' . ($columnDiff->getOldColumnName()->getQuotedName($this)) . ' ' - . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); - } - - if (isset($diff->addedIndexes['primary'])) { - $keyColumns = array_unique(array_values($diff->addedIndexes['primary']->getColumns())); - $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; - unset($diff->addedIndexes['primary']); - } elseif (isset($diff->changedIndexes['primary'])) { - // Necessary in case the new primary key includes a new auto_increment column - foreach ($diff->changedIndexes['primary']->getColumns() as $columnName) { - if (isset($diff->addedColumns[$columnName]) && $diff->addedColumns[$columnName]->getAutoincrement()) { - $keyColumns = array_unique(array_values($diff->changedIndexes['primary']->getColumns())); - $queryParts[] = 'DROP PRIMARY KEY'; - $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; - unset($diff->changedIndexes['primary']); - break; - } - } - } - - $sql = []; - $tableSql = []; - - if (! $this->onSchemaAlterTable($diff, $tableSql)) { - if (count($queryParts) > 0) { - $sql[] = 'ALTER TABLE ' . $diff->getName($this)->getQuotedName($this) . ' ' - . implode(', ', $queryParts); - } - - // Espo: remove autoincrement column - if (!empty($autoincrementRemovedIndexes)) { - $tableName = $diff->getName($this)->getQuotedName($this); - - foreach ($autoincrementRemovedIndexes as $index) { - $sql[] = $this->getDropIndexSQL($index, $tableName); - } - } - // Espo: end - - $sql = array_merge( - $this->getPreAlterTableIndexForeignKeySQL($diff), - $sql, - $this->getPostAlterTableIndexForeignKeySQL($diff) - ); - } - - return array_merge($sql, $tableSql, $columnSql); - } - - public function getClobTypeDeclarationSQL(array $column) - { - if (! empty($column['length']) && is_numeric($column['length'])) { - $length = $column['length']; - - if ($length <= static::LENGTH_LIMIT_TINYTEXT) { - return 'TINYTEXT'; - } - - if ($length <= static::LENGTH_LIMIT_TEXT) { - return 'TEXT'; - } - - if ($length > static::LENGTH_LIMIT_MEDIUMTEXT) { - return 'LONGTEXT'; - } - } - - return 'MEDIUMTEXT'; - } - - protected function _getCreateTableSQL($name, array $columns, array $options = []) - { - if (!isset($options['charset'])) { - $options['charset'] = 'utf8mb4'; - } - - return parent::_getCreateTableSQL($name, $columns, $options); - } - - public function getColumnDeclarationSQL($name, array $column) - { - if (isset($column['columnDefinition'])) { - $declaration = $this->getCustomTypeDeclarationSQL($column); - } else { - $default = $this->getDefaultValueDeclarationSQL($column); - - $charset = ! empty($column['charset']) ? - ' ' . $this->getColumnCharsetDeclarationSQL($column['charset']) : ''; - - $collation = ! empty($column['collation']) ? - ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; - - $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; - - $unique = ! empty($column['unique']) ? - ' ' . $this->getUniqueFieldDeclarationSQL() : ''; - - $check = ! empty($column['check']) ? ' ' . $column['check'] : ''; - - $typeDecl = $column['type']->getSQLDeclaration($column, $this); - $declaration = $typeDecl . $charset . $default . $notnull . $unique . $check . $collation; - - // Espo: Unable to create autoindex column in existing table fix - if (!empty($column['uniqueDeclaration'])) { - $declaration = $typeDecl . $charset . $default . $notnull . $check . $collation - . ", ADD". $unique . " " . $column['uniqueDeclaration']; - } - // Espo: end - - if ($this->supportsInlineColumnComments() && isset($column['comment']) && $column['comment'] !== '') { - $declaration .= ' ' . $this->getInlineColumnCommentSQL($column['comment']); - } - } - - return $name . ' ' . $declaration; - } -} diff --git a/application/Espo/Core/Utils/Database/DBAL/Traits/Schema/Comparator.php b/application/Espo/Core/Utils/Database/DBAL/Traits/Schema/Comparator.php deleted file mode 100644 index 1f67bd7255..0000000000 --- a/application/Espo/Core/Utils/Database/DBAL/Traits/Schema/Comparator.php +++ /dev/null @@ -1,102 +0,0 @@ -getColumnDbType($column1); - $column2DbType = $this->getColumnDbType($column2); - - if ($column1DbType == $column2DbType) { - return; - } - - if (! $column1->getType() instanceof TextType) { - $changedProperties[] = 'type'; - - return; - } - - $column1DbLength = $this->getColumnDbLength($column1); - $column2DbLength = $this->getColumnDbLength($column2); - - if ( - $column1DbLength && $column2DbLength && - $column2DbLength > $column1DbLength - ) { - $changedProperties[] = 'type'; - } - } - - /** - * @return string - */ - private function getColumnDbType(Column $column) - { - $dbType = $column->getType()->getName(); - - // Here was db type obtained by dbal type. - // Can't obtain it from Connection as the mapping is not public. - - return strtoupper($dbType); - } - - /** - * @return ?int - */ - private function getColumnDbLength(Column $column) - { - $dbType = $this->getColumnDbType($column); - - switch ($dbType) { - case 'LONGTEXT': - return 4294967295; - } - - $constName = '\\Doctrine\\DBAL\\Platforms\\MySQLPlatform::LENGTH_LIMIT_' . $dbType; - - if (defined($constName)) { - return constant($constName); - } - - return null; - } -} diff --git a/application/Espo/Core/Utils/Database/DBAL/ConnectionFactory.php b/application/Espo/Core/Utils/Database/Dbal/ConnectionFactory.php similarity index 97% rename from application/Espo/Core/Utils/Database/DBAL/ConnectionFactory.php rename to application/Espo/Core/Utils/Database/Dbal/ConnectionFactory.php index 789b50fdcf..906eec1a6b 100644 --- a/application/Espo/Core/Utils/Database/DBAL/ConnectionFactory.php +++ b/application/Espo/Core/Utils/Database/Dbal/ConnectionFactory.php @@ -27,7 +27,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\Utils\Database\DBAL; +namespace Espo\Core\Utils\Database\Dbal; use Doctrine\DBAL\Connection; use Espo\ORM\DatabaseParams; diff --git a/application/Espo/Core/Utils/Database/DBAL/ConnectionFactoryFactory.php b/application/Espo/Core/Utils/Database/Dbal/ConnectionFactoryFactory.php similarity index 98% rename from application/Espo/Core/Utils/Database/DBAL/ConnectionFactoryFactory.php rename to application/Espo/Core/Utils/Database/Dbal/ConnectionFactoryFactory.php index 2376aea513..b0a1762d3b 100644 --- a/application/Espo/Core/Utils/Database/DBAL/ConnectionFactoryFactory.php +++ b/application/Espo/Core/Utils/Database/Dbal/ConnectionFactoryFactory.php @@ -27,7 +27,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\Utils\Database\DBAL; +namespace Espo\Core\Utils\Database\Dbal; use Espo\Core\Binding\BindingContainerBuilder; use Espo\Core\InjectableFactory; diff --git a/application/Espo/Core/Utils/Database/Dbal/Factories/MysqlConnectionFactory.php b/application/Espo/Core/Utils/Database/Dbal/Factories/MysqlConnectionFactory.php new file mode 100644 index 0000000000..759d865927 --- /dev/null +++ b/application/Espo/Core/Utils/Database/Dbal/Factories/MysqlConnectionFactory.php @@ -0,0 +1,85 @@ +getHost() || !$databaseParams->getName()) { + throw new RuntimeException("No required database params."); + } + + $params = [ + 'pdo' => $this->pdo, + 'host' => $databaseParams->getHost(), + 'dbname' => $databaseParams->getName(), + 'driverOptions' => PdoOptions::getOptionsFromDatabaseParams($databaseParams), + ]; + + if ($databaseParams->getPort() !== null) { + $params['port'] = $databaseParams->getPort(); + } + + if ($databaseParams->getUsername() !== null) { + $params['user'] = $databaseParams->getUsername(); + } + + if ($databaseParams->getPassword() !== null) { + $params['password'] = $databaseParams->getPassword(); + } + + if ($databaseParams->getCharset() !== null) { + $params['charset'] = $databaseParams->getCharset(); + } + + return new Connection($params, $driver); + } +} diff --git a/application/Espo/Core/Utils/Database/DBAL/Factories/PostgresqlConnectionFactory.php b/application/Espo/Core/Utils/Database/Dbal/Factories/PostgresqlConnectionFactory.php similarity index 66% rename from application/Espo/Core/Utils/Database/DBAL/Factories/PostgresqlConnectionFactory.php rename to application/Espo/Core/Utils/Database/Dbal/Factories/PostgresqlConnectionFactory.php index 6729b9c585..3a98e8d87f 100644 --- a/application/Espo/Core/Utils/Database/DBAL/Factories/PostgresqlConnectionFactory.php +++ b/application/Espo/Core/Utils/Database/Dbal/Factories/PostgresqlConnectionFactory.php @@ -27,17 +27,17 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\Utils\Database\DBAL\Factories; +namespace Espo\Core\Utils\Database\Dbal\Factories; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver\PDO\PgSQL\Driver as PostgreSQLDriver; use Doctrine\DBAL\Exception as DBALException; -use Doctrine\DBAL\VersionAwarePlatformDriver as Driver; -use Espo\Core\Utils\Database\DBAL\ConnectionFactory; +use Espo\Core\Utils\Database\Dbal\ConnectionFactory; use Espo\ORM\DatabaseParams; use Espo\ORM\PDO\Options as PdoOptions; use PDO; +use RuntimeException; class PostgresqlConnectionFactory implements ConnectionFactory { @@ -50,47 +50,35 @@ class PostgresqlConnectionFactory implements ConnectionFactory */ public function create(DatabaseParams $databaseParams): Connection { - $driver = $this->createDriver(); + $driver = new PostgreSQLDriver(); - $version = $this->getDatabaseVersion() ?? ''; - $platform = $driver->createDatabasePlatformForVersion($version); + if (!$databaseParams->getHost() || !$databaseParams->getName()) { + throw new RuntimeException("No required database params."); + } $params = [ - 'platform' => $platform, + 'pdo' => $this->pdo, 'host' => $databaseParams->getHost(), - 'port' => $databaseParams->getPort(), 'dbname' => $databaseParams->getName(), - 'charset' => $databaseParams->getCharset(), - 'user' => $databaseParams->getUsername(), - 'password' => $databaseParams->getPassword(), 'driverOptions' => PdoOptions::getOptionsFromDatabaseParams($databaseParams), ]; - return new Connection($params, $driver); - } - - private function createDriver(): Driver - { - $driverClass = PostgreSQLDriver::class; - - return new $driverClass(); - } - - private function getDatabaseVersion(): ?string - { - $sql = "SHOW server_version"; - - $sth = $this->pdo->prepare($sql); - $sth->execute(); - - $row = $sth->fetch(PDO::FETCH_NUM); - - $value = $row[0] ?: null; - - if ($value === null) { - return null; + if ($databaseParams->getPort() !== null) { + $params['port'] = $databaseParams->getPort(); } - return (string) $value; + if ($databaseParams->getUsername() !== null) { + $params['user'] = $databaseParams->getUsername(); + } + + if ($databaseParams->getPassword() !== null) { + $params['password'] = $databaseParams->getPassword(); + } + + if ($databaseParams->getCharset() !== null) { + $params['charset'] = $databaseParams->getCharset(); + } + + return new Connection($params, $driver); } } diff --git a/application/Espo/Core/Utils/Database/DBAL/Types/LongtextType.php b/application/Espo/Core/Utils/Database/Dbal/Types/LongtextType.php similarity index 97% rename from application/Espo/Core/Utils/Database/DBAL/Types/LongtextType.php rename to application/Espo/Core/Utils/Database/Dbal/Types/LongtextType.php index 43204cfa17..2d6f66b030 100644 --- a/application/Espo/Core/Utils/Database/DBAL/Types/LongtextType.php +++ b/application/Espo/Core/Utils/Database/Dbal/Types/LongtextType.php @@ -27,7 +27,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\Utils\Database\DBAL\Types; +namespace Espo\Core\Utils\Database\Dbal\Types; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\TextType; diff --git a/application/Espo/Core/Utils/Database/DBAL/Types/MediumtextType.php b/application/Espo/Core/Utils/Database/Dbal/Types/MediumtextType.php similarity index 97% rename from application/Espo/Core/Utils/Database/DBAL/Types/MediumtextType.php rename to application/Espo/Core/Utils/Database/Dbal/Types/MediumtextType.php index f08a47ca80..497f9353ab 100644 --- a/application/Espo/Core/Utils/Database/DBAL/Types/MediumtextType.php +++ b/application/Espo/Core/Utils/Database/Dbal/Types/MediumtextType.php @@ -27,7 +27,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\Utils\Database\DBAL\Types; +namespace Espo\Core\Utils\Database\Dbal\Types; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\TextType; diff --git a/application/Espo/Core/Utils/Database/Helper.php b/application/Espo/Core/Utils/Database/Helper.php index b46877379c..fb22129bd9 100644 --- a/application/Espo/Core/Utils/Database/Helper.php +++ b/application/Espo/Core/Utils/Database/Helper.php @@ -33,7 +33,7 @@ use Doctrine\DBAL\Connection as DbalConnection; use Espo\Core\ORM\DatabaseParamsFactory; use Espo\Core\ORM\PDO\PDOFactoryFactory; -use Espo\Core\Utils\Database\DBAL\ConnectionFactoryFactory as DBALConnectionFactoryFactory; +use Espo\Core\Utils\Database\Dbal\ConnectionFactoryFactory as DBALConnectionFactoryFactory; use Espo\ORM\DatabaseParams; use PDO; diff --git a/application/Espo/Core/Utils/Database/Schema/Builder.php b/application/Espo/Core/Utils/Database/Schema/Builder.php index 813b6fc921..4652c10e9d 100644 --- a/application/Espo/Core/Utils/Database/Schema/Builder.php +++ b/application/Espo/Core/Utils/Database/Schema/Builder.php @@ -417,6 +417,10 @@ class Builder $result['platformOptions']['collation'] = $column->getCollation(); } + if ($column->getCharset()) { + $result['platformOptions']['charset'] = $column->getCharset(); + } + return $result; } diff --git a/application/Espo/Core/Utils/Database/Schema/Column.php b/application/Espo/Core/Utils/Database/Schema/Column.php index 5c7402eea6..27138977d6 100644 --- a/application/Espo/Core/Utils/Database/Schema/Column.php +++ b/application/Espo/Core/Utils/Database/Schema/Column.php @@ -39,6 +39,7 @@ class Column private ?int $scale = null; private ?bool $unsigned = null; private ?string $collation = null; + private ?string $charset = null; private function __construct( private string $name, @@ -100,6 +101,11 @@ class Column return $this->collation; } + public function getCharset(): ?string + { + return $this->charset; + } + public function withNotNull(bool $notNull = true): self { $obj = clone $this; @@ -166,4 +172,12 @@ class Column return $obj; } + + public function withCharset(?string $charset): self + { + $obj = clone $this; + $obj->charset = $charset; + + return $obj; + } } diff --git a/application/Espo/Core/Utils/Database/Schema/ColumnPreparators/MysqlColumnPreparator.php b/application/Espo/Core/Utils/Database/Schema/ColumnPreparators/MysqlColumnPreparator.php index 49a2150e5e..bda04dd235 100644 --- a/application/Espo/Core/Utils/Database/Schema/ColumnPreparators/MysqlColumnPreparator.php +++ b/application/Espo/Core/Utils/Database/Schema/ColumnPreparators/MysqlColumnPreparator.php @@ -30,7 +30,8 @@ namespace Espo\Core\Utils\Database\Schema\ColumnPreparators; use Doctrine\DBAL\Types\Types; -use Espo\Core\Utils\Database\DBAL\Types\MediumtextType; +use Espo\Core\Utils\Database\Dbal\Types\LongtextType; +use Espo\Core\Utils\Database\Dbal\Types\MediumtextType; use Espo\Core\Utils\Database\Helper; use Espo\Core\Utils\Database\Schema\Column; use Espo\Core\Utils\Database\Schema\ColumnPreparator; @@ -152,17 +153,34 @@ class MysqlColumnPreparator implements ColumnPreparator ->withUnsigned(); } + if ( + !in_array($columnType, [ + Types::STRING, + Types::TEXT, + MediumtextType::NAME, + LongtextType::NAME, + ]) + ) { + return $column; + } + $collation = $binary ? 'utf8mb4_bin' : 'utf8mb4_unicode_ci'; + $charset = 'utf8mb4'; + if ($mb3) { $collation = $binary ? - 'utf8_bin' : - 'utf8_unicode_ci'; + 'utf8mb3_bin' : + 'utf8mb3_unicode_ci'; + + $charset = 'utf8mb3'; } - return $column->withCollation($collation); + return $column + ->withCollation($collation) + ->withCharset($charset); } private function getMaxIndexLength(): int diff --git a/application/Espo/Core/Utils/Database/Schema/DiffModifier.php b/application/Espo/Core/Utils/Database/Schema/DiffModifier.php new file mode 100644 index 0000000000..52da572125 --- /dev/null +++ b/application/Espo/Core/Utils/Database/Schema/DiffModifier.php @@ -0,0 +1,311 @@ +removedTables = []; + + foreach ($diff->changedTables as $tableDiff) { + $reRun = $this->amendTableDiff($tableDiff, $secondRun) || $reRun; + } + + return $reRun; + } + + /** + * @throws DbalException + */ + private function amendTableDiff(TableDiff $tableDiff, bool $secondRun): bool + { + $reRun = false; + + /** + * @todo Leave only for MariaDB? + * MariaDB supports RENAME INDEX as of v10.5. + * Find out how long does it take to rename fo different databases. + */ + // Prevent index renaming as an operation may take a lot of time. + $tableDiff->renamedIndexes = []; + + foreach ($tableDiff->removedColumns as $name => $column) { + $reRun = $this->moveRemovedAutoincrementColumnToChanged($tableDiff, $column, $name) || $reRun; + } + + // Prevent column removal to prevent data loss. + $tableDiff->removedColumns = []; + + // Prevent column renaming as a not desired behavior. + foreach ($tableDiff->renamedColumns as $renamedColumn) { + $addedName = strtolower($renamedColumn->getName()); + $tableDiff->addedColumns[$addedName] = $renamedColumn; + } + + $tableDiff->renamedColumns = []; + + foreach ($tableDiff->addedColumns as $column) { + // Suppress autoincrement as need having a unique index first. + $reRun = $this->amendAddedColumnAutoincrement($column) || $reRun; + } + + foreach ($tableDiff->changedColumns as $name => $columnDiff) { + // Prevent decreasing length for string columns to prevent data loss. + $this->amendColumnDiffLength($tableDiff, $columnDiff, $name); + // Prevent longtext => mediumtext to prevent data loss. + $this->amendColumnDiffTextType($tableDiff, $columnDiff, $name); + // Prevent changing collation. + $this->amendColumnDiffCollation($tableDiff, $columnDiff, $name); + // Prevent changing charset. + $this->amendColumnDiffCharset($tableDiff, $columnDiff, $name); + // Prevent setting autoincrement in first run. + if (!$secondRun) { + $reRun = $this->amendColumnDiffAutoincrement($tableDiff, $columnDiff, $name) || $reRun; + } + } + + return $reRun; + } + + private function amendColumnDiffLength(TableDiff $tableDiff, ColumnDiff $columnDiff, string $name): void + { + $fromColumn = $columnDiff->fromColumn; + $column = $columnDiff->column; + + if (!$fromColumn) { + return; + } + + if (!in_array('length', $columnDiff->changedProperties)) { + return; + } + + $fromLength = $fromColumn->getLength() ?? 255; + $length = $column->getLength() ?? 255; + + if ($fromLength <= $length) { + return; + } + + $column->setLength($fromLength); + + self::unsetChangedColumnProperty($tableDiff, $columnDiff, $name, 'length'); + } + + /** + * @throws DbalException + */ + private function amendColumnDiffTextType(TableDiff $tableDiff, ColumnDiff $columnDiff, string $name): void + { + $fromColumn = $columnDiff->fromColumn; + $column = $columnDiff->column; + + if (!$fromColumn) { + return; + } + + if (!in_array('type', $columnDiff->changedProperties)) { + return; + } + + $fromType = $fromColumn->getType(); + $type = $column->getType(); + + if ( + !$fromType instanceof TextType || + !$type instanceof TextType + ) { + return; + } + + $typePriority = [ + Types::TEXT, + MediumtextType::NAME, + LongtextType::NAME, + ]; + + $fromIndex = array_search($fromType->getName(), $typePriority); + $index = array_search($type->getName(), $typePriority); + + if ($index >= $fromIndex) { + return; + } + + $column->setType(Type::getType($fromType->getName())); + + self::unsetChangedColumnProperty($tableDiff, $columnDiff, $name, 'type'); + } + + private function amendColumnDiffCollation(TableDiff $tableDiff, ColumnDiff $columnDiff, string $name): void + { + $fromColumn = $columnDiff->fromColumn; + $column = $columnDiff->column; + + if (!$fromColumn) { + return; + } + + if (!in_array('collation', $columnDiff->changedProperties)) { + return; + } + + $fromCollation = $fromColumn->getPlatformOption('collation'); + + if (!$fromCollation) { + return; + } + + $column->setPlatformOption('collation', $fromCollation); + + self::unsetChangedColumnProperty($tableDiff, $columnDiff, $name, 'collation'); + } + + private function amendColumnDiffCharset(TableDiff $tableDiff, ColumnDiff $columnDiff, string $name): void + { + $fromColumn = $columnDiff->fromColumn; + $column = $columnDiff->column; + + if (!$fromColumn) { + return; + } + + if (!in_array('charset', $columnDiff->changedProperties)) { + return; + } + + $fromCharset = $fromColumn->getPlatformOption('charset'); + + if (!$fromCharset) { + return; + } + + $column->setPlatformOption('charset', $fromCharset); + + self::unsetChangedColumnProperty($tableDiff, $columnDiff, $name, 'charset'); + } + + private function amendColumnDiffAutoincrement(TableDiff $tableDiff, ColumnDiff $columnDiff, string $name): bool + { + $fromColumn = $columnDiff->fromColumn; + $column = $columnDiff->column; + + if (!$fromColumn) { + return false; + } + + if (!in_array('autoincrement', $columnDiff->changedProperties)) { + return false; + } + + $column + ->setAutoincrement(false) + ->setNotnull(false) + ->setDefault(null); + + self::unsetChangedColumnProperty($tableDiff, $columnDiff, $name, 'autoincrement'); + + return true; + } + + private function amendAddedColumnAutoincrement(Column $column): bool + { + if (!$column->getAutoincrement()) { + return false; + } + + $column + ->setAutoincrement(false) + ->setNotnull(false) + ->setDefault(null); + + return true; + } + + private function moveRemovedAutoincrementColumnToChanged(TableDiff $tableDiff, Column $column, string $name): bool + { + if (!$column->getAutoincrement()) { + return false; + } + + $newColumn = clone $column; + + $newColumn + ->setAutoincrement(false) + ->setNotnull(false) + ->setDefault(null); + + $changedProperties = [ + 'autoincrement', + 'notnull', + 'default', + ]; + + $tableDiff->changedColumns[$name] = new ColumnDiff($name, $newColumn, $changedProperties, $column); + + foreach ($tableDiff->removedIndexes as $indexName => $index) { + if ($index->getColumns() === [$name]) { + unset($tableDiff->removedIndexes[$indexName]); + } + } + + return true; + } + + private static function unsetChangedColumnProperty( + TableDiff $tableDiff, + ColumnDiff $columnDiff, + string $name, + string $property + ): void { + + if (count($columnDiff->changedProperties) === 1) { + unset($tableDiff->changedColumns[$name]); + } + + $columnDiff->changedProperties = array_diff($columnDiff->changedProperties, [$property]); + } +} diff --git a/application/Espo/Core/Utils/Database/Schema/SchemaManager.php b/application/Espo/Core/Utils/Database/Schema/SchemaManager.php index cf7c75fe03..c6a3636359 100644 --- a/application/Espo/Core/Utils/Database/Schema/SchemaManager.php +++ b/application/Espo/Core/Utils/Database/Schema/SchemaManager.php @@ -32,14 +32,15 @@ namespace Espo\Core\Utils\Database\Schema; use Doctrine\DBAL\Connection as DbalConnection; use Doctrine\DBAL\Exception as DbalException; use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Schema\Schema as DbalSchema; -use Doctrine\DBAL\Schema\SchemaDiff as DbalSchemaDiff; +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use Doctrine\DBAL\Schema\Comparator; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\SchemaDiff; use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Types\Type; use Espo\Core\Binding\BindingContainerBuilder; use Espo\Core\InjectableFactory; -use Espo\Core\Utils\Database\DBAL\Schema\Comparator; use Espo\Core\Utils\Database\Helper; use Espo\Core\Utils\Log; use Espo\Core\Utils\Metadata\OrmMetadataData; @@ -51,6 +52,8 @@ use Throwable; */ class SchemaManager { + /** @var AbstractSchemaManager */ + private AbstractSchemaManager $schemaManager; private Comparator $comparator; private Builder $builder; @@ -62,8 +65,13 @@ class SchemaManager private Log $log, private Helper $helper, private MetadataProvider $metadataProvider, + private DiffModifier $diffModifier, private InjectableFactory $injectableFactory ) { + $this->schemaManager = $this->getDbalConnection()->createSchemaManager(); + // Not using a platform specific comparator as it unsets a collation and charset if + // they match a table default. + //$this->comparator = $this->schemaManager->createComparator(); $this->comparator = new Comparator($this->getPlatform()); $this->initFieldTypes(); @@ -81,6 +89,9 @@ class SchemaManager return $this->helper; } + /** + * @throws DbalException + */ private function getPlatform(): AbstractPlatform { return $this->getDbalConnection()->getDatabasePlatform(); @@ -88,7 +99,7 @@ class SchemaManager private function getDbalConnection(): DbalConnection { - return $this->getDatabaseHelper()->getDbalConnection(); + return $this->helper->getDbalConnection(); } /** @@ -113,15 +124,15 @@ class SchemaManager * * @param ?string[] $entityTypeList Specific entity types. * @throws SchemaException + * @throws DbalException */ public function rebuild(?array $entityTypeList = null): bool { - $currentSchema = $this->getCurrentSchema(); - + $fromSchema = $this->introspectSchema(); $schema = $this->builder->build($this->ormMetadataData->getData(), $entityTypeList); try { - $this->processPreRebuildActions($currentSchema, $schema); + $this->processPreRebuildActions($fromSchema, $schema); } catch (Throwable $e) { $this->log->alert('Rebuild database pre-rebuild error: '. $e->getMessage()); @@ -129,14 +140,58 @@ class SchemaManager return false; } - $queries = $this->getDiffSql($currentSchema, $schema); + $diff = $this->comparator->compareSchemas($fromSchema, $schema); + $needReRun = $this->diffModifier->modify($diff); + $sql = $this->composeDiffSql($diff); + $result = $this->runSql($sql); + + if (!$result) { + return false; + } + + if ($needReRun) { + // Needed to handle auto-increment column creation/removal/change. + // As an auto-increment column requires having a unique index, but + // Doctrine DBAL does not handle this. + $intermediateSchema = $this->introspectSchema(); + $schema = $this->builder->build($this->ormMetadataData->getData(), $entityTypeList); + + $diff = $this->comparator->compareSchemas($intermediateSchema, $schema); + + $this->diffModifier->modify($diff, true); + $sql = $this->composeDiffSql($diff); + $result = $this->runSql($sql); + } + + if (!$result) { + return false; + } + + try { + $this->processPostRebuildActions($fromSchema, $schema); + } + catch (Throwable $e) { + $this->log->alert('Rebuild database post-rebuild error: ' . $e->getMessage()); + + return false; + } + + return true; + } + + /** + * @param string[] $queries + * @return bool + */ + private function runSql(array $queries): bool + { $result = true; $connection = $this->getDbalConnection(); foreach ($queries as $sql) { - $this->log->info('SCHEMA, Execute Query: '. $sql); + $this->log->info('Schema, query: '. $sql); try { $connection->executeQuery($sql); @@ -148,51 +203,29 @@ class SchemaManager } } - try { - $this->processPostRebuildActions($currentSchema, $schema); - } - catch (Throwable $e) { - $this->log->alert('Rebuild database post-rebuild error: ' . $e->getMessage()); - - return false; - } - return $result; } /** - * Get current database schema. + * Introspect and return a current database schema. + * + * @throws DbalException */ - private function getCurrentSchema(): DbalSchema + private function introspectSchema(): Schema { - return $this->getDbalConnection() - ->getSchemaManager() - ->createSchema(); + return $this->schemaManager->introspectSchema(); } /** - * Get SQL queries of database schema. - * - * @return string[] Array of SQL queries. + * @return string[] + * @throws DbalException */ - private function toSql(DbalSchemaDiff $schema) + private function composeDiffSql(SchemaDiff $diff): array { - return $schema->toSaveSql($this->getPlatform()); + return $this->getPlatform()->getAlterSchemaSQL($diff); } - /** - * Get SQL queries to get from one to another schema. - * - * @return string[] Array of SQL queries. - */ - private function getDiffSql(DbalSchema $fromSchema, DbalSchema $toSchema) - { - $schemaDiff = $this->comparator->compareSchemas($fromSchema, $toSchema); - - return $this->toSql($schemaDiff); - } - - private function processPreRebuildActions(DbalSchema $actualSchema, DbalSchema $schema): void + private function processPreRebuildActions(Schema $actualSchema, Schema $schema): void { $binding = BindingContainerBuilder::create() ->bindInstance(Helper::class, $this->helper) @@ -205,7 +238,7 @@ class SchemaManager } } - private function processPostRebuildActions(DbalSchema $actualSchema, DbalSchema $schema): void + private function processPostRebuildActions(Schema $actualSchema, Schema $schema): void { $binding = BindingContainerBuilder::create() ->bindInstance(Helper::class, $this->helper) diff --git a/application/Espo/Resources/metadata/app/database.json b/application/Espo/Resources/metadata/app/database.json index 18dbb31965..708871035a 100644 --- a/application/Espo/Resources/metadata/app/database.json +++ b/application/Espo/Resources/metadata/app/database.json @@ -2,7 +2,7 @@ "platforms": { "Mysql": { "detailsProviderClassName": "Espo\\Core\\Utils\\Database\\DetailsProviders\\MysqlDetailsProvider", - "dbalConnectionFactoryClassName": "Espo\\Core\\Utils\\Database\\DBAL\\Factories\\MysqlConnectionFactory", + "dbalConnectionFactoryClassName": "Espo\\Core\\Utils\\Database\\Dbal\\Factories\\MysqlConnectionFactory", "indexHelperClassName": "Espo\\Core\\Utils\\Database\\Orm\\IndexHelpers\\MysqlIndexHelper", "columnPreparatorClassName": "Espo\\Core\\Utils\\Database\\Schema\\ColumnPreparators\\MysqlColumnPreparator", "preRebuildActionClassNameList": [ @@ -10,12 +10,12 @@ ], "postRebuildActionClassNameList": [], "dbalTypeClassNameMap": { - "mediumtext": "Espo\\Core\\Utils\\Database\\DBAL\\Types\\MediumtextType", - "longtext": "Espo\\Core\\Utils\\Database\\DBAL\\Types\\LongtextType" + "mediumtext": "Espo\\Core\\Utils\\Database\\Dbal\\Types\\MediumtextType", + "longtext": "Espo\\Core\\Utils\\Database\\Dbal\\Types\\LongtextType" } }, "Postgresql": { - "dbalConnectionFactoryClassName": "Espo\\Core\\Utils\\Database\\DBAL\\Factories\\PostgresqlConnectionFactory" + "dbalConnectionFactoryClassName": "Espo\\Core\\Utils\\Database\\Dbal\\Factories\\PostgresqlConnectionFactory" } } } diff --git a/composer.json b/composer.json index cd8458eaeb..94e8911771 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "nesbot/carbon": "^2.26", "zbateson/mail-mime-parser": "1.3.*", "phpoffice/phpspreadsheet": "^1.16", - "doctrine/dbal": "^3.3.3", + "doctrine/dbal": "^3.5.3", "league/flysystem-async-aws-s3": "^2.0", "johngrogg/ics-parser": "^3.0", "phpseclib/phpseclib": "^3.0", diff --git a/composer.lock b/composer.lock index be41d2b596..0e98d40340 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ffda69f674ecffbd773206c0e88aeb52", + "content-hash": "be0031b1415143f100d0d315151d8bc5", "packages": [ { "name": "async-aws/core", @@ -374,38 +374,38 @@ }, { "name": "doctrine/dbal", - "version": "3.3.3", + "version": "3.5.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "82331b861727c15b1f457ef05a8729e508e7ead5" + "reference": "88fa7e5189fd5ec6682477044264dc0ed4e3aa1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/82331b861727c15b1f457ef05a8729e508e7ead5", - "reference": "82331b861727c15b1f457ef05a8729e508e7ead5", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/88fa7e5189fd5ec6682477044264dc0ed4e3aa1e", + "reference": "88fa7e5189fd5ec6682477044264dc0ed4e3aa1e", "shasum": "" }, "require": { "composer-runtime-api": "^2", "doctrine/cache": "^1.11|^2.0", - "doctrine/deprecations": "^0.5.3", - "doctrine/event-manager": "^1.0", - "php": "^7.3 || ^8.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", "psr/cache": "^1|^2|^3", "psr/log": "^1|^2|^3" }, "require-dev": { - "doctrine/coding-standard": "9.0.0", - "jetbrains/phpstorm-stubs": "2021.1", - "phpstan/phpstan": "1.4.6", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "9.5.16", - "psalm/plugin-phpunit": "0.16.1", - "squizlabs/php_codesniffer": "3.6.2", - "symfony/cache": "^5.2|^6.0", - "symfony/console": "^2.7|^3.0|^4.0|^5.0|^6.0", - "vimeo/psalm": "4.22.0" + "doctrine/coding-standard": "11.0.0", + "jetbrains/phpstorm-stubs": "2022.3", + "phpstan/phpstan": "1.9.4", + "phpstan/phpstan-strict-rules": "^1.4", + "phpunit/phpunit": "9.5.27", + "psalm/plugin-phpunit": "0.18.4", + "squizlabs/php_codesniffer": "3.7.1", + "symfony/cache": "^5.4|^6.0", + "symfony/console": "^4.4|^5.4|^6.0", + "vimeo/psalm": "4.30.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -465,7 +465,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.3.3" + "source": "https://github.com/doctrine/dbal/tree/3.5.3" }, "funding": [ { @@ -481,7 +481,7 @@ "type": "tidelift" } ], - "time": "2022-03-09T15:39:50+00:00" + "time": "2023-01-12T10:21:44+00:00" }, { "name": "doctrine/deprecations", diff --git a/tests/integration/Core/Tester.php b/tests/integration/Core/Tester.php index 96358e36fb..dfd6acd461 100644 --- a/tests/integration/Core/Tester.php +++ b/tests/integration/Core/Tester.php @@ -139,6 +139,8 @@ class Tester protected function getTestConfigData() { + $this->changeDirToBase(); + if (file_exists($this->configPath)) { $data = include($this->configPath); } @@ -258,12 +260,19 @@ class Tester $this->loadData(); } - public function terminate() + private function changeDirToBase(): void { - $baseDir = str_replace('/' . $this->installPath, '', getcwd()); + $installPath = str_replace('/', DIRECTORY_SEPARATOR, $this->installPath); + + $baseDir = str_replace(DIRECTORY_SEPARATOR . $installPath, '', getcwd()); chdir($baseDir); set_include_path($baseDir); + } + + public function terminate() + { + $this->changeDirToBase(); if ($this->getParam('fullReset')) { $this->saveTestConfigData('lastModifiedTime', null); diff --git a/tests/integration/Espo/Core/Utils/Database/AutoIncrementFieldTest.php b/tests/integration/Espo/Core/Utils/Database/AutoIncrementFieldTest.php index 95908df17d..a29e9eaf7b 100644 --- a/tests/integration/Espo/Core/Utils/Database/AutoIncrementFieldTest.php +++ b/tests/integration/Espo/Core/Utils/Database/AutoIncrementFieldTest.php @@ -43,7 +43,7 @@ class AutoIncrementFieldTest extends Base $this->assertEquals('UNI', $column['COLUMN_KEY']); } - public function testColumnOnExistingTable() + public function testColumnOnExistingTable(): void { $this->updateDefs('Test', 'testAutoIncrement', [ 'type' => 'autoincrement', @@ -59,19 +59,17 @@ class AutoIncrementFieldTest extends Base $this->assertEquals('UNI', $column['COLUMN_KEY']); } - public function testDeleteColumnOnExistingTable() + public function testDeleteColumnOnExistingTable(): void { // 1. Create "testAutoIncrement" field $this->testColumnOnExistingTable(); // 2. Delete "testAutoIncrement" field - $metadata = $this->getContainer()->get('metadata'); - $entityDefs = $metadata->delete('entityDefs', 'Test', [ - 'fields.testAutoIncrement', - ]); - $metadata->save(); + $this->getMetadata()->delete('entityDefs', 'Test', ['fields.testAutoIncrement']); + $this->getMetadata()->save(); - $this->getContainer()->get('dataManager')->rebuild([$entityName]); + // Issue that it requires rebuilding between removing and adding a new indexes. + $this->getDataManager()->rebuild(); $column = $this->getColumnInfo('Test', 'testAutoIncrement'); @@ -83,18 +81,18 @@ class AutoIncrementFieldTest extends Base $this->assertEmpty($column['COLUMN_KEY']); } - public function testDeleteCreateColumnOnExistingTable() + public function testDeleteCreateColumnOnExistingTable(): void { // 1. Create "testAutoIncrement" field $this->testColumnOnExistingTable(); // 2. Delete "testAutoIncrement" field - $metadata = $this->getContainer()->get('metadata'); - $entityDefs = $metadata->delete('entityDefs', 'Test', [ - 'fields.testAutoIncrement', - ]); + $metadata = $this->getMetadata(); + $metadata->delete('entityDefs', 'Test', ['fields.testAutoIncrement']); $metadata->save(); + $this->getDataManager()->rebuild(); + // 3. Create "testAutoIncrement2" field $this->updateDefs('Test', 'testAutoIncrement2', [ 'type' => 'autoincrement', diff --git a/tests/integration/Espo/Core/Utils/Database/Base.php b/tests/integration/Espo/Core/Utils/Database/Base.php index 4dcad16681..cf3fece20c 100644 --- a/tests/integration/Espo/Core/Utils/Database/Base.php +++ b/tests/integration/Espo/Core/Utils/Database/Base.php @@ -91,7 +91,7 @@ abstract class Base extends \tests\integration\Core\BaseTestCase $metadata->set('entityDefs', 'Test', $entityDefs); $metadata->save(); - $this->getContainer()->get('dataManager')->rebuild([$entityName]); + $this->getDataManager()->rebuild([$entityName]); } } diff --git a/tests/integration/Espo/Core/Utils/Database/HelperTest.php b/tests/integration/Espo/Core/Utils/Database/HelperTest.php index e52575c6d1..882387d67e 100644 --- a/tests/integration/Espo/Core/Utils/Database/HelperTest.php +++ b/tests/integration/Espo/Core/Utils/Database/HelperTest.php @@ -29,24 +29,18 @@ namespace tests\integration\Espo\Core\Utils\Database; -use tests\unit\ReflectionHelper; use Espo\Core\Utils\Database\Helper; use Doctrine\DBAL\Connection; -use Espo\Core\Exceptions\Error; use PDO; -use RuntimeException; - class HelperTest extends \tests\integration\Core\BaseTestCase { + /** @var ?Helper */ protected $helper; - protected $reflection; protected function initTest() { - $this->helper = $this->getInjectableFactory()->create(Helper::class); - - $this->reflection = new ReflectionHelper($this->helper); + $this->helper = $this->getInjectableFactory()->create(Helper::class); } private function getDatabaseInfo() @@ -84,41 +78,7 @@ class HelperTest extends \tests\integration\Core\BaseTestCase { $this->initTest(); - $this->assertInstanceOf(PDO::class, $this->helper->getPdoConnection()); - } - - public function testGetMaxIndexLength() - { - $this->initTest(); - - $databaseInfo = $this->getDatabaseInfo(); - if (empty($databaseInfo)) { - return; - } - - $expectedMaxIndexLength = 767; - - switch ($databaseInfo['type']) { - case 'mysql': - if (version_compare($databaseInfo['version'], '5.7.0') >= 0) { - $expectedMaxIndexLength = 3072; - } - break; - - case 'mariadb': - if (version_compare($databaseInfo['version'], '10.2.2') >= 0) { - $expectedMaxIndexLength = 3072; - } - break; - } - - $engine = $this->reflection->invokeMethod('getTableEngine'); - $result = ($engine == 'MyISAM') ? 1000 : $expectedMaxIndexLength; - $this->assertEquals($result, $this->helper->getMaxIndexLength()); - - $engine = $this->reflection->invokeMethod('getTableEngine', ['account']); - $result = ($engine == 'MyISAM') ? 1000 : $expectedMaxIndexLength; - $this->assertEquals($expectedMaxIndexLength, $this->helper->getMaxIndexLength('account')); + $this->assertInstanceOf(PDO::class, $this->helper->getPDO()); } public function testGetDatabaseInfo() @@ -130,8 +90,8 @@ class HelperTest extends \tests\integration\Core\BaseTestCase return; } - $this->assertEquals($databaseInfo['type'], strtolower($this->helper->getDatabaseType())); - $this->assertEquals($databaseInfo['version'], $this->helper->getDatabaseVersion()); + $this->assertEquals($databaseInfo['type'], strtolower($this->helper->getType())); + $this->assertEquals($databaseInfo['version'], $this->helper->getVersion()); } public function testGetDatabaseType() @@ -145,11 +105,11 @@ class HelperTest extends \tests\integration\Core\BaseTestCase switch ($databaseInfo['type']) { case 'mysql': - $this->assertEquals('MySQL', $this->helper->getDatabaseType()); + $this->assertEquals('MySQL', $this->helper->getType()); break; case 'mariadb': - $this->assertEquals('MariaDB', $this->helper->getDatabaseType()); + $this->assertEquals('MariaDB', $this->helper->getType()); break; } } diff --git a/tests/integration/Espo/Core/Utils/Database/TextFieldTest.php b/tests/integration/Espo/Core/Utils/Database/TextFieldTest.php index 56728ea2ee..070807c72b 100644 --- a/tests/integration/Espo/Core/Utils/Database/TextFieldTest.php +++ b/tests/integration/Espo/Core/Utils/Database/TextFieldTest.php @@ -68,6 +68,8 @@ class TextFieldTest extends Base ]); $this->getContainer()->get('metadata')->save(); + $this->getDataManager()->rebuildDatabase(); + $column = $this->getColumnInfo('Test', 'testText'); $this->assertNotEmpty($column); diff --git a/tests/integration/Espo/Core/Utils/Database/VarcharFieldTest.php b/tests/integration/Espo/Core/Utils/Database/VarcharFieldTest.php index eb02fb88a2..213123cd8e 100644 --- a/tests/integration/Espo/Core/Utils/Database/VarcharFieldTest.php +++ b/tests/integration/Espo/Core/Utils/Database/VarcharFieldTest.php @@ -210,4 +210,18 @@ class VarcharFieldTest extends Base $this->assertEquals('test-default', $column['COLUMN_DEFAULT']); } } + + /** + * Make sure columns not removed. + */ + public function testRemoveField(): void + { + $this->getMetadata()->delete('entityDefs', 'Test', ['fields.testVarchar']); + $this->getMetadata()->save(); + $this->getDataManager()->rebuildDatabase(); + + $column = $this->getColumnInfo('Test', 'testVarchar'); + + $this->assertTrue((bool) $column); + } }