Skip to content

Commit 03397ec

Browse files
committed
Fix handling timestamp with time zone in Oracle
1 parent fd47abd commit 03397ec

File tree

4 files changed

+135
-2
lines changed

4 files changed

+135
-2
lines changed

src/Driver/OCI8/Middleware/InitializeSession.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,40 @@
66
use Doctrine\DBAL\Driver\Connection;
77
use Doctrine\DBAL\Driver\Middleware;
88
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
9+
use Doctrine\Deprecations\Deprecation;
910
use SensitiveParameter;
1011

1112
class InitializeSession implements Middleware
1213
{
14+
private $useProperTzFormat = false;
15+
16+
public function __construct(bool $useProperTzFormat = false)
17+
{
18+
if (! $useProperTzFormat) {
19+
Deprecation::triggerIfCalledFromOutside(
20+
'doctrine/dbal',
21+
'https://github.com/doctrine/dbal/pull/6012',
22+
'Not passing `true` in argument 1 for "%s()" is deprecated, as this value ensures the proper format for'
23+
. ' the "NLS_TIMESTAMP_TZ_FORMAT" session option. This argument will be removed in 4.0 as the proper'
24+
. ' behavior will be used by default.',
25+
__METHOD__,
26+
);
27+
}
28+
29+
$this->useProperTzFormat = $useProperTzFormat;
30+
}
31+
1332
public function wrap(Driver $driver): Driver
1433
{
15-
return new class ($driver) extends AbstractDriverMiddleware {
34+
return new class ($driver, $this->useProperTzFormat) extends AbstractDriverMiddleware {
35+
private $useProperTzFormat = false;
36+
37+
public function __construct(Driver $driver, bool $useProperTzFormat = false)
38+
{
39+
parent::__construct($driver);
40+
$this->useProperTzFormat = $useProperTzFormat;
41+
}
42+
1643
/**
1744
* {@inheritDoc}
1845
*/
@@ -22,13 +49,16 @@ public function connect(
2249
): Connection {
2350
$connection = parent::connect($params);
2451

52+
// Use "YYYY-MM-DD HH24:MI:SSTZH:TZM" in 4.0.
53+
$tzFormat = $this->useProperTzFormat ? 'YYYY-MM-DD HH24:MI:SSTZH:TZM' : 'YYYY-MM-DD HH24:MI:SS TZH:TZM';
54+
2555
$connection->exec(
2656
'ALTER SESSION SET'
2757
. " NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
2858
. " NLS_TIME_FORMAT = 'HH24:MI:SS'"
2959
. " NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
3060
. " NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
31-
. " NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZH:TZM'"
61+
. " NLS_TIMESTAMP_TZ_FORMAT = '" . $tzFormat . "'"
3262
. " NLS_NUMERIC_CHARACTERS = '.,'",
3363
);
3464

src/Platforms/OraclePlatform.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ public function getNowExpression($type = 'timestamp')
8383
);
8484

8585
switch ($type) {
86+
case 'timestamptz':
87+
return 'TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SSTZH:TZM\')';
8688
case 'date':
8789
case 'time':
8890
case 'timestamp':
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Functional\Types;
6+
7+
use DateTimeImmutable;
8+
use DateTimeZone;
9+
use Doctrine\DBAL\Schema\Table;
10+
use Doctrine\DBAL\Tests\FunctionalTestCase;
11+
use Doctrine\DBAL\Tests\TestUtil;
12+
use Doctrine\DBAL\Types\Type;
13+
use Doctrine\DBAL\Types\Types;
14+
15+
use function sprintf;
16+
17+
class DateTimeTzImmutableTypeTest extends FunctionalTestCase
18+
{
19+
private const TEST_TABLE = 'datetimetz_test';
20+
21+
protected function setUp(): void
22+
{
23+
if (! TestUtil::isDriverOneOf('pdo_oci', 'oci8')) {
24+
self::markTestSkipped('This test is only allowed for "pdo_oci" and "oci8" by now.');
25+
}
26+
27+
$table = new Table(self::TEST_TABLE);
28+
$table->addColumn('id', Types::INTEGER);
29+
30+
$table->addColumn('val', Types::DATETIMETZ_IMMUTABLE);
31+
$table->setPrimaryKey(['id']);
32+
33+
$this->dropAndCreateTable($table);
34+
}
35+
36+
public function testInsertAndSelect(): void
37+
{
38+
$platform = $this->connection->getDatabasePlatform();
39+
$dateTimeTzImmutableType = Type::getType(Types::DATETIMETZ_IMMUTABLE);
40+
41+
$id1 = 1;
42+
$value1 = new DateTimeImmutable('1986-03-22 19:45:30', new DateTimeZone('America/Argentina/Buenos_Aires'));
43+
44+
$this->insert($id1, $value1);
45+
46+
$res1 = $this->select($id1);
47+
48+
$resultDateTimeTzValue = $dateTimeTzImmutableType
49+
->convertToPHPValue($res1, $platform)
50+
->setTimezone(new DateTimeZone('UTC'));
51+
52+
self::assertInstanceOf(DateTimeImmutable::class, $resultDateTimeTzValue);
53+
self::assertSame($value1->getTimestamp(), $resultDateTimeTzValue->getTimestamp());
54+
self::assertSame($value1->getTimestamp(), $resultDateTimeTzValue->getTimestamp());
55+
self::assertSame('UTC', $resultDateTimeTzValue->format('T'));
56+
self::assertSame('1986-03-22T22:45:30+00:00', $resultDateTimeTzValue->format(DateTimeImmutable::ATOM));
57+
}
58+
59+
private function insert(int $id, DateTimeImmutable $value): void
60+
{
61+
$result = $this->connection->insert(self::TEST_TABLE, [
62+
'id' => $id,
63+
'val' => $value,
64+
], [
65+
Types::INTEGER,
66+
Type::getType(Types::DATETIMETZ_IMMUTABLE),
67+
]);
68+
69+
self::assertSame(1, $result);
70+
}
71+
72+
private function select(int $id): string
73+
{
74+
$value = $this->connection->fetchOne(
75+
sprintf('SELECT val FROM %s WHERE id = ?', self::TEST_TABLE),
76+
[$id],
77+
[Types::INTEGER],
78+
);
79+
80+
self::assertIsString($value);
81+
82+
return $value;
83+
}
84+
}

tests/Platforms/OraclePlatformTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,23 @@ public function asciiStringSqlDeclarationDataProvider(): array
927927
];
928928
}
929929

930+
/** @psalm-return iterable<int, array{string, string}> */
931+
public function getNowExpressionCases(): iterable
932+
{
933+
yield ['TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SSTZH:TZM\')', 'timestamptz'];
934+
yield ['TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SS\')', 'date'];
935+
yield ['TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SS\')', 'time'];
936+
yield ['TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SS\')', 'timestamp'];
937+
yield ['TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SS\')', 'unknown_unsupported_type'];
938+
}
939+
940+
/** @dataProvider getNowExpressionCases */
941+
public function testGetNowExpression(string $expected, string $type): void
942+
{
943+
/** @psalm-suppress DeprecatedMethod */
944+
self::assertSame($expected, $this->platform->getNowExpression($type));
945+
}
946+
930947
protected function getLimitOffsetCastToIntExpectedQuery(): string
931948
{
932949
return 'SELECT * FROM (SELECT a.*, ROWNUM AS doctrine_rownum FROM (SELECT * FROM user) a WHERE ROWNUM <= 3)'

0 commit comments

Comments
 (0)