Skip to content

[SQLite] Support timezone information on SqlitePlatform::getDateTimeTzFormatString() #6006

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ on:
- cron: "42 3 * * *"

env:
fail-fast: true
fail-fast: false

jobs:
phpunit-smoke-check:
name: "PHPUnit with SQLite"
runs-on: "${{ matrix.os }}"

strategy:
fail-fast: false
matrix:
os:
- "ubuntu-22.04"
Expand Down Expand Up @@ -373,15 +374,15 @@ jobs:
- "7.4"
- "8.0"
mysql-version:
- "5.7"
# - "5.7"
- "8.0"
extension:
- "mysqli"
- "pdo_mysql"
config-file-suffix:
- ""
include:
- mysql-version: "5.7"
# - mysql-version: "5.7"
- mysql-version: "8.0"
# https://stackoverflow.com/questions/60902904/how-to-pass-mysql-native-password-to-mysql-service-in-github-actions
custom-entrypoint: >-
Expand Down
2 changes: 1 addition & 1 deletion src/Driver/OCI8/Middleware/InitializeSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function connect(
. " NLS_TIME_FORMAT = 'HH24:MI:SS'"
. " 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'"
. " NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SSTZH:TZM'"
. " NLS_NUMERIC_CHARACTERS = '.,'",
);

Expand Down
10 changes: 10 additions & 0 deletions src/Platforms/MySQL80Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
*/
class MySQL80Platform extends MySQL57Platform
{
/**
* {@inheritdoc}
*
* @link https://dev.mysql.com/doc/refman/8.0/en/date-and-time-literals.html#date-and-time-string-numeric-literals
*/
public function getDateTimeTzFormatString()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overriding getDateTimeTzFormatString without overriding getDateTimeTzTypeDeclarationSQL (so that it does not fallback to the non-tz type) will not provide a working implementation of the type (it will actually make things worse than doing the silent fallback consistently)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid I'm missing the point here.
AFAIK, there are no specific types on MySQL for TIMESTAMP nor DATETIME:

MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of the tz type is that it stores a value for a precise datetime, ensuring that when reading it again, we get the same point in time without having to agree on which timezone is implicitly used by the code.

For a tz type, we need to make sure that on insertion, we can provide a timezone and it is not ignored, but also that on reads, we can get a timezone info from the DB.
If MySQL works only with its implicit timezone on reads, it means we cannot support a tz type properly in DBAL (note that I'm not a fan of the fact that DBAL silently falls back to the non-tz type as it hides the fact that your db is not timezone aware anymore).

There is a good reason is the EcmaScript proposal for the Temporal API has separate objects representing ZonedDateTime and PlainDateTime.

{
return 'Y-m-d H:i:sP';
}

/**
* {@inheritdoc}
*
Expand Down
2 changes: 2 additions & 0 deletions src/Platforms/OraclePlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public function getNowExpression($type = 'timestamp')
);

switch ($type) {
case 'timestamptz':
return 'TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SSTZH:TZM\')';
case 'date':
case 'time':
case 'timestamp':
Expand Down
6 changes: 6 additions & 0 deletions src/Platforms/SqlitePlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ public function getCurrentDatabaseExpression(): string
return "'main'";
}

/** @inheritDoc */
public function getDateTimeTzFormatString()
{
return 'Y-m-d H:i:sO';
}

/**
* {@inheritDoc}
*/
Expand Down
81 changes: 81 additions & 0 deletions tests/Functional/Types/DateTimeTzImmutableTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Types;

use DateTimeImmutable;
use DateTimeZone;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;

use function sprintf;

class DateTimeTzImmutableTypeTest extends FunctionalTestCase
{
private const TEST_TABLE = 'datetimetz_test';

protected function setUp(): void
{
$this->iniSet('date.timezone', 'UTC');

$table = new Table(self::TEST_TABLE);
$table->addColumn('id', Types::INTEGER);

$table->addColumn('val', Types::DATETIMETZ_IMMUTABLE);
$table->setPrimaryKey(['id']);

$this->dropAndCreateTable($table);
}

public function testInsertAndSelect(): void
{
$platform = $this->connection->getDatabasePlatform();
$dateTimeTzImmutableType = Type::getType(Types::DATETIMETZ_IMMUTABLE);

$id1 = 1;
$value1 = new DateTimeImmutable('1986-03-22 19:45:30', new DateTimeZone('America/Argentina/Buenos_Aires'));

$this->insert($id1, $value1);

$res1 = $this->select($id1);

$resultDateTimeTzValue = $dateTimeTzImmutableType
->convertToPHPValue($res1, $platform)
->setTimezone(new DateTimeZone('UTC'));

self::assertInstanceOf(DateTimeImmutable::class, $resultDateTimeTzValue);
self::assertSame($value1->getTimestamp(), $resultDateTimeTzValue->getTimestamp());
self::assertSame($value1->getTimestamp(), $resultDateTimeTzValue->getTimestamp());
self::assertSame('UTC', $resultDateTimeTzValue->format('T'));
self::assertSame('1986-03-22T22:45:30+00:00', $resultDateTimeTzValue->format(DateTimeImmutable::ATOM));
}

private function insert(int $id, DateTimeImmutable $value): void
{
$result = $this->connection->insert(self::TEST_TABLE, [
'id' => $id,
'val' => $value,
], [
Types::INTEGER,
Type::getType(Types::DATETIMETZ_IMMUTABLE),
]);

self::assertSame(1, $result);
}

private function select(int $id): string
{
$value = $this->connection->fetchOne(
sprintf('SELECT val FROM %s WHERE id = ?', self::TEST_TABLE),
[$id],
[Types::INTEGER],
);

self::assertIsString($value);

return $value;
}
}
12 changes: 12 additions & 0 deletions tests/Platforms/SqlitePlatformTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Doctrine\DBAL\Tests\Platforms;

use DateTimeImmutable;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\SqlitePlatform;
Expand Down Expand Up @@ -763,4 +764,15 @@ public function testGetCreateTableSQLWithColumnCollation(): void
$this->platform->getCreateTableSQL($table),
);
}

public function testGetDateTimeTzFormatString(): void
{
$date = DateTimeImmutable::createFromFormat(
$this->platform->getDateTimeTzFormatString(),
'1986-03-22 22:45:30-03:00',
);

self::assertInstanceOf(DateTimeImmutable::class, $date);
self::assertSame('GMT-0300', $date->format('T'));
}
}