diff --git a/.drone.yml b/.drone.yml index 031cd1737f53..f3dd2c442fbe 100644 --- a/.drone.yml +++ b/.drone.yml @@ -116,7 +116,7 @@ pipeline: services: mariadb: - image: mariadb:10.3 + image: mariadb:10.2 environment: - MYSQL_USER=owncloud - MYSQL_PASSWORD=owncloud @@ -236,3 +236,7 @@ matrix: - PHP_VERSION: 7.1 DB_TYPE: sqlite TEST_SUITE: coverage + + - PHP_VERSION: 7.1 + DB_TYPE: mariadb + TEST_SUITE: coverage diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index d1737167bd8b..0fa528df1e42 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -33,6 +33,7 @@ use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Cache\QueryCacheProfile; use Doctrine\Common\EventManager; +use Doctrine\DBAL\Driver\ServerInfoAwareConnection; use Doctrine\DBAL\Exception\ConstraintViolationException; use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Schema\Schema; @@ -439,4 +440,27 @@ public function allows4ByteCharacters() { } return false; } + + /** + * Returns the version of the related platform if applicable. + * + * Returns null if either the driver is not capable to create version + * specific platform instances, no explicit server version was specified + * or the underlying driver connection cannot determine the platform + * version without having to query it (performance reasons). + * + * @return null|string + */ + public function getDatabaseVersionString() + { + // Automatic platform version detection. + if ($this->_conn instanceof ServerInfoAwareConnection && + ! $this->_conn->requiresQueryForServerVersion() + ) { + return $this->_conn->getServerVersion(); + } + + // Unable to detect platform version. + return null; + } } diff --git a/lib/private/DB/ConnectionFactory.php b/lib/private/DB/ConnectionFactory.php index ef98ff570169..1469c160a39e 100644 --- a/lib/private/DB/ConnectionFactory.php +++ b/lib/private/DB/ConnectionFactory.php @@ -29,6 +29,7 @@ use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Event\Listeners\OracleSessionInit; use Doctrine\DBAL\Event\Listeners\SQLSessionInit; +use Doctrine\DBAL\Events; use OC\SystemConfig; /** @@ -116,6 +117,10 @@ public function getConnection($type, $additionalConnectionParams) { case 'mysql': $eventManager->addEventSubscriber( new SQLSessionInit("SET SESSION AUTOCOMMIT=1")); + $eventManager->addEventListener( + Events::onSchemaColumnDefinition, + new MySqlSchemaColumnDefinitionListener() + ); break; case 'oci': $eventManager->addEventSubscriber(new OracleSessionInit); diff --git a/lib/private/DB/MySqlSchemaColumnDefinitionListener.php b/lib/private/DB/MySqlSchemaColumnDefinitionListener.php new file mode 100644 index 000000000000..94a7bf0dcaef --- /dev/null +++ b/lib/private/DB/MySqlSchemaColumnDefinitionListener.php @@ -0,0 +1,253 @@ + + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OC\DB; + +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Driver\ServerInfoAwareConnection; +use Doctrine\DBAL\Event\SchemaColumnDefinitionEventArgs; +use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Types\Type; + + +/** + * Class MySqlSchemaColumnDefinitionListener + * + * @package OC\DB + * + * This class contains the smallest portion of native Doctrine code taken from + * Doctrine\DBAL\Platforms\MySqlSchemaManager that allows to bypass the call to + * native _getPortableTableColumnDefinition method + * + * TODO: remove once https://github.com/owncloud/core/issues/28695 is fixed and Doctrine upgraded + */ +class MySqlSchemaColumnDefinitionListener{ + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + protected $_platform; + + public function onSchemaColumnDefinition(SchemaColumnDefinitionEventArgs $eventArgs) { + // We need an instance of platform with ownCloud-specific mappings + // this part can't be moved to constructor - it leads to an infinite recursion + if (is_null($this->_platform)) { + $this->_platform = \OC::$server->getDatabaseConnection()->getDatabasePlatform(); + } + + $version = \OC::$server->getDatabaseConnection()->getDatabaseVersionString(); + $mariadb = false !== stripos($version, 'mariadb'); + if ($mariadb && version_compare($this->getMariaDbMysqlVersionNumber($version), '10.2.7', '>=')) { + $tableColumn = $eventArgs->getTableColumn(); + try { + $column = $this->_getPortableTableColumnDefinition($tableColumn); + $eventArgs->preventDefault(); + $eventArgs->setColumn($column); + } catch (DBALException $e){ + // Pass + } + } + } + + + /** + * Given a table comment this method tries to extract a typehint for Doctrine Type, or returns + * the type given as default. + * + * @param string $comment + * @param string $currentType + * + * @return string + */ + public function extractDoctrineTypeFromComment($comment, $currentType) + { + if (preg_match("(\(DC2Type:([a-zA-Z0-9_]+)\))", $comment, $match)) { + $currentType = $match[1]; + } + + return $currentType; + } + + /** + * @param string $comment + * @param string $type + * + * @return string + */ + public function removeDoctrineTypeFromComment($comment, $type) + { + return str_replace('(DC2Type:'.$type.')', '', $comment); + } + + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $dbType = strtolower($tableColumn['type']); + $dbType = strtok($dbType, '(), '); + if (isset($tableColumn['length'])) { + $length = $tableColumn['length']; + } else { + $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)) { + $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; + } + + $length = ((int) $length == 0) ? null : (int) $length; + + $default = isset($tableColumn['default']) ? $tableColumn['default'] : null; + $columnDefault = $this->getMariaDb1027ColumnDefault($default); + + $options = array( + 'length' => $length, + 'unsigned' => (bool) (strpos($tableColumn['type'], 'unsigned') !== false), + 'fixed' => (bool) $fixed, + // This line was changed to fix breaking change introduced in MariaDB 10.2.6 + 'default' => $columnDefault, + 'notnull' => (bool) ($tableColumn['null'] != 'YES'), + 'scale' => null, + 'precision' => null, + 'autoincrement' => (bool) (strpos($tableColumn['extra'], 'auto_increment') !== false), + 'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== '' + ? $tableColumn['comment'] + : null, + ); + + if ($scale !== null && $precision !== null) { + $options['scale'] = $scale; + $options['precision'] = $precision; + } + + $column = new Column($tableColumn['field'], Type::getType($type), $options); + + if (isset($tableColumn['collation'])) { + $column->setPlatformOption('collation', $tableColumn['collation']); + } + + return $column; + } + + /** + * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers. + * + * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted + * to distinguish them from expressions (see MDEV-10134). + * - Note: Quoted 'NULL' is not enforced by Maria, it is technically possible to have + * null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053) + * + * @link https://mariadb.com/kb/en/library/information-schema-columns-table/ + * @link https://jira.mariadb.org/browse/MDEV-13132 + * + * @param null|string $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7 + * @return string + */ + private function getMariaDb1027ColumnDefault($columnDefault) + { + if ($columnDefault === 'NULL' || $columnDefault === null) { + return null; + } + if ($columnDefault[0] === "'") { + return preg_replace('/^\'(.*)\'$/', '$1', $columnDefault); + } + + return $columnDefault; + } + + /** + * Detect MariaDB server version, including hack for some mariadb distributions + * that starts with the prefix '5.5.5-' + * + * @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial' + * @return string + * @throws DBALException + */ + private function getMariaDbMysqlVersionNumber($versionString) + { + if ( !preg_match('/^(?:5\.5\.5-)?(mariadb-)?(?P\d+)\.(?P\d+)\.(?P\d+)/i', $versionString, $versionParts)) { + throw DBALException::invalidPlatformVersionSpecified( + $versionString, + '^(?:5\.5\.5-)?(mariadb-)?..' + ); + } + return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch']; + } + +} diff --git a/tests/drone/test-phpunit.sh b/tests/drone/test-phpunit.sh index 477b85f7982d..ea550956de7a 100755 --- a/tests/drone/test-phpunit.sh +++ b/tests/drone/test-phpunit.sh @@ -44,8 +44,8 @@ fi ./occ app:enable federation ./occ app:enable federatedfilesharing -if [[ "${DB_TYPE}" == "none" ]]; then - GROUP="--exclude-group DB" +if [[ "${DB_TYPE}" == "none" || "${DB_TYPE}" == "sqlite" ]]; then + GROUP="" else GROUP="--group DB" fi