From 1f0bb83786006804c4eb84028f8175f7d15999d7 Mon Sep 17 00:00:00 2001 From: "Matthew J. Mucklo" Date: Sun, 4 Aug 2019 15:49:46 -0700 Subject: [PATCH] Address issue #3631 by adding support for a float datatype Also try to add a new functional test per request. --- .appveyor.yml | 10 +- .scrutinizer.yml | 2 +- .travis.yml | 108 +++++++++++++++- .../DBAL/Driver/AbstractMySQLDriver.php | 7 +- .../DBAL/Driver/Mysqli/MysqliConnection.php | 2 +- .../Mysqli/MysqliConnectionException.php | 10 ++ .../DBAL/Driver/Mysqli/MysqliStatement.php | 16 ++- lib/Doctrine/DBAL/Driver/PDOConnection.php | 2 +- .../DBAL/Driver/PDOConnectionException.php | 10 ++ lib/Doctrine/DBAL/Driver/PDOStatement.php | 1 + lib/Doctrine/DBAL/ParameterType.php | 5 + lib/Doctrine/DBAL/Schema/Column.php | 14 ++ lib/Doctrine/DBAL/Types/FloatType.php | 9 ++ .../Driver/Mysqli/MysqliConnectionTest.php | 2 +- .../DBAL/Functional/Types/DoubleTest.php | 120 ++++++++++++++++++ tests/continuousphp/bootstrap.php | 2 +- tests/travis/install-db2-ibm_db2.sh | 1 + tests/travis/install-language.sh | 9 ++ tests/travis/install-mysql-8.0.sh | 1 + tests/travis/setlocale-de.php | 3 + 20 files changed, 318 insertions(+), 16 deletions(-) create mode 100644 lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnectionException.php create mode 100644 lib/Doctrine/DBAL/Driver/PDOConnectionException.php create mode 100644 tests/Doctrine/Tests/DBAL/Functional/Types/DoubleTest.php create mode 100755 tests/travis/install-language.sh create mode 100644 tests/travis/setlocale-de.php diff --git a/.appveyor.yml b/.appveyor.yml index 63a9c0ddb73..4e0b54d3269 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -22,21 +22,21 @@ environment: driver: sqlsrv db_version: sql2008r2sp2 coverage: yes - php: 7.2 + php: 7.3 - db: mssql driver: sqlsrv db_version: sql2012sp1 - php: 7.2 + php: 7.3 coverage: yes - db: mssql driver: sqlsrv db_version: sql2017 coverage: no - php: 7.2 + php: 7.3 - db: mssql driver: pdo_sqlsrv db_version: sql2017 - php: 7.2 + php: 7.3 coverage: yes init: @@ -103,7 +103,7 @@ install: New-Item -path c:\tools -name ocular -itemtype directory } if (!(Test-Path c:\tools\ocular\ocular.phar)) { - appveyor-retry appveyor DownloadFile https://github.com/scrutinizer-ci/ocular/releases/download/1.5.2/ocular.phar -Filename C:\tools\ocular\ocular.phar + appveyor-retry appveyor DownloadFile https://github.com/scrutinizer-ci/ocular/releases/download/1.6.0/ocular.phar -Filename C:\tools\ocular\ocular.phar Set-Content -path 'C:\tools\ocular\ocular.bat' -Value ('@php C:\tools\ocular\ocular.phar %*') } diff --git a/.scrutinizer.yml b/.scrutinizer.yml index a3b8a5621ca..7ef3efade4c 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -21,7 +21,7 @@ before_commands: tools: external_code_coverage: timeout: 3600 - runs: 30 # 25x Travis (jobs with COVERAGE=yes) + 3x AppVeyor (jobs with coverage=yes) + 2x ContinuousPHP + runs: 32 # 27x Travis (jobs with COVERAGE=yes) + 3x AppVeyor (jobs with coverage=yes) + 2x ContinuousPHP filter: excluded_paths: diff --git a/.travis.yml b/.travis.yml index e0d87665991..a2641b36698 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,16 +23,22 @@ install: script: - | + phpflags= + phpexe= + if [ "x$LANGUAGE" != "x" ]; then + phpflags=-dauto_prepend_file=$PWD/tests/travis/setlocale-de.php + phpexe=php + fi if [ "x$COVERAGE" == "xyes" ]; then - ./vendor/bin/phpunit --configuration tests/travis/$DB.travis.xml --coverage-clover clover.xml + $phpexe $phpflags ./vendor/bin/phpunit --configuration tests/travis/$DB.travis.xml --coverage-clover clover.xml else - ./vendor/bin/phpunit --configuration tests/travis/$DB.travis.xml + $phpexe $phpflags ./vendor/bin/phpunit --configuration tests/travis/$DB.travis.xml fi after_script: - | if [ "x$COVERAGE" == "xyes" ]; then - travis_retry wget https://github.com/scrutinizer-ci/ocular/releases/download/1.5.2/ocular.phar + travis_retry wget https://github.com/scrutinizer-ci/ocular/releases/download/1.6.0/ocular.phar travis_retry php ocular.phar code-coverage:upload --format=php-clover clover.xml fi @@ -339,3 +345,99 @@ jobs: install: - composer config minimum-stability dev - travis_retry composer update --prefer-dist + + - stage: Test + php: 7.2 + env: LANGUAGE=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 LANG=de_DE.UTF-8 DB=mysqli.docker MYSQL_VERSION=8.0 + sudo: required + services: + - docker + before_script: + - bash ./tests/travis/install-language.sh + - bash ./tests/travis/install-mysql-8.0.sh + + - stage: Test + php: 7.3 + env: LANGUAGE=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 LANG=de_DE.UTF-8 DB=mysqli.docker MYSQL_VERSION=8.0 COVERAGE=yes + sudo: required + services: + - docker + before_script: + - bash ./tests/travis/install-language.sh + - bash ./tests/travis/install-mysql-8.0.sh + + - stage: Test + php: 7.3 + env: LANGUAGE=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 LANG=de_DE.UTF-8 DB=mysqli.docker MYSQL_VERSION=5.7 + sudo: required + services: + - docker + before_script: + - bash ./tests/travis/install-language.sh + - bash ./tests/travis/install-mysql-5.7.sh + + - stage: Test + php: 7.3 + env: LANGUAGE=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 LANG=de_DE.UTF-8 DB=sqlite COVERAGE=yes + sudo: required + services: + - docker + before_script: + - bash ./tests/travis/install-language.sh + + - stage: Test + php: 7.3 + env: LANGUAGE=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 LANG=de_DE.UTF-8 DB=pgsql POSTGRESQL_VERSION=10.0 + sudo: required + services: + - postgresql + addons: + postgresql: "9.6" + before_script: + - bash ./tests/travis/install-language.sh + - bash ./tests/travis/install-postgres-10.sh + + - stage: Test + php: 7.3 + env: LANGUAGE=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 LANG=de_DE.UTF-8 DB=pgsql POSTGRESQL_VERSION=11.0 + sudo: required + services: + - docker + before_script: + - bash ./tests/travis/install-language.sh + - bash ./tests/travis/install-postgres-11.sh + + - stage: Test + php: 7.3 + env: LANGUAGE=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 LANG=de_DE.UTF-8 DB=sqlsrv + sudo: required + services: + - docker + before_script: + - bash ./tests/travis/install-language.sh + - bash ./tests/travis/install-sqlsrv-dependencies.sh + - bash ./tests/travis/install-mssql-sqlsrv.sh + - bash ./tests/travis/install-mssql.sh + + - stage: Test + php: 7.3 + env: LANGUAGE=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 LANG=de_DE.UTF-8 DB=pdo_sqlsrv + sudo: required + services: + - docker + before_script: + - bash ./tests/travis/install-language.sh + - bash ./tests/travis/install-sqlsrv-dependencies.sh + - bash ./tests/travis/install-mssql-pdo_sqlsrv.sh + - bash ./tests/travis/install-mssql.sh + + - stage: Test + php: 7.3 + env: LANGUAGE=de_DE.UTF-8 LC_ALL=de_DE.UTF-8 LANG=de_DE.UTF-8 DB=ibm_db2 + sudo: required + services: + - docker + before_script: + - bash ./tests/travis/install-language.sh + - bash ./tests/travis/install-db2.sh + - bash ./tests/travis/install-db2-ibm_db2.sh diff --git a/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php b/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php index c46ddc63dd7..aab5d366958 100644 --- a/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php +++ b/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php @@ -90,8 +90,13 @@ public function convertException($message, DriverException $exception) case '1429': case '2002': case '2005': + case '2054': return new Exception\ConnectionException($message, $exception); - + case '2006': + if ($exception instanceof Driver\Mysqli\MysqliConnectionException || $exception instanceof PDOConnectionException) { + return new Exception\ConnectionException($message, $exception); + } + break; case '1048': case '1121': case '1138': diff --git a/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php b/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php index 1f1a1d218c3..871755702e6 100644 --- a/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php +++ b/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php @@ -68,7 +68,7 @@ public function __construct(array $params, $username, $password, array $driverOp }); try { if (! $this->conn->real_connect($params['host'], $username, $password, $dbname, $port, $socket, $flags)) { - throw new MysqliException($this->conn->connect_error, $this->conn->sqlstate ?? 'HY000', $this->conn->connect_errno); + throw new MysqliConnectionException($this->conn->connect_error, $this->conn->sqlstate ?? 'HY000', $this->conn->connect_errno); } } finally { restore_error_handler(); diff --git a/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnectionException.php b/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnectionException.php new file mode 100644 index 00000000000..bd26b739ea3 --- /dev/null +++ b/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnectionException.php @@ -0,0 +1,10 @@ + 's', ParameterType::INTEGER => 'i', ParameterType::LARGE_OBJECT => 'b', + ParameterType::DOUBLE => 'd', ]; /** @var mysqli */ @@ -272,10 +274,20 @@ private function sendLongData($streams) private function bindUntypedValues(array $values) { $params = []; - $types = str_repeat('s', count($values)); - + $types = ''; foreach ($values as &$v) { $params[] =& $v; + // fix for issue #3631 - detect parameter types as they have to be bound differently + switch (gettype($v)) { + case 'boolean': + $types .= self::$_paramTypeMap[ParameterType::BOOLEAN]; + break; + case 'double': + $types .= self::$_paramTypeMap[ParameterType::DOUBLE]; + break; + default: + $types .= self::$_paramTypeMap[ParameterType::STRING]; + } } return $this->_stmt->bind_param($types, ...$params); diff --git a/lib/Doctrine/DBAL/Driver/PDOConnection.php b/lib/Doctrine/DBAL/Driver/PDOConnection.php index 336542ea50a..d851789f6a8 100644 --- a/lib/Doctrine/DBAL/Driver/PDOConnection.php +++ b/lib/Doctrine/DBAL/Driver/PDOConnection.php @@ -28,7 +28,7 @@ public function __construct($dsn, $user = null, $password = null, ?array $option $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, [PDOStatement::class, []]); $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (\PDOException $exception) { - throw new PDOException($exception); + throw new PDOConnectionException($exception); } } diff --git a/lib/Doctrine/DBAL/Driver/PDOConnectionException.php b/lib/Doctrine/DBAL/Driver/PDOConnectionException.php new file mode 100644 index 00000000000..61de97d103f --- /dev/null +++ b/lib/Doctrine/DBAL/Driver/PDOConnectionException.php @@ -0,0 +1,10 @@ + PDO::PARAM_LOB, ParameterType::LARGE_OBJECT => PDO::PARAM_LOB, ParameterType::BOOLEAN => PDO::PARAM_BOOL, + ParameterType::DOUBLE => PDO::PARAM_STR, ]; private const FETCH_MODE_MAP = [ diff --git a/lib/Doctrine/DBAL/ParameterType.php b/lib/Doctrine/DBAL/ParameterType.php index 422fee895ee..53ba350c7f8 100644 --- a/lib/Doctrine/DBAL/ParameterType.php +++ b/lib/Doctrine/DBAL/ParameterType.php @@ -49,6 +49,11 @@ final class ParameterType */ public const BINARY = 16; + /** + * Represents a double data type. + */ + public const DOUBLE = 17; + /** * This class cannot be instantiated. */ diff --git a/lib/Doctrine/DBAL/Schema/Column.php b/lib/Doctrine/DBAL/Schema/Column.php index 56c39c14994..f7131fb9eec 100644 --- a/lib/Doctrine/DBAL/Schema/Column.php +++ b/lib/Doctrine/DBAL/Schema/Column.php @@ -5,9 +5,13 @@ use Doctrine\DBAL\Types\Type; use const E_USER_DEPRECATED; use function array_merge; +use function is_float; use function is_numeric; +use function localeconv; use function method_exists; use function sprintf; +use function str_replace; +use function strval; use function trigger_error; /** @@ -193,6 +197,16 @@ public function setNotnull($notnull) */ public function setDefault($default) { + if (is_float($default)) { + $localeInfo = localeconv(); + $decimal = $localeInfo['decimal_point'] ?? '.'; + if ($decimal !== '.') { + // SQL standard is '.' for all decimal points so convert to string (issue #3631) + // Also see https://stackoverflow.com/questions/6627239/insert-non-english-decimal-points-in-mysql/6627551#6627551 + $default = strval($default); + $default = str_replace($decimal, '.', $default); + } + } $this->_default = $default; return $this; diff --git a/lib/Doctrine/DBAL/Types/FloatType.php b/lib/Doctrine/DBAL/Types/FloatType.php index 4988d7253dc..f11a0a04f08 100644 --- a/lib/Doctrine/DBAL/Types/FloatType.php +++ b/lib/Doctrine/DBAL/Types/FloatType.php @@ -2,6 +2,7 @@ namespace Doctrine\DBAL\Types; +use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; class FloatType extends Type @@ -29,4 +30,12 @@ public function convertToPHPValue($value, AbstractPlatform $platform) { return $value === null ? null : (float) $value; } + + /** + * {@inheritdoc} + */ + public function getBindingType() + { + return ParameterType::DOUBLE; + } } diff --git a/tests/Doctrine/Tests/DBAL/Driver/Mysqli/MysqliConnectionTest.php b/tests/Doctrine/Tests/DBAL/Driver/Mysqli/MysqliConnectionTest.php index dcfbe715608..41c0e296560 100644 --- a/tests/Doctrine/Tests/DBAL/Driver/Mysqli/MysqliConnectionTest.php +++ b/tests/Doctrine/Tests/DBAL/Driver/Mysqli/MysqliConnectionTest.php @@ -53,7 +53,7 @@ public function testRestoresErrorHandlerOnException() : void new MysqliConnection(['host' => '255.255.255.255'], 'user', 'pass'); self::fail('An exception was supposed to be raised'); } catch (MysqliException $e) { - self::assertSame('Network is unreachable', $e->getMessage()); + self::assertSame(2002, $e->getErrorCode()); } self::assertSame($handler, set_error_handler($default_handler), 'Restoring error handler failed.'); diff --git a/tests/Doctrine/Tests/DBAL/Functional/Types/DoubleTest.php b/tests/Doctrine/Tests/DBAL/Functional/Types/DoubleTest.php new file mode 100644 index 00000000000..47658842edc --- /dev/null +++ b/tests/Doctrine/Tests/DBAL/Functional/Types/DoubleTest.php @@ -0,0 +1,120 @@ +addColumn('id', 'integer'); + $table->addColumn('val', 'float'); + $table->setPrimaryKey(['id']); + + $sm = $this->connection->getSchemaManager(); + $sm->dropAndCreateTable($table); + } + + public function testInsertAndSelect() : void + { + $value1 = 1.1; + $value2 = 77.99999999999; + $value3 = microtime(true); + + $this->insert(1, $value1); + $this->insert(2, $value2); + $this->insert(3, $value3); + + $result1 = $this->select(1); + $result2 = $this->select(2); + $result3 = $this->select(3); + + if (is_string($result1)) { + $result1 = floatval($result1); + $result2 = floatval($result2); + $result3 = floatval($result3); + } + + if ($result1 === false) { + $this->fail('Expected $result1 to not be false'); + return; + } + if ($result2 === false) { + $this->fail('Expected $result2 to not be false'); + return; + } + if ($result3 === false) { + $this->fail('Expected $result3 to not be false'); + return; + } + + $diff1 = abs($result1 - $value1); + $diff2 = abs($result2 - $value2); + $diff3 = abs($result3 - $value3); + + $this->assertLessThanOrEqual(0.0001, $diff1, sprintf('%f, %f, %f', $diff1, $result1, $value1)); + $this->assertLessThanOrEqual(0.0001, $diff2, sprintf('%f, %f, %f', $diff2, $result2, $value2)); + $this->assertLessThanOrEqual(0.0001, $diff3, sprintf('%f, %f, %f', $diff3, $result3, $value3)); + + $result1 = $this->selectDouble($value1); + $result2 = $this->selectDouble($value2); + $result3 = $this->selectDouble($value3); + + $this->assertSame(is_int($result1) ? 1 : '1', $result1); + $this->assertSame(is_int($result2) ? 2 : '2', $result2); + $this->assertSame(is_int($result3) ? 3 : '3', $result3); + } + + private function insert(int $id, float $value) : void + { + $result = $this->connection->insert('double_table', [ + 'id' => $id, + 'val' => $value, + ], [ + ParameterType::INTEGER, + ParameterType::DOUBLE, + ]); + + self::assertSame(1, $result); + } + + /** + * @return mixed + */ + private function select(int $id) + { + return $this->connection->fetchColumn( + 'SELECT val FROM double_table WHERE id = ?', + [$id], + 0, + [ParameterType::INTEGER] + ); + } + + /** + * @return mixed + */ + private function selectDouble(float $value) + { + return $this->connection->fetchColumn( + 'SELECT id FROM double_table WHERE val = ?', + [$value], + 0, + [ParameterType::DOUBLE] + ); + } +} diff --git a/tests/continuousphp/bootstrap.php b/tests/continuousphp/bootstrap.php index 9f6b0971a79..882812d0657 100644 --- a/tests/continuousphp/bootstrap.php +++ b/tests/continuousphp/bootstrap.php @@ -23,7 +23,7 @@ $file = $_SERVER['argv'][$pos + 1]; register_shutdown_function(static function () use ($file) : void { - $cmd = 'wget https://github.com/scrutinizer-ci/ocular/releases/download/1.5.2/ocular.phar' + $cmd = 'wget https://github.com/scrutinizer-ci/ocular/releases/download/1.6.0/ocular.phar' . ' && php ocular.phar code-coverage:upload --format=php-clover ' . escapeshellarg($file); passthru($cmd); diff --git a/tests/travis/install-db2-ibm_db2.sh b/tests/travis/install-db2-ibm_db2.sh index b59bb6396fd..fc98805571f 100644 --- a/tests/travis/install-db2-ibm_db2.sh +++ b/tests/travis/install-db2-ibm_db2.sh @@ -5,6 +5,7 @@ set -ex echo "Installing extension" ( # updating APT packages as per support recommendation + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 6B05F25D762E3157 sudo apt -y -q update sudo apt install ksh diff --git a/tests/travis/install-language.sh b/tests/travis/install-language.sh new file mode 100755 index 00000000000..3f9ccea4cab --- /dev/null +++ b/tests/travis/install-language.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -ex + +echo "Installing language pack..." + +sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 6B05F25D762E3157 +sudo apt -y -q update +sudo apt install language-pack-de diff --git a/tests/travis/install-mysql-8.0.sh b/tests/travis/install-mysql-8.0.sh index 952a4300ba7..6e0557aa03c 100644 --- a/tests/travis/install-mysql-8.0.sh +++ b/tests/travis/install-mysql-8.0.sh @@ -12,6 +12,7 @@ sudo docker run \ -p 33306:3306 \ --name mysql80 \ mysql:8.0 \ + mysqld \ --default-authentication-plugin=mysql_native_password sudo docker exec -i mysql80 bash <<< 'until echo \\q | mysql doctrine_tests > /dev/null 2>&1 ; do sleep 1; done' diff --git a/tests/travis/setlocale-de.php b/tests/travis/setlocale-de.php new file mode 100644 index 00000000000..e47ee1e2001 --- /dev/null +++ b/tests/travis/setlocale-de.php @@ -0,0 +1,3 @@ +