Commit c3ecc4b9 authored by Benjamin Eberlei's avatar Benjamin Eberlei

DBAL-22 - Fix DateTime with Timezone handling in Postgres and Oracle by...

DBAL-22 - Fix DateTime with Timezone handling in Postgres and Oracle by introducing new type DateTimeTz and have SchemaManagers of both platforms detect this type. Added general conversion testcase which made some bugs in Oracle Date Handling and $stmt->fetchColumn() appear
parent ea700de1
......@@ -186,7 +186,7 @@ class OCI8Statement implements \Doctrine\DBAL\Driver\Statement
*/
public function fetchColumn($columnIndex = 0)
{
$row = oci_fetch_row($this->_sth);
$row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
return $row[$columnIndex];
}
......
......@@ -45,7 +45,7 @@ class OracleSessionInit implements EventSubscriber
{
protected $_defaultSessionVars = array(
'NLS_TIME_FORMAT' => "HH24:MI:SS",
'NLS_DATE_FORMAT' => "YYYY-MM-DD",
'NLS_DATE_FORMAT' => "YYYY-MM-DD HH24:MI:SS",
'NLS_TIMESTAMP_FORMAT' => "YYYY-MM-DD HH24:MI:SS",
'NLS_TIMESTAMP_TZ_FORMAT' => "YYYY-MM-DD HH24:MI:SS TZH:TZM",
);
......
......@@ -1652,6 +1652,17 @@ abstract class AbstractPlatform
throw DBALException::notSupported(__METHOD__);
}
/**
* Obtain DBMS specific SQL to be used to create datetime with timezone offset fields.
*
* @param array $fieldDeclaration
*/
public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
{
throw DBALException::notSupported(__METHOD__);
}
/**
* Obtain DBMS specific SQL to be used to create date fields in statements
* like CREATE TABLE.
......@@ -1825,15 +1836,23 @@ abstract class AbstractPlatform
* the format of a stored datetime value of this platform.
*
* @return string The format string.
*
* @todo We need to get the specific format for each dbms and override this
* function for each platform
*/
public function getDateTimeFormatString()
{
return 'Y-m-d H:i:s';
}
/**
* Gets the format string, as accepted by the date() function, that describes
* the format of a stored datetime with timezone value of this platform.
*
* @return string The format string.
*/
public function getDateTimeTzFormatString()
{
return 'Y-m-d H:i:s';
}
/**
* Gets the format string, as accepted by the date() function, that describes
* the format of a stored date value of this platform.
......
......@@ -172,7 +172,7 @@ class MySqlPlatform extends AbstractPlatform
/** @override */
public function getClobTypeDeclarationSQL(array $field)
{
if ( ! empty($field['length'])) {
if ( ! empty($field['length']) && is_numeric($field['length'])) {
$length = $field['length'];
if ($length <= 255) {
return 'TINYTEXT';
......
......@@ -192,6 +192,14 @@ class OraclePlatform extends AbstractPlatform
* @override
*/
public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
{
return 'TIMESTAMP(0)';
}
/**
* @override
*/
public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration)
{
return 'TIMESTAMP(0) WITH TIME ZONE';
}
......@@ -602,11 +610,21 @@ LEFT JOIN all_cons_columns r_cols
return "CREATE GLOBAL TEMPORARY TABLE";
}
public function getDateTimeFormatString()
public function getDateTimeTzFormatString()
{
return 'Y-m-d H:i:sP';
}
public function getDateFormatString()
{
return 'Y-m-d 00:00:00';
}
public function getTimeFormatString()
{
return '1900-01-01 H:i:s';
}
public function fixSchemaElementName($schemaElementName)
{
if (strlen($schemaElementName) > 30) {
......@@ -672,7 +690,7 @@ LEFT JOIN all_cons_columns r_cols
'char' => 'string',
'nchar' => 'string',
'date' => 'datetime',
'timestamp' => 'datetime',
'timestamp' => 'datetimetz',
'float' => 'decimal',
'long' => 'string',
'clob' => 'text',
......
......@@ -531,6 +531,14 @@ class PostgreSqlPlatform extends AbstractPlatform
* @override
*/
public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration)
{
return 'TIMESTAMP(0) WITHOUT TIME ZONE';
}
/**
* @override
*/
public function getDateTimeTypeTzDeclarationSQL(array $fieldDeclaration)
{
return 'TIMESTAMP(0) WITH TIME ZONE';
}
......@@ -611,7 +619,7 @@ class PostgreSqlPlatform extends AbstractPlatform
return strtolower($column);
}
public function getDateTimeFormatString()
public function getDateTimeTzFormatString()
{
return 'Y-m-d H:i:sO';
}
......@@ -666,7 +674,7 @@ class PostgreSqlPlatform extends AbstractPlatform
'date' => 'date',
'datetime' => 'datetime',
'timestamp' => 'datetime',
'timestamptz' => 'datetime',
'timestamptz' => 'datetimetz',
'time' => 'time',
'timetz' => 'time',
'float' => 'decimal',
......
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
......@@ -171,6 +169,19 @@ abstract class AbstractSchemaManager
return $this->_getPortableTableIndexesList($tableIndexes, $table);
}
/**
* Return true if all the given tables exist.
*
* @param array $tableNames
* @return bool
*/
public function tablesExist($tableNames)
{
$tableNames = array_map('strtolower', (array)$tableNames);
return count($tableNames) == count(\array_intersect($tableNames, array_map('strtolower', $this->listTableNames())));
}
/**
* Return a list of all tables in the current database
*
......
......@@ -41,7 +41,7 @@ class Column extends AbstractAsset
/**
* @var int
*/
protected $_length = 255;
protected $_length = null;
/**
* @var int
......
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\DBAL\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
/**
* DateTime type saving additional timezone information.
*
* Caution: Databases are not necessarily experts at storing timezone related
* data of dates. First, of all the supported vendors only PostgreSQL and Oracle
* support storing Timezone data. But those two don't save the actual timezone
* attached to a DateTime instance (for example "Europe/Berlin" or "America/Montreal")
* but the current offset of them related to UTC. That means depending on daylight saving times
* or not you may get different offsets.
*
* This datatype makes only sense to use, if your application works with an offset, not
* with an actual timezone that uses transitions. Otherwise your DateTime instance
* attached with a timezone such as Europe/Berlin gets saved into the database with
* the offset and re-created from persistence with only the offset, not the original timezone
* attached.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.com
* @since 1.0
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
*/
class DateTimeTzType extends Type
{
public function getName()
{
return Type::DATETIMETZ;
}
public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getDateTimeTzTypeDeclarationSQL($fieldDeclaration);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return ($value !== null)
? $value->format($platform->getDateTimeTzFormatString()) : null;
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return ($value !== null)
? \DateTime::createFromFormat($platform->getDateTimeTzFormatString(), $value) : null;
}
}
\ No newline at end of file
......@@ -38,6 +38,7 @@ abstract class Type
const BIGINT = 'bigint';
const BOOLEAN = 'boolean';
const DATETIME = 'datetime';
const DATETIMETZ = 'datetimetz';
const DATE = 'date';
const TIME = 'time';
const DECIMAL = 'decimal';
......@@ -61,6 +62,7 @@ abstract class Type
self::STRING => 'Doctrine\DBAL\Types\StringType',
self::TEXT => 'Doctrine\DBAL\Types\TextType',
self::DATETIME => 'Doctrine\DBAL\Types\DateTimeType',
self::DATETIMETZ => 'Doctrine\DBAL\Types\DateTimeTzType',
self::DATE => 'Doctrine\DBAL\Types\DateType',
self::TIME => 'Doctrine\DBAL\Types\TimeType',
self::DECIMAL => 'Doctrine\DBAL\Types\DecimalType'
......
......@@ -34,6 +34,7 @@ class AllTests
$suite->addTestSuite('Doctrine\Tests\DBAL\Types\ArrayTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Types\ObjectTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Types\DateTimeTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Types\DateTimeTzTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Types\DateTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Types\TimeTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Types\BooleanTest');
......
......@@ -3,6 +3,7 @@
namespace Doctrine\Tests\DBAL\Functional;
use Doctrine\Tests\DBAL\Functional;
use Doctrine\Tests\TestUtil;
if (!defined('PHPUnit_MAIN_METHOD')) {
define('PHPUnit_MAIN_METHOD', 'Dbal_Functional_AllTests::main');
......@@ -21,14 +22,24 @@ class AllTests
{
$suite = new \Doctrine\Tests\DbalFunctionalTestSuite('Doctrine Dbal Functional');
$conn= TestUtil::getConnection();
$sm = $conn->getSchemaManager();
if ($sm instanceof Doctrine\DBAL\Schema\SqliteSchemaManager) {
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\Schema\SqliteSchemaManagerTest');
} else if ($sm instanceof Doctrine\DBAL\Schema\MySqlSchemaManager) {
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\Schema\MySqlSchemaManagerTest');
} else if ($sm instanceof Doctrine\DBAL\Schema\PostgreSqlSchemaManager) {
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\Schema\PostgreSqlSchemaManagerTest');
} else if ($sm instanceof Doctrine\DBAL\Schema\OracleSchemaManager) {
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\Schema\OracleSchemaManagerTest');
} else if ($sm instanceof Doctrine\DBAL\Schema\DB2SchemaManager) {
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\Schema\Db2SchemaManagerTest');
}
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\ConnectionTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\DataAccessTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\WriteTest');
$suite->addTestSuite('Doctrine\Tests\DBAL\Functional\TypeConversionTest');
return $suite;
}
......
<?php
namespace Doctrine\Tests\DBAL\Functional;
use Doctrine\DBAL\Types\Type;
require_once __DIR__ . '/../../TestInit.php';
class TypeConversionTest extends \Doctrine\Tests\DbalFunctionalTestCase
{
static private $typeCounter = 0;
public function setUp()
{
parent::setUp();
/* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */
$sm = $this->_conn->getSchemaManager();
if (!$sm->tablesExist(array('type_conversion'))) {
$table = new \Doctrine\DBAL\Schema\Table("type_conversion");
$table->addColumn('id', 'integer', array('notnull' => false));
$table->addColumn('test_string', 'string', array('notnull' => false));
$table->addColumn('test_boolean', 'boolean', array('notnull' => false));
$table->addColumn('test_bigint', 'bigint', array('notnull' => false));
$table->addColumn('test_smallint', 'bigint', array('notnull' => false));
$table->addColumn('test_datetime', 'datetime', array('notnull' => false));
$table->addColumn('test_date', 'date', array('notnull' => false));
$table->addColumn('test_time', 'time', array('notnull' => false));
$table->addColumn('test_text', 'text', array('notnull' => false));
$table->addColumn('test_array', 'array', array('notnull' => false));
$table->addColumn('test_object', 'object', array('notnull' => false));
$table->setPrimaryKey(array('id'));
$sm->createTable($table);
}
}
static public function dataIdempotentDataConversion()
{
$obj = new \stdClass();
$obj->foo = "bar";
$obj->bar = "baz";
return array(
array('string', 'ABCDEFGaaaBBB', 'string'),
array('boolean', true, 'bool'),
array('boolean', false, 'bool'),
array('bigint', 12345678, 'string'),
array('smallint', 123, 'int'),
array('datetime', new \DateTime('2010-04-05 10:10:10'), 'DateTime'),
array('date', new \DateTime('2010-04-05'), 'DateTime'),
array('time', new \DateTime('10:10:10'), 'DateTime'),
array('text', str_repeat('foo ', 1000), 'string'),
array('array', array('foo' => 'bar'), 'array'),
array('object', $obj, 'object'),
);
}
/**
* @dataProvider dataIdempotentDataConversion
* @param string $type
* @param mixed $originalValue
* @param string $expectedPhpType
*/
public function testIdempotentDataConversion($type, $originalValue, $expectedPhpType)
{
$columnName = "test_" . $type;
$typeInstance = Type::getType($type);
$insertionValue = $typeInstance->convertToDatabaseValue($originalValue, $this->_conn->getDatabasePlatform());
$this->_conn->insert('type_conversion', array('id' => ++self::$typeCounter, $columnName => $insertionValue));
$sql = "SELECT " . $columnName . " FROM type_conversion WHERE id = " . self::$typeCounter;
$actualDbValue = $typeInstance->convertToPHPValue($this->_conn->fetchColumn($sql), $this->_conn->getDatabasePlatform());
$this->assertType($expectedPhpType, $actualDbValue, "The expected type from the conversion to and back from the database should be " . $expectedPhpType);
$this->assertEquals($originalValue, $actualDbValue, "Conversion between values should produce the same out as in value, but doesnt!");
}
}
\ No newline at end of file
......@@ -21,17 +21,19 @@ class DateTimeTest extends \Doctrine\Tests\DbalTestCase
public function testDateTimeConvertsToDatabaseValue()
{
$this->assertTrue(
is_string($this->_type->convertToDatabaseValue(new \DateTime(), $this->_platform))
);
$date = new \DateTime('1985-09-01 10:10:10');
$expected = $date->format($this->_platform->getDateTimeTzFormatString());
$actual = is_string($this->_type->convertToDatabaseValue($date, $this->_platform));
$this->assertEquals($expected, $actual);
}
public function testDateTimeConvertsToPHPValue()
{
// Birthday of jwage and also birthday of Doctrine. Send him a present ;)
$this->assertTrue(
$this->_type->convertToPHPValue('1985-09-01 00:00:00', $this->_platform)
instanceof \DateTime
);
$date = $this->_type->convertToPHPValue('1985-09-01 00:00:00', $this->_platform);
$this->assertType('DateTime', $date);
$this->assertEquals('1985-09-01 00:00:00', $date->format('Y-m-d H:i:s'));
}
}
\ No newline at end of file
<?php
namespace Doctrine\Tests\DBAL\Types;
use Doctrine\DBAL\Types\Type;
use Doctrine\Tests\DBAL\Mocks;
require_once __DIR__ . '/../../TestInit.php';
class DateTimeTzTest extends \Doctrine\Tests\DbalTestCase
{
protected
$_platform,
$_type;
protected function setUp()
{
$this->_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform();
$this->_type = Type::getType('datetimetz');
}
public function testDateTimeConvertsToDatabaseValue()
{
$date = new \DateTime('1985-09-01 10:10:10');
$expected = $date->format($this->_platform->getDateTimeTzFormatString());
$actual = is_string($this->_type->convertToDatabaseValue($date, $this->_platform));
$this->assertEquals($expected, $actual);
}
public function testDateTimeConvertsToPHPValue()
{
// Birthday of jwage and also birthday of Doctrine. Send him a present ;)
$date = $this->_type->convertToPHPValue('1985-09-01 00:00:00', $this->_platform);
$this->assertType('DateTime', $date);
$this->assertEquals('1985-09-01 00:00:00', $date->format('Y-m-d H:i:s'));
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment