This commit is contained in:
Yuri Kuznetsov
2023-02-02 16:11:59 +02:00
parent 99fb897b63
commit a81759b0f1
31 changed files with 610 additions and 1569 deletions
@@ -1,109 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Driver\PDO\MySQL;
use Doctrine\DBAL\Driver\AbstractMySQLDriver;
use Doctrine\DBAL\Driver\PDO\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use PDO;
// Espo: requires for the issue https://github.com/doctrine/dbal/issues/4496
use Espo\Core\Utils\Database\DBAL\Schema\MySQLSchemaManager;
use Doctrine\DBAL\Connection as MySQLDriverConnection;
// End: espo
final class Driver extends AbstractMySQLDriver
{
/**
* {@inheritdoc}
* @return Connection
*/
public function connect(array $params)
{
$driverOptions = $params['driverOptions'] ?? [];
if (!empty($params['persistent'])) {
$driverOptions[PDO::ATTR_PERSISTENT] = true;
}
$pdo = new PDO(
$this->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
}
@@ -1,134 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Factories;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\VersionAwarePlatformDriver as Driver;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Database\DBAL\ConnectionFactory;
use Espo\Core\Utils\Database\DBAL\Driver\PDO\MySQL\Driver as PDOMySQLDriver;
use Espo\ORM\DatabaseParams;
use Espo\ORM\PDO\Options as PdoOptions;
use PDO;
use ReflectionClass;
use RuntimeException;
class MysqlConnectionFactory implements ConnectionFactory
{
/** @var array<string, class-string<Driver>> */
private $driverClassNameMap = [
'mysqli' => 'Doctrine\\DBAL\\Driver\\Mysqli\\Driver', // @todo Revise usage.
'pdo_mysql' => PDOMySQLDriver::class,
];
/** @var array<string, class-string<AbstractPlatform>> */
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;
}
}
@@ -1,55 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Platforms;
use Doctrine\DBAL\{
Types\Types,
Platforms\Keywords\MariaDb102Keywords,
};
class MariaDb1027Platform extends MySQLPlatform
{
public function getJsonTypeDeclarationSQL(array $column): string
{
return 'LONGTEXT';
}
protected function getReservedKeywordsClass(): string
{
return MariaDb102Keywords::class;
}
protected function initializeDoctrineTypeMappings(): void
{
parent::initializeDoctrineTypeMappings();
$this->doctrineTypeMapping['json'] = Types::JSON;
}
}
@@ -1,38 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Platforms;
use Doctrine\DBAL\Platforms\MySQL57Platform as OriginalMySQL57Platform;
use Espo\Core\Utils\Database\DBAL\Traits\Platforms\MySQLPlatform as MySQLPlatformTrait;
class MySQL57Platform extends OriginalMySQL57Platform
{
use MySQLPlatformTrait;
}
@@ -1,38 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Platforms;
use Doctrine\DBAL\Platforms\MySQL80Platform as OriginalMySQL80Platform;
use Espo\Core\Utils\Database\DBAL\Traits\Platforms\MySQLPlatform as MySQLPlatformTrait;
class MySQL80Platform extends OriginalMySQL80Platform
{
use MySQLPlatformTrait;
}
@@ -1,38 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Platforms;
use Doctrine\DBAL\Platforms\MySQLPlatform as OriginalMySQLPlatform;
use Espo\Core\Utils\Database\DBAL\Traits\Platforms\MySQLPlatform as MySQLPlatformTrait;
class MySQLPlatform extends OriginalMySQLPlatform
{
use MySQLPlatformTrait;
}
@@ -1,353 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Schema;
use Espo\Core\Utils\Database\DBAL\Traits\Schema\Comparator as ComparatorTrait;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\Comparator as OriginalComparator;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types;
class Comparator extends OriginalComparator
{
// Espo
use ComparatorTrait;
// Espo: end
public function diffColumn(Column $column1, Column $column2)
{
$properties1 = $column1->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]
);
}
}
}
@@ -1,230 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Schema;
use Doctrine\DBAL\Schema\MySQLSchemaManager as Original;
// Espo: requires for the issue https://github.com/doctrine/dbal/issues/4496
//use Doctrine\DBAL\Platforms\MariaDb1027Platform;
//use Doctrine\DBAL\Platforms\MySQLPlatform;
use Espo\Core\Utils\Database\DBAL\Platforms\MariaDb1027Platform;
use Espo\Core\Utils\Database\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Column;
// Espo: end
use Doctrine\DBAL\Types\Type;
use function array_change_key_case;
use function array_shift;
use function array_values;
use function assert;
use function explode;
use function is_string;
use function preg_match;
use function strpos;
use function strtok;
use function strtolower;
use function strtr;
use const CASE_LOWER;
class MySQLSchemaManager extends Original
{
private const MARIADB_ESCAPE_SEQUENCES = [
'\\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;
}
}
@@ -1,298 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Traits\Platforms;
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Schema\{
TableDiff,
ColumnDiff,
Column,
Identifier,
};
trait MySQLPlatform
{
public function getAlterTableSQL(TableDiff $diff)
{
$columnSql = [];
$queryParts = [];
$newName = $diff->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;
}
}
@@ -1,102 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\DBAL\Traits\Schema;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Types\TextType;
trait Comparator
{
/**
* Fix problem with executing query for custom types.
*
* @param string[] $changedProperties
* @return void
*/
protected function espoFixTypeDiff(array &$changedProperties, Column $column1, Column $column2)
{
$column1DbType = $this->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;
}
}
@@ -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;
@@ -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;
@@ -0,0 +1,85 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\Dbal\Factories;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\PDO\MySQL\Driver as PDOMySQLDriver;
use Doctrine\DBAL\Exception as DBALException;
use Espo\Core\Utils\Database\Dbal\ConnectionFactory;
use Espo\ORM\DatabaseParams;
use Espo\ORM\PDO\Options as PdoOptions;
use PDO;
use RuntimeException;
class MysqlConnectionFactory implements ConnectionFactory
{
public function __construct(
private PDO $pdo
) {}
/**
* @throws DBALException
*/
public function create(DatabaseParams $databaseParams): Connection
{
$driver = new PDOMySQLDriver();
if (!$databaseParams->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);
}
}
@@ -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);
}
}
@@ -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;
@@ -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;
@@ -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;
@@ -417,6 +417,10 @@ class Builder
$result['platformOptions']['collation'] = $column->getCollation();
}
if ($column->getCharset()) {
$result['platformOptions']['charset'] = $column->getCharset();
}
return $result;
}
@@ -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;
}
}
@@ -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
@@ -0,0 +1,311 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Database\Schema;
use Doctrine\DBAL\Exception as DbalException;
use Doctrine\DBAL\Schema\Column as Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\SchemaDiff;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Types\TextType;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Espo\Core\Utils\Database\Dbal\Types\LongtextType;
use Espo\Core\Utils\Database\Dbal\Types\MediumtextType;
class DiffModifier
{
/**
* @throws DbalException
*/
public function modify(SchemaDiff $diff, bool $secondRun = false): bool
{
$reRun = false;
$diff->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]);
}
}
@@ -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<AbstractPlatform> */
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)
@@ -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"
}
}
}
+1 -1
View File
@@ -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",
Generated
+20 -20
View File
@@ -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",
+11 -2
View File
@@ -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);
@@ -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',
@@ -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]);
}
}
@@ -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;
}
}
@@ -68,6 +68,8 @@ class TextFieldTest extends Base
]);
$this->getContainer()->get('metadata')->save();
$this->getDataManager()->rebuildDatabase();
$column = $this->getColumnInfo('Test', 'testText');
$this->assertNotEmpty($column);
@@ -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);
}
}