diff --git a/.travis.yml b/.travis.yml index 4facbe8df8a..ca5892be3d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_install: - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available" before_script: - - if [[ "$DB" == "mysql" || "$DB" == "mysqli" || "$DB" == "mariadb" ]]; then mysql < tests/travis/create-mysql-schema.sql; fi; + - if [[ "$DB" == "mysql" || "$DB" == "mysqli" || "$DB" == *"mariadb"* ]]; then mysql < tests/travis/create-mysql-schema.sql; fi; install: - travis_retry composer -n install @@ -103,6 +103,38 @@ jobs: addons: mariadb: 10.1 + - stage: Test + php: 7.1 + env: DB=mariadb MARIADB_VERSION=10.2 + addons: + mariadb: 10.2 + - stage: Test + php: 7.2 + env: DB=mariadb MARIADB_VERSION=10.2 + addons: + mariadb: 10.2 + - stage: Test + php: nightly + env: DB=mariadb MARIADB_VERSION=10.2 + addons: + mariadb: 10.2 + + - stage: Test + php: 7.1 + env: DB=mariadb.mysqli MARIADB_VERSION=10.2 + addons: + mariadb: 10.2 + - stage: Test + php: 7.2 + env: DB=mariadb.mysqli MARIADB_VERSION=10.2 + addons: + mariadb: 10.2 + - stage: Test + php: nightly + env: DB=mariadb.mysqli MARIADB_VERSION=10.2 + addons: + mariadb: 10.2 + - stage: Test php: 7.1 env: DB=pgsql POSTGRESQL_VERSION=9.2 diff --git a/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php b/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php index a06d2c0ab67..308c857907a 100644 --- a/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php +++ b/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php @@ -22,6 +22,7 @@ use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Platforms\MariaDb1027Platform; use Doctrine\DBAL\Platforms\MySQL57Platform; use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Schema\MySqlSchemaManager; @@ -123,35 +124,74 @@ public function convertException($message, DriverException $exception) /** * {@inheritdoc} + * + * @throws DBALException */ public function createDatabasePlatformForVersion($version) { - if ( ! preg_match('/^(?P\d+)(?:\.(?P\d+)(?:\.(?P\d+))?)?/', $version, $versionParts)) { - throw DBALException::invalidPlatformVersionSpecified( - $version, - '..' - ); + $mariadb = false !== stripos($version, 'mariadb'); + if ($mariadb && version_compare($this->getMariaDbMysqlVersionNumber($version), '10.2.7', '>=')) { + return new MariaDb1027Platform(); } - if (false !== stripos($version, 'mariadb')) { - return $this->getDatabasePlatform(); + if ( ! $mariadb && version_compare($this->getOracleMysqlVersionNumber($version), '5.7.9', '>=')) { + return new MySQL57Platform(); } + return $this->getDatabasePlatform(); + } + + /** + * Get a normalized 'version number' from the server string + * returned by Oracle MySQL servers. + * + * @param string $versionString Version string returned by the driver, i.e. '5.7.10' + * @throws DBALException + */ + private function getOracleMysqlVersionNumber(string $versionString) : string + { + if ( ! preg_match( + '/^(?P\d+)(?:\.(?P\d+)(?:\.(?P\d+))?)?/', + $versionString, + $versionParts + )) { + throw DBALException::invalidPlatformVersionSpecified( + $versionString, + '..' + ); + } $majorVersion = $versionParts['major']; - $minorVersion = isset($versionParts['minor']) ? $versionParts['minor'] : 0; - $patchVersion = isset($versionParts['patch']) ? $versionParts['patch'] : null; + $minorVersion = $versionParts['minor'] ?? 0; + $patchVersion = $versionParts['patch'] ?? null; if ('5' === $majorVersion && '7' === $minorVersion && null === $patchVersion) { $patchVersion = '9'; } - $version = $majorVersion . '.' . $minorVersion . '.' . $patchVersion; + return $majorVersion . '.' . $minorVersion . '.' . $patchVersion; + } - if (version_compare($version, '5.7.9', '>=')) { - return new MySQL57Platform(); + /** + * 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' + * @throws DBALException + */ + private function getMariaDbMysqlVersionNumber(string $versionString) : string + { + 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 $this->getDatabasePlatform(); + return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch']; } /** @@ -170,6 +210,7 @@ public function getDatabase(\Doctrine\DBAL\Connection $conn) /** * {@inheritdoc} + * @return MySqlPlatform */ public function getDatabasePlatform() { @@ -178,6 +219,7 @@ public function getDatabasePlatform() /** * {@inheritdoc} + * @return MySqlSchemaManager */ public function getSchemaManager(\Doctrine\DBAL\Connection $conn) { diff --git a/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php b/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php index 9d30b968620..79955a0c046 100644 --- a/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php +++ b/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php @@ -94,9 +94,18 @@ public function getWrappedResourceHandle() /** * {@inheritdoc} + * + * The server version detection includes a special case for MariaDB + * to support '5.5.5-' prefixed versions introduced in Maria 10+ + * @link https://jira.mariadb.org/browse/MDEV-4088 */ public function getServerVersion() { + $serverInfos = $this->_conn->get_server_info(); + if (false !== stripos($serverInfos, 'mariadb')) { + return $serverInfos; + } + $majorVersion = floor($this->_conn->server_version / 10000); $minorVersion = floor(($this->_conn->server_version - $majorVersion * 10000) / 100); $patchVersion = floor($this->_conn->server_version - $majorVersion * 10000 - $minorVersion * 100); diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php index 1822a56335a..443472807d0 100644 --- a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -2287,19 +2287,19 @@ public function getDefaultValueDeclarationSQL($field) $type = $field['type']; if ($type instanceof Types\PhpIntegerMappingType) { - return " DEFAULT " . $default; + return ' DEFAULT ' . $default; } if ($type instanceof Types\PhpDateTimeMappingType && $default === $this->getCurrentTimestampSQL()) { - return " DEFAULT " . $this->getCurrentTimestampSQL(); + return ' DEFAULT ' . $this->getCurrentTimestampSQL(); } if ($type instanceof Types\TimeType && $default === $this->getCurrentTimeSQL()) { - return " DEFAULT " . $this->getCurrentTimeSQL(); + return ' DEFAULT ' . $this->getCurrentTimeSQL(); } if ($type instanceof Types\DateType && $default === $this->getCurrentDateSQL()) { - return " DEFAULT " . $this->getCurrentDateSQL(); + return ' DEFAULT ' . $this->getCurrentDateSQL(); } if ($type instanceof Types\BooleanType) { diff --git a/lib/Doctrine/DBAL/Platforms/Keywords/MariaDb102Keywords.php b/lib/Doctrine/DBAL/Platforms/Keywords/MariaDb102Keywords.php new file mode 100644 index 00000000000..7563f3193f6 --- /dev/null +++ b/lib/Doctrine/DBAL/Platforms/Keywords/MariaDb102Keywords.php @@ -0,0 +1,285 @@ +. + */ + +namespace Doctrine\DBAL\Platforms\Keywords; + +/** + * MariaDb reserved keywords list. + * @link https://mariadb.com/kb/en/the-mariadb-library/reserved-words/ + */ +final class MariaDb102Keywords extends MySQLKeywords +{ + /** + * {@inheritdoc} + */ + public function getName() : string + { + return 'MariaDb102'; + } + + /** + * {@inheritdoc} + */ + protected function getKeywords() : array + { + return [ + 'ACCESSIBLE', + 'ADD', + 'ALL', + 'ALTER', + 'ANALYZE', + 'AND', + 'AS', + 'ASC', + 'ASENSITIVE', + 'BEFORE', + 'BETWEEN', + 'BIGINT', + 'BINARY', + 'BLOB', + 'BOTH', + 'BY', + 'CALL', + 'CASCADE', + 'CASE', + 'CHANGE', + 'CHAR', + 'CHARACTER', + 'CHECK', + 'COLLATE', + 'COLUMN', + 'CONDITION', + 'CONSTRAINT', + 'CONTINUE', + 'CONVERT', + 'CREATE', + 'CROSS', + 'CURRENT_DATE', + 'CURRENT_TIME', + 'CURRENT_TIMESTAMP', + 'CURRENT_USER', + 'CURSOR', + 'DATABASE', + 'DATABASES', + 'DAY_HOUR', + 'DAY_MICROSECOND', + 'DAY_MINUTE', + 'DAY_SECOND', + 'DEC', + 'DECIMAL', + 'DECLARE', + 'DEFAULT', + 'DELAYED', + 'DELETE', + 'DESC', + 'DESCRIBE', + 'DETERMINISTIC', + 'DISTINCT', + 'DISTINCTROW', + 'DIV', + 'DOUBLE', + 'DROP', + 'DUAL', + 'EACH', + 'ELSE', + 'ELSEIF', + 'ENCLOSED', + 'ESCAPED', + 'EXISTS', + 'EXIT', + 'EXPLAIN', + 'FALSE', + 'FETCH', + 'FLOAT', + 'FLOAT4', + 'FLOAT8', + 'FOR', + 'FORCE', + 'FOREIGN', + 'FROM', + 'FULLTEXT', + 'GENERATED', + 'GET', + 'GENERAL', + 'GRANT', + 'GROUP', + 'HAVING', + 'HIGH_PRIORITY', + 'HOUR_MICROSECOND', + 'HOUR_MINUTE', + 'HOUR_SECOND', + 'IF', + 'IGNORE', + 'IGNORE_SERVER_IDS', + 'IN', + 'INDEX', + 'INFILE', + 'INNER', + 'INOUT', + 'INSENSITIVE', + 'INSERT', + 'INT', + 'INT1', + 'INT2', + 'INT3', + 'INT4', + 'INT8', + 'INTEGER', + 'INTERVAL', + 'INTO', + 'IO_AFTER_GTIDS', + 'IO_BEFORE_GTIDS', + 'IS', + 'ITERATE', + 'JOIN', + 'KEY', + 'KEYS', + 'KILL', + 'LEADING', + 'LEAVE', + 'LEFT', + 'LIKE', + 'LIMIT', + 'LINEAR', + 'LINES', + 'LOAD', + 'LOCALTIME', + 'LOCALTIMESTAMP', + 'LOCK', + 'LONG', + 'LONGBLOB', + 'LONGTEXT', + 'LOOP', + 'LOW_PRIORITY', + 'MASTER_BIND', + 'MASTER_HEARTBEAT_PERIOD', + 'MASTER_SSL_VERIFY_SERVER_CERT', + 'MATCH', + 'MAXVALUE', + 'MEDIUMBLOB', + 'MEDIUMINT', + 'MEDIUMTEXT', + 'MIDDLEINT', + 'MINUTE_MICROSECOND', + 'MINUTE_SECOND', + 'MOD', + 'MODIFIES', + 'NATURAL', + 'NO_WRITE_TO_BINLOG', + 'NOT', + 'NULL', + 'NUMERIC', + 'ON', + 'OPTIMIZE', + 'OPTIMIZER_COSTS', + 'OPTION', + 'OPTIONALLY', + 'OR', + 'ORDER', + 'OUT', + 'OUTER', + 'OUTFILE', + 'PARTITION', + 'PRECISION', + 'PRIMARY', + 'PROCEDURE', + 'PURGE', + 'RANGE', + 'READ', + 'READ_WRITE', + 'READS', + 'REAL', + 'RECURSIVE', + 'REFERENCES', + 'REGEXP', + 'RELEASE', + 'RENAME', + 'REPEAT', + 'REPLACE', + 'REQUIRE', + 'RESIGNAL', + 'RESTRICT', + 'RETURN', + 'REVOKE', + 'RIGHT', + 'RLIKE', + 'ROWS', + 'SCHEMA', + 'SCHEMAS', + 'SECOND_MICROSECOND', + 'SELECT', + 'SENSITIVE', + 'SEPARATOR', + 'SET', + 'SHOW', + 'SIGNAL', + 'SLOW', + 'SMALLINT', + 'SPATIAL', + 'SPECIFIC', + 'SQL', + 'SQL_BIG_RESULT', + 'SQL_CALC_FOUND_ROWS', + 'SQL_SMALL_RESULT', + 'SQLEXCEPTION', + 'SQLSTATE', + 'SQLWARNING', + 'SSL', + 'STARTING', + 'STORED', + 'STRAIGHT_JOIN', + 'TABLE', + 'TERMINATED', + 'THEN', + 'TINYBLOB', + 'TINYINT', + 'TINYTEXT', + 'TO', + 'TRAILING', + 'TRIGGER', + 'TRUE', + 'UNDO', + 'UNION', + 'UNIQUE', + 'UNLOCK', + 'UNSIGNED', + 'UPDATE', + 'USAGE', + 'USE', + 'USING', + 'UTC_DATE', + 'UTC_TIME', + 'UTC_TIMESTAMP', + 'VALUES', + 'VARBINARY', + 'VARCHAR', + 'VARCHARACTER', + 'VARYING', + 'VIRTUAL', + 'WHEN', + 'WHERE', + 'WHILE', + 'WITH', + 'WRITE', + 'XOR', + 'YEAR_MONTH', + 'ZEROFILL', + ]; + } +} diff --git a/lib/Doctrine/DBAL/Platforms/MariaDb1027Platform.php b/lib/Doctrine/DBAL/Platforms/MariaDb1027Platform.php new file mode 100644 index 00000000000..1955fb05912 --- /dev/null +++ b/lib/Doctrine/DBAL/Platforms/MariaDb1027Platform.php @@ -0,0 +1,69 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\Types\Type; + +/** + * Provides the behavior, features and SQL dialect of the MariaDB 10.2 (10.2.7 GA) database platform. + * + * Note: Should not be used with versions prior ro 10.2.7. + * + * @author Vanvelthem Sébastien + * @link www.doctrine-project.org + */ +final class MariaDb1027Platform extends MySqlPlatform +{ + /** + * {@inheritdoc} + */ + public function hasNativeJsonType() : bool + { + return false; + } + + /** + * {@inheritdoc} + * + * @link https://mariadb.com/kb/en/library/json-data-type/ + */ + public function getJsonTypeDeclarationSQL(array $field) : string + { + return 'LONGTEXT'; + } + + /** + * {@inheritdoc} + */ + protected function getReservedKeywordsClass() : string + { + return Keywords\MariaDb102Keywords::class; + } + + /** + * {@inheritdoc} + */ + protected function initializeDoctrineTypeMappings() : void + { + parent::initializeDoctrineTypeMappings(); + + $this->doctrineTypeMapping['json'] = Type::JSON; + } +} diff --git a/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php b/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php index 9de4b8e330f..00e2ba9a4b3 100644 --- a/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php +++ b/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php @@ -19,6 +19,7 @@ namespace Doctrine\DBAL\Schema; +use Doctrine\DBAL\Platforms\MariaDb1027Platform; use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Types\Type; @@ -63,11 +64,11 @@ protected function _getPortableUserDefinition($user) /** * {@inheritdoc} */ - protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) { foreach ($tableIndexes as $k => $v) { $v = array_change_key_case($v, CASE_LOWER); - if ($v['key_name'] == 'PRIMARY') { + if ($v['key_name'] === 'PRIMARY') { $v['primary'] = true; } else { $v['primary'] = false; @@ -120,14 +121,14 @@ protected function _getPortableTableColumnDefinition($tableColumn) $tableColumn['name'] = ''; } - $scale = null; + $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); + $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); } @@ -143,8 +144,8 @@ protected function _getPortableTableColumnDefinition($tableColumn) case 'decimal': if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['type'], $match)) { $precision = $match[1]; - $scale = $match[2]; - $length = null; + $scale = $match[2]; + $length = null; } break; case 'tinytext': @@ -176,25 +177,29 @@ protected function _getPortableTableColumnDefinition($tableColumn) break; } - $length = ((int) $length == 0) ? null : (int) $length; + if ($this->_platform instanceof MariaDb1027Platform) { + $columnDefault = $this->getMariaDb1027ColumnDefault($this->_platform, $tableColumn['default']); + } else { + $columnDefault = $tableColumn['default']; + } $options = [ - 'length' => $length, - 'unsigned' => (bool) (strpos($tableColumn['type'], 'unsigned') !== false), + 'length' => $length !== null ? (int) $length : null, + 'unsigned' => strpos($tableColumn['type'], 'unsigned') !== false, 'fixed' => (bool) $fixed, - 'default' => isset($tableColumn['default']) ? $tableColumn['default'] : null, - 'notnull' => (bool) ($tableColumn['null'] != 'YES'), + 'default' => $columnDefault, + 'notnull' => $tableColumn['null'] !== 'YES', 'scale' => null, 'precision' => null, - 'autoincrement' => (bool) (strpos($tableColumn['extra'], 'auto_increment') !== false), + 'autoincrement' => 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; + $options['scale'] = (int) $scale; + $options['precision'] = (int) $precision; } $column = new Column($tableColumn['field'], Type::getType($type), $options); @@ -206,6 +211,45 @@ protected function _getPortableTableColumnDefinition($tableColumn) 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). + * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema + * as current_timestamp(), currdate(), currtime() + * - 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) + * - \' is always stored as '' in information_schema (normalized) + * + * @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 + */ + private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault) : ?string + { + if ($columnDefault === 'NULL' || $columnDefault === null) { + return null; + } + if ($columnDefault[0] === "'") { + return stripslashes( + str_replace("''", "'", + preg_replace('/^\'(.*)\'$/', '$1', $columnDefault) + ) + ); + } + switch ($columnDefault) { + case 'current_timestamp()': + return $platform->getCurrentTimestampSQL(); + case 'curdate()': + return $platform->getCurrentDateSQL(); + case 'curtime()': + return $platform->getCurrentTimeSQL(); + } + return $columnDefault; + } + /** * {@inheritdoc} */ @@ -214,11 +258,11 @@ protected function _getPortableTableForeignKeysList($tableForeignKeys) $list = []; foreach ($tableForeignKeys as $value) { $value = array_change_key_case($value, CASE_LOWER); - if (!isset($list[$value['constraint_name']])) { - if (!isset($value['delete_rule']) || $value['delete_rule'] == "RESTRICT") { + if ( ! isset($list[$value['constraint_name']])) { + if ( ! isset($value['delete_rule']) || $value['delete_rule'] === "RESTRICT") { $value['delete_rule'] = null; } - if (!isset($value['update_rule']) || $value['update_rule'] == "RESTRICT") { + if ( ! isset($value['update_rule']) || $value['update_rule'] === "RESTRICT") { $value['update_rule'] = null; } @@ -231,15 +275,17 @@ protected function _getPortableTableForeignKeysList($tableForeignKeys) 'onUpdate' => $value['update_rule'], ]; } - $list[$value['constraint_name']]['local'][] = $value['column_name']; + $list[$value['constraint_name']]['local'][] = $value['column_name']; $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name']; } $result = []; foreach ($list as $constraint) { $result[] = new ForeignKeyConstraint( - array_values($constraint['local']), $constraint['foreignTable'], - array_values($constraint['foreign']), $constraint['name'], + array_values($constraint['local']), + $constraint['foreignTable'], + array_values($constraint['foreign']), + $constraint['name'], [ 'onDelete' => $constraint['onDelete'], 'onUpdate' => $constraint['onUpdate'], diff --git a/tests/Doctrine/Tests/DBAL/Driver/AbstractDriverTest.php b/tests/Doctrine/Tests/DBAL/Driver/AbstractDriverTest.php index ed4dd21d0fa..dd448d65730 100644 --- a/tests/Doctrine/Tests/DBAL/Driver/AbstractDriverTest.php +++ b/tests/Doctrine/Tests/DBAL/Driver/AbstractDriverTest.php @@ -7,7 +7,6 @@ use Doctrine\DBAL\Driver\ExceptionConverterDriver; use Doctrine\DBAL\VersionAwarePlatformDriver; use Doctrine\Tests\DbalTestCase; -use Throwable; abstract class AbstractDriverTest extends DbalTestCase { @@ -111,19 +110,29 @@ public function testCreatesDatabasePlatformForVersion() $data = $this->getDatabasePlatformsForVersions(); - if (empty($data)) { - $this->fail( + self::assertNotEmpty( + $data, + sprintf( + 'No test data found for test %s. You have to return test data from %s.', + get_class($this) . '::' . __FUNCTION__, + get_class($this) . '::getDatabasePlatformsForVersions' + ) + ); + + foreach ($data as $item) { + $generatedVersion = get_class($this->driver->createDatabasePlatformForVersion($item[0])); + + self::assertSame( + $item[1], + $generatedVersion, sprintf( - 'No test data found for test %s. You have to return test data from %s.', - get_class($this) . '::' . __FUNCTION__, - get_class($this) . '::getDatabasePlatformsForVersions' + 'Expected platform for version "%s" should be "%s", "%s" given', + $item[0], + $item[1], + $generatedVersion ) ); } - - foreach ($data as $item) { - self::assertSame($item[1], get_class($this->driver->createDatabasePlatformForVersion($item[0]))); - } } /** diff --git a/tests/Doctrine/Tests/DBAL/Driver/AbstractMySQLDriverTest.php b/tests/Doctrine/Tests/DBAL/Driver/AbstractMySQLDriverTest.php index 87676a1920d..11fd12a52fb 100644 --- a/tests/Doctrine/Tests/DBAL/Driver/AbstractMySQLDriverTest.php +++ b/tests/Doctrine/Tests/DBAL/Driver/AbstractMySQLDriverTest.php @@ -3,6 +3,8 @@ namespace Doctrine\Tests\DBAL\Driver; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\MariaDb1027Platform; +use Doctrine\DBAL\Platforms\MySQL57Platform; use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Schema\MySqlSchemaManager; @@ -52,20 +54,24 @@ protected function createSchemaManager(Connection $connection) return new MySqlSchemaManager($connection); } - protected function getDatabasePlatformsForVersions() + protected function getDatabasePlatformsForVersions() : array { - return array( - array('5.6.9', 'Doctrine\DBAL\Platforms\MySqlPlatform'), - array('5.7', 'Doctrine\DBAL\Platforms\MySQL57Platform'), - array('5.7.0', 'Doctrine\DBAL\Platforms\MySqlPlatform'), - array('5.7.8', 'Doctrine\DBAL\Platforms\MySqlPlatform'), - array('5.7.9', 'Doctrine\DBAL\Platforms\MySQL57Platform'), - array('5.7.10', 'Doctrine\DBAL\Platforms\MySQL57Platform'), - array('6', 'Doctrine\DBAL\Platforms\MySQL57Platform'), - array('10.0.15-MariaDB-1~wheezy', 'Doctrine\DBAL\Platforms\MySqlPlatform'), - array('10.1.2a-MariaDB-a1~lenny-log', 'Doctrine\DBAL\Platforms\MySqlPlatform'), - array('5.5.40-MariaDB-1~wheezy', 'Doctrine\DBAL\Platforms\MySqlPlatform'), - ); + return [ + ['5.6.9', MySqlPlatform::class], + ['5.7', MySQL57Platform::class], + ['5.7.0', MySqlPlatform::class], + ['5.7.8', MySqlPlatform::class], + ['5.7.9', MySQL57Platform::class], + ['5.7.10', MySQL57Platform::class], + ['6', MySQL57Platform::class], + ['10.0.15-MariaDB-1~wheezy', MySqlPlatform::class], + ['5.5.5-10.1.25-MariaDB', MySqlPlatform::class], + ['10.1.2a-MariaDB-a1~lenny-log', MySqlPlatform::class], + ['5.5.40-MariaDB-1~wheezy', MySqlPlatform::class], + ['5.5.5-MariaDB-10.2.8+maria~xenial-log', MariaDb1027Platform::class], + ['10.2.8-MariaDB-10.2.8+maria~xenial-log', MariaDb1027Platform::class], + ['10.2.8-MariaDB-1~lenny-log', MariaDb1027Platform::class] + ]; } protected function getExceptionConversionData() diff --git a/tests/Doctrine/Tests/DBAL/Driver/DrizzlePDOMySql/DriverTest.php b/tests/Doctrine/Tests/DBAL/Driver/DrizzlePDOMySql/DriverTest.php index 49261714e34..004043c2051 100644 --- a/tests/Doctrine/Tests/DBAL/Driver/DrizzlePDOMySql/DriverTest.php +++ b/tests/Doctrine/Tests/DBAL/Driver/DrizzlePDOMySql/DriverTest.php @@ -35,12 +35,12 @@ protected function createSchemaManager(Connection $connection) return new DrizzleSchemaManager($connection); } - protected function getDatabasePlatformsForVersions() + protected function getDatabasePlatformsForVersions() : array { - return array( - array('foo', 'Doctrine\DBAL\Platforms\DrizzlePlatform'), - array('bar', 'Doctrine\DBAL\Platforms\DrizzlePlatform'), - array('baz', 'Doctrine\DBAL\Platforms\DrizzlePlatform'), - ); + return [ + ['foo', 'Doctrine\DBAL\Platforms\DrizzlePlatform'], + ['bar', 'Doctrine\DBAL\Platforms\DrizzlePlatform'], + ['baz', 'Doctrine\DBAL\Platforms\DrizzlePlatform'], + ]; } } diff --git a/tests/Doctrine/Tests/DBAL/Functional/Schema/MySqlSchemaManagerTest.php b/tests/Doctrine/Tests/DBAL/Functional/Schema/MySqlSchemaManagerTest.php index d6070cc4c86..4af8b138b4c 100644 --- a/tests/Doctrine/Tests/DBAL/Functional/Schema/MySqlSchemaManagerTest.php +++ b/tests/Doctrine/Tests/DBAL/Functional/Schema/MySqlSchemaManagerTest.php @@ -2,11 +2,13 @@ namespace Doctrine\Tests\DBAL\Functional\Schema; +use Doctrine\DBAL\Platforms\MariaDb1027Platform; use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Schema\Comparator; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Type; +use Doctrine\Tests\Types\MySqlPointType; class MySqlSchemaManagerTest extends SchemaManagerFunctionalTestCase { @@ -14,8 +16,8 @@ protected function setUp() { parent::setUp(); - if (!Type::hasType('point')) { - Type::addType('point', 'Doctrine\Tests\Types\MySqlPointType'); + if ( ! Type::hasType('point')) { + Type::addType('point', MySqlPointType::class); } } @@ -155,11 +157,17 @@ public function testDropPrimaryKeyWithAutoincrementColumn() */ public function testDoesNotPropagateDefaultValuesForUnsupportedColumnTypes() { + if ($this->_sm->getDatabasePlatform() instanceof MariaDb1027Platform) { + $this->markTestSkipped( + 'MariaDb102Platform supports default values for BLOB and TEXT columns and will propagate values' + ); + } + $table = new Table("text_blob_default_value"); - $table->addColumn('def_text', 'text', array('default' => 'def')); - $table->addColumn('def_text_null', 'text', array('notnull' => false, 'default' => 'def')); - $table->addColumn('def_blob', 'blob', array('default' => 'def')); - $table->addColumn('def_blob_null', 'blob', array('notnull' => false, 'default' => 'def')); + $table->addColumn('def_text', 'text', ['default' => 'def']); + $table->addColumn('def_text_null', 'text', ['notnull' => false, 'default' => 'def']); + $table->addColumn('def_blob', 'blob', ['default' => 'def']); + $table->addColumn('def_blob_null', 'blob', ['notnull' => false, 'default' => 'def']); $this->_sm->dropAndCreateTable($table); @@ -325,4 +333,148 @@ public function testListFloatTypeColumns() self::assertFalse($columns['col']->getUnsigned()); self::assertTrue($columns['col_unsigned']->getUnsigned()); } + + public function testJsonColumnType() : void + { + $table = new Table('test_mysql_json'); + $table->addColumn('col_json', 'json'); + $this->_sm->dropAndCreateTable($table); + + $columns = $this->_sm->listTableColumns('test_mysql_json'); + + self::assertSame(TYPE::JSON, $columns['col_json']->getType()->getName()); + } + + public function testColumnDefaultCurrentTimestamp() : void + { + $platform = $this->_sm->getDatabasePlatform(); + + $table = new Table("test_column_defaults_current_timestamp"); + + $currentTimeStampSql = $platform->getCurrentTimestampSQL(); + + $table->addColumn('col_datetime', 'datetime', ['notnull' => true, 'default' => $currentTimeStampSql]); + $table->addColumn('col_datetime_nullable', 'datetime', ['default' => $currentTimeStampSql]); + + $this->_sm->dropAndCreateTable($table); + + $onlineTable = $this->_sm->listTableDetails("test_column_defaults_current_timestamp"); + self::assertSame($currentTimeStampSql, $onlineTable->getColumn('col_datetime')->getDefault()); + self::assertSame($currentTimeStampSql, $onlineTable->getColumn('col_datetime_nullable')->getDefault()); + + $comparator = new Comparator(); + + $diff = $comparator->diffTable($table, $onlineTable); + self::assertFalse($diff, "Tables should be identical with column defaults."); + } + + public function testColumnDefaultsAreValid() + { + $table = new Table("test_column_defaults_are_valid"); + + $currentTimeStampSql = $this->_sm->getDatabasePlatform()->getCurrentTimestampSQL(); + $table->addColumn('col_datetime', 'datetime', ['default' => $currentTimeStampSql]); + $table->addColumn('col_datetime_null', 'datetime', ['notnull' => false, 'default' => null]); + $table->addColumn('col_int', 'integer', ['default' => 1]); + $table->addColumn('col_neg_int', 'integer', ['default' => -1]); + $table->addColumn('col_string', 'string', ['default' => 'A']); + $table->addColumn('col_decimal', 'decimal', ['scale' => 3, 'precision' => 6, 'default' => -2.3]); + $table->addColumn('col_date', 'date', ['default' => '2012-12-12']); + + $this->_sm->dropAndCreateTable($table); + + $this->_conn->executeUpdate( + "INSERT INTO test_column_defaults_are_valid () VALUES()" + ); + + $row = $this->_conn->fetchAssoc( + 'SELECT *, DATEDIFF(CURRENT_TIMESTAMP(), col_datetime) as diff_seconds FROM test_column_defaults_are_valid' + ); + + self::assertInstanceOf(\DateTime::class, \DateTime::createFromFormat('Y-m-d H:i:s', $row['col_datetime'])); + self::assertNull($row['col_datetime_null']); + self::assertSame('2012-12-12', $row['col_date']); + self::assertSame('A', $row['col_string']); + self::assertEquals(1, $row['col_int']); + self::assertEquals(-1, $row['col_neg_int']); + self::assertEquals('-2.300', $row['col_decimal']); + self::assertLessThan(5, $row['diff_seconds']); + } + + /** + * MariaDB 10.2+ does support CURRENT_TIME and CURRENT_DATE as + * column default values for time and date columns. + * (Not supported on Mysql as of 5.7.19) + * + * Note that MariaDB 10.2+, when storing default in information_schema, + * silently change CURRENT_TIMESTAMP as 'current_timestamp()', + * CURRENT_TIME as 'currtime()' and CURRENT_DATE as 'currdate()'. + * This test also ensure proper aliasing to not trigger a table diff. + */ + public function testColumnDefaultValuesCurrentTimeAndDate() : void + { + if ( ! $this->_sm->getDatabasePlatform() instanceof MariaDb1027Platform) { + $this->markTestSkipped('Only relevant for MariaDb102Platform.'); + } + + $platform = $this->_sm->getDatabasePlatform(); + + $table = new Table("test_column_defaults_current_time_and_date"); + + $currentTimestampSql = $platform->getCurrentTimestampSQL(); + $currentTimeSql = $platform->getCurrentTimeSQL(); + $currentDateSql = $platform->getCurrentDateSQL(); + + $table->addColumn('col_datetime', 'datetime', ['default' => $currentTimestampSql]); + $table->addColumn('col_date', 'date', ['default' => $currentDateSql]); + $table->addColumn('col_time', 'time', ['default' => $currentTimeSql]); + + $this->_sm->dropAndCreateTable($table); + + $onlineTable = $this->_sm->listTableDetails("test_column_defaults_current_time_and_date"); + + self::assertSame($currentTimestampSql, $onlineTable->getColumn('col_datetime')->getDefault()); + self::assertSame($currentDateSql, $onlineTable->getColumn('col_date')->getDefault()); + self::assertSame($currentTimeSql, $onlineTable->getColumn('col_time')->getDefault()); + + $comparator = new Comparator(); + + $diff = $comparator->diffTable($table, $onlineTable); + self::assertFalse($diff, "Tables should be identical with column defauts time and date."); + } + + /** + * Ensure default values (un-)escaping is properly done by mysql platforms. + * The test is voluntarily relying on schema introspection due to current + * doctrine limitations. Once #2850 is landed, this test can be removed. + * @see https://dev.mysql.com/doc/refman/5.7/en/string-literals.html + */ + public function testEnsureDefaultsAreUnescapedFromSchemaIntrospection() : void + { + $platform = $this->_sm->getDatabasePlatform(); + $this->_conn->query('DROP TABLE IF EXISTS test_column_defaults_with_create'); + + $escapeSequences = [ + "\\0", // An ASCII NUL (X'00') character + "\\'", "''", // Single quote + '\\"', '""', // Double quote + '\\b', // A backspace character + '\\n', // A new-line character + '\\r', // A carriage return character + '\\t', // A tab character + '\\Z', // ASCII 26 (Control+Z) + '\\\\', // A backslash (\) character + '\\%', // A percent (%) character + '\\_', // An underscore (_) character + ]; + + $default = implode('+', $escapeSequences); + + $sql = "CREATE TABLE test_column_defaults_with_create( + col1 VARCHAR(255) NULL DEFAULT {$platform->quoteStringLiteral($default)} + )"; + $this->_conn->query($sql); + $onlineTable = $this->_sm->listTableDetails("test_column_defaults_with_create"); + self::assertSame($default, $onlineTable->getColumn('col1')->getDefault()); + } } diff --git a/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php b/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php index b8106c43fed..0484e13b3c3 100644 --- a/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php +++ b/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php @@ -526,7 +526,7 @@ public function testGetDefaultValueDeclarationSQL() { // non-timestamp value will get single quotes $field = array( - 'type' => 'string', + 'type' => Type::getType('string'), 'default' => 'non_timestamp' ); diff --git a/tests/Doctrine/Tests/DBAL/Platforms/MariaDb1027PlatformTest.php b/tests/Doctrine/Tests/DBAL/Platforms/MariaDb1027PlatformTest.php new file mode 100644 index 00000000000..79d08d1a07d --- /dev/null +++ b/tests/Doctrine/Tests/DBAL/Platforms/MariaDb1027PlatformTest.php @@ -0,0 +1,50 @@ +_platform->hasNativeJsonType()); + } + + /** + * From MariaDB 10.2.7, JSON type is an alias to LONGTEXT + * @link https://mariadb.com/kb/en/library/json-data-type/ + */ + public function testReturnsJsonTypeDeclarationSQL() : void + { + self::assertSame('LONGTEXT', $this->_platform->getJsonTypeDeclarationSQL([])); + } + + public function testInitializesJsonTypeMapping() : void + { + self::assertTrue($this->_platform->hasDoctrineTypeMappingFor('json')); + self::assertSame(Type::JSON, $this->_platform->getDoctrineTypeMapping('json')); + } + + /** + * Overrides and skips AbstractMySQLPlatformTestCase test regarding propagation + * of unsupported default values for Blob and Text columns. + * + * @see AbstractMySQLPlatformTestCase::testDoesNotPropagateDefaultValuesForUnsupportedColumnTypes() + */ + public function testDoesNotPropagateDefaultValuesForUnsupportedColumnTypes() : void + { + $this->markTestSkipped('MariaDB102Platform support propagation of default values for BLOB and TEXT columns'); + } +} diff --git a/tests/travis/mariadb.mysqli.travis.xml b/tests/travis/mariadb.mysqli.travis.xml new file mode 100644 index 00000000000..84bdac32c04 --- /dev/null +++ b/tests/travis/mariadb.mysqli.travis.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + ../Doctrine/Tests/DBAL + + + + + + performance + locking_functional + + + +