Skip to content
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

Add microseconds support on datetime/time types #2873

Open
lcobucci opened this issue Oct 1, 2017 · 34 comments
Open

Add microseconds support on datetime/time types #2873

lcobucci opened this issue Oct 1, 2017 · 34 comments

Comments

@lcobucci
Copy link
Member

lcobucci commented Oct 1, 2017

This was suggested on doctrine/orm#6510, the user has the need of sorting entries with a more accurate precision. Some comments about this on #1515

@PabloKowalczyk
Copy link

Any progress with this issue?

@nick4fake
Copy link

It was a really unexpected finding. Are there any workarounds?

@PabloKowalczyk
Copy link

@nick4fake Yes, you can write something like this: https://github.com/PabloKowalczyk/Todora/blob/master/src/DateTimeImmutableMicrosecondsType.php
This class extends Doctrine\DBAL\Types\VarDateTimeImmutableType so it will be preserving microseconds when fetching date from DB and convertToDatabaseValue implementation will save microseconds to DB.

Note: This class covers only \DateTimeImmutable instances. If you need working schema tool you should also override getSQLDeclaration method.

Don't forget about type override:
Symfony:

doctrine:
    dbal:
        types:
            datetime_immutable: \Todora\DateTimeImmutableMicrosecondsType

Also you can read this: http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/known-vendor-issues.html#datetime-datetimetz-and-time-types

@ossinkine
Copy link

Adding .u to AbstractPlatform.php#L3356 does not solve the issue?

@Majkl578
Copy link
Contributor

Before this can be considered, portability needs to be assessed, since the underlying data types must support saving fractional seconds.

@morozov What do you think about this for 3.0?

@morozov
Copy link
Member

morozov commented Aug 17, 2018

@Majkl578 as for the portability assessment, I think we already have a decent majority (SQL Server supports fractions too). It doesn't look like a breaking change, so it can go to master.

@Majkl578 Majkl578 added this to the 3.0.0 milestone Aug 17, 2018
@VasekPurchart
Copy link

MariaDB: ?

MariaDB has also 0-6 fractional seconds from 5.3 https://mariadb.com/kb/en/library/microseconds-in-mariadb/

@kiler129
Copy link

The status as of now:

Based on \Doctrine\DBAL\Platforms\AbstractPlatform::getDateTimeFormatString:

  • SQLServer & SQLServer2008 forces .000 as microseconds
  • SQLAnywhere supports .u
  • Default sets no microseconds

So changing this is trivial, we just need to ensure it will not explode and this needs to be considered on per platform basis as Majkl said.

@TomHAnderson
Copy link
Member

This PR was started because I created a custom type for date time with microseconds:

#3291

weaverryan added a commit to symfony/symfony-docs that referenced this issue May 24, 2019
…uzet)

This PR was submitted for the master branch but it was merged into the 4.3 branch instead (closes #10616).

Discussion
----------

[Messenger] Describe the doctrine transport

Add documentation relative to the PR symfony/symfony#29007

TODO

- [ ] Document the max granularity in seconds for delay (doctrine/dbal#2873)

Commits
-------

27c0f9d [Messenger] Describe the doctrine tranport
@morozov morozov removed this from the 3.0.0 milestone Jun 2, 2019
@rquadling
Copy link
Contributor

rquadling commented Sep 11, 2019

> * MySQL: supports 0-6 fractional seconds since 5.7: https://dev.mysql.com/doc/refman/8.0/en/fractional-seconds.html

MySQL 5.6.4 and up expands fractional seconds support for TIME, DATETIME, and TIMESTAMP values, with up to microseconds (6 digits) precision:
https://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html

@flaushi
Copy link

flaushi commented Sep 25, 2019

Is it possible to just overwrite or re-register the datetime type? (.u should work on postgres and mysql)

In a symfony-setting like so:

doctrine:
    dbal:
        types:
            datetime: App\DBAL\Types\CarbonMicrosecondsType
class CarbonMicrosecondsType extends Type
{
    const TYPENAME = 'datetime';

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] == true) {
            return 'TIMESTAMP';
        }
        if($platform instanceof PostgreSqlPlatform)
            return 'TIMESTAMP(6) WITHOUT TIME ZONE';
        else
            return 'DATETIME(6)';
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if($value === null || $value instanceof CarbonInterface)
            return $value;

        if ($value instanceof DateTimeInterface) {
            return Carbon::instance($value);
        }

        $val = Carbon::createFromFormat('Y-m-d H:i:s.u', $value);

        if ( ! $val) {
            $val = Carbon::instance(date_create($value));
        }

        if ( ! $val) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                'Y-m-d H:i:s.u'
            );
        }

        return $val;
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if (null === $value) {
            return $value;
        }

        if ($value instanceof DateTimeInterface) {
            //var_dump('inner '.$value->format('Y-m-d H:i:s.u'));
            return $value->format('Y-m-d H:i:s.u');
        }

        throw ConversionException::conversionFailedInvalidType(
            $value,
            $this->getName(),
            ['null', 'DateTime']
        );
    }

    public function getName()
    {
        return self::TYPENAME;
    }
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}

@PabloKowalczyk
Copy link

@flaushi please remember that not everyone have Carbon installed, more portable version will be with DateTime(Immutable).

@flaushi
Copy link

flaushi commented Sep 26, 2019

@PabloKowalczyk Yes, absolutely, but one can replace Carbon with DateTime eaasily. For the sake of completeness, I solved my problem posted above. The reason was that postgres returns in format Y-m-d H:i:s if microseconds are set to zero, and in format Y-m-d H:i:s.u if not. So I finally use this type, which works fine:

class CarbonMicrosecondsType extends Type
{
    const TYPENAME = 'datetime';

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] == true) {
            return 'TIMESTAMP';
        }
        if($platform instanceof PostgreSqlPlatform)
            return 'TIMESTAMP(6) WITHOUT TIME ZONE';
        else
            return 'DATETIME(6)';
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if($value === null || $value instanceof CarbonInterface)
            return $value;

        if ($value instanceof DateTimeInterface) {
            return Carbon::instance($value);
        }
        $val = DateTime::createFromFormat('Y-m-d H:i:s.u', $value);

        if ( ! $val) {
            $val = Carbon::instance(date_create($value));
        } else {
            $val = Carbon::instance($val);
        }

        if ( ! $val) {
            throw ConversionException::conversionFailedFormat(
                $value,
                $this->getName(),
                'Y-m-d H:i:s.u'
            );
        }

        return $val;
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if (null === $value) {
            return $value;
        }

        if ($value instanceof DateTimeInterface) {
            return $value->format('Y-m-d H:i:s.u');
        }

        throw ConversionException::conversionFailedInvalidType(
            $value,
            $this->getName(),
            ['null', 'DateTime']
        );
    }

    public function getName()
    {
        return self::TYPENAME;
    }
    public function requiresSQLCommentHint(AbstractPlatform $platform)
    {
        return true;
    }
}

@siraic
Copy link

siraic commented Oct 7, 2019

I overwrote the DateTimeImmutable type, made the length dependant on the 'precision' fieldDeclaration, and simply added '.u' to the datetime formats. It caused some unexpected behaviour because by default mysql rounds times instead of truncates.

We're used to truncating times in PHP (with the format method you only print the parts you need), but mysql rounds the values that fall outside the column definition. This behaviour can be changed in mysql 8: https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#sqlmode_time_truncate_fractional.

When using mysql 5.6, 5.7 or 8 without the TIME_TRUNCATE_FRACTIONAL mode, be aware that sending 2018-12-31 23:59:59.50000 to a DATETIME(0) field will be stored as 2019-01-01 00:00:00

@flaushi
Copy link

flaushi commented Oct 7, 2019

Yes, mysql and postgres have strange behaviours, I agree. A really good advice is to use date_create($value), because it seems to be quite flexible in parsing the timestamps with and without microseconds.

@flaushi
Copy link

flaushi commented Oct 19, 2019

I am coming back to this issue, because now we have a use case e.g. in symfony/symfony#33785.

I tried to change the used type in symfony messenger, but it relies on doctrine's builtin datetime type.
Would it at least be possible to include a new microseconds type in doctrine, s.t. the referenced issue could be solved by using this "official" type?

I can well understand that the symfony team will not be willing to introduce a custom DBAL type on their side, but just selecting a new microseconds-based one will be possible I guess.

@kylekatarnls
Copy link

Hi, please let me reanimate this issue.

I found that microseconds-precision (6) was the default for SQLServer2012Platform:
https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Platforms/SQLServer2012Platform.php#L1156

But all other platforms had (0). I would say the dates should be stored with the maximal precision available for proper sorting and comparisons.

And it seems relevant than the "precision" field declaration option should work the same way for floating numbers and dates. So I would expect an annotation like: @ORM\Column(type="datetime", precision=3) would set the datetime precision to 3 (milliseconds) and the default annotation @ORM\Column(type="datetime") would set the precision to 6 by default.

If the precision would exceed the maximum supported by the platform, this maximum should be used instead.

Is there anything preventing us to implement it that way? If not, I would be happy to propose a pull-request for that.

@flaushi
Copy link

flaushi commented Mar 22, 2020

@kylekatarnls, looks good. But is there any platform that does not support microseconds (precision = 6)?

@kylekatarnls
Copy link

According to @Majkl578 There is at least SQLite that only support 0 or 3: #2873 (comment)

And some other platforms/versions to be checked.

But there is Travis unit tests in this repo to check every supported platform, so we could just give a try and add unit tests for the precision and see if a platform fail on Travis.

@flaushi
Copy link

flaushi commented Mar 22, 2020

Yes, right, SQLite doesn't make thinks easier all the time...

So, great idea!

@kylekatarnls
Copy link

So here is the summary:

PostgreSQL: 0-6: https://www.postgresql.org/docs/10/static/datatype-datetime.html
MySQL: 0-6 since 5.7: https://dev.mysql.com/doc/refman/8.0/en/fractional-seconds.html
SQLite: has no built-in date/time type, but date/time functions support 0 or 3 fractional seconds: https://www.sqlite.org/lang_datefunc.html
Oracle: 0-9: https://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements003.htm#BABGIGCJ
DB2: 0-12: https://www.ibm.com/support/knowledgecenter/en/SSEPEK_10.0.0/sqlref/src/tpc/db2z_timestampvalues.html
MariaDB: 0-6 since 5.3: https://mariadb.com/kb/en/library/microseconds-in-mariadb/
SQL Server: 0-7:
https://docs.microsoft.com/fr-fr/sql/t-sql/data-types/datetime-transact-sql?view=sql-server-ver15
SQL Anywhere: 0-6: http://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.help.sqlanywhere.12.0.1/dbreference/datetime-date-wsqltype.html
SQL Azure: see SQL Server

I would add that custom types like the one proposed by @flaushi here #2873 (comment) has no way to distinguish precision set to 0 from precision not set. Annotation (int) cast replace null with 0 so both appears as 0 in $fieldDeclaration it's too bad because if you set a default to 6 in a custom type you would not be able to use precision=0 in ORM\Column() annotation, it would work with any value but 0.

As a first step precision should be passed as null (for date types) when not set. As it's purely ignored for the moment by the internal types it would make no difference for existing types but it would allow custom types choose an other default value and allowing customization.

@akalineskou
Copy link

I added microseconds to datetime (for postgres v10+), by extending the postgres platform to return the timeformat with the microseconds, added the platform service in doctrine config, extended the datetime type to return the correct sql declaration for postgres and overriding the type in doctrine config.

The migrations I had to manually create (since the diff doesn't see the change), but afterwards the datetime fields are created with the correct sql declaration.

doctrine.yaml

doctrine:
    dbal:
        platform_service: PostgreSQLMicrosecondsPlatform

        types:
            datetime: DateTimeMicrosecondsType

PostgreSQLMicrosecondsPlatform.php

<?php
use Doctrine\DBAL\Platforms\PostgreSQL100Platform;

class PostgreSQLMicrosecondsPlatform extends PostgreSQL100Platform
{
    /**
     * @inheritDoc
     */
    public function getDateTimeFormatString()
    {
        return sprintf('%s.u', parent::getDateTimeFormatString());
    }
}

DateTimeMicrosecondsType.php

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\PostgreSQL100Platform;
use Doctrine\DBAL\Types\DateTimeType;

class DateTimeMicrosecondsType extends DateTimeType
{
    /**
     * @inheritDoc
     */
    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        if ($platform instanceof PostgreSQL100Platform) {
            return 'TIMESTAMP(6) WITHOUT TIME ZONE';
        }

        return parent::getSQLDeclaration($column, $platform);
    }
}

@rquadling
Copy link
Contributor

Just in case anyone is using a VERY old version of PHP and Doctrine, and is wondering why it may be that fractional seconds are not working as expected, there was a bug:76386 in mysqlnd that was fixed in PHP 7.3.0.

@jzecca
Copy link

jzecca commented May 17, 2022

@akalineskou

I added microseconds to datetime (for postgres v10+), by extending the postgres platform to return the timeformat with the microseconds, added the platform service in doctrine config, extended the datetime type to return the correct sql declaration for postgres and overriding the type in doctrine config.

Is there a reason why you extend the DateTime type instead of just setting its declaration in the Platform?

This seems to work for me (but I'm no expert when it comes to Doctrine internals):

# config/packages/doctrine.yaml
doctrine:
    dbal:
        platform_service: App\Doctrine\Platform\PostgreSQLPlatform
namespace App\Doctrine\Platform;

use Doctrine\DBAL\Platforms\PostgreSQL100Platform;

class PostgreSQLPlatform extends PostgreSQL100Platform
{
    public function getDateTimeFormatString(): string
    {
        return str_replace('s', 's.u', parent::getDateTimeFormatString());
    }

    public function getDateTimeTzFormatString(): string
    {
        return str_replace('s', 's.u', parent::getDateTimeTzFormatString());
    }

    public function getTimeFormatString(): string
    {
        return str_replace('s', 's.u', parent::getTimeFormatString());
    }

    public function getDateTimeTypeDeclarationSQL(array $column): string
    {
        return str_replace('(0)', '(6)', parent::getDateTimeTypeDeclarationSQL($column));
    }

    public function getDateTimeTzTypeDeclarationSQL(array $column): string
    {
        return str_replace('(0)', '(6)', parent::getDateTimeTzTypeDeclarationSQL($column));
    }

    public function getTimeTypeDeclarationSQL(array $column): string
    {
        return str_replace('(0)', '(6)', parent::getTimeTypeDeclarationSQL($column));
    }
}

@akalineskou
Copy link

@jzecca Can't remember exactly why, but I think I wanted to just change the datetime without altering the others as a proof of concept...

@tyteen4a03
Copy link

+1 - we've had to put in a workaround for 10 years now so would be great to finally see this fixed.

@isodude
Copy link

isodude commented Aug 23, 2022

In MariaDB there's a bug (I think?) that converts DATETIME(0) to DATETIME(6) when running SELECT GREATEST(field1, COALESCE(field2, 0)) ... It works properly if either GREATEST or COALESCE is not nested.

@mtournay
Copy link

Hi

according to the response in this thread, it's not a bug

https://jira.mariadb.org/browse/MDEV-4560?jql=text%20~%20%22greatest%20datetime%22

@isodude
Copy link

isodude commented Aug 23, 2022

Hi

according to the response in this thread, it's not a bug

https://jira.mariadb.org/browse/MDEV-4560?jql=text%20~%20%22greatest%20datetime%22

I think it's not related

(MariaDB 10.5.16)

MariaDB [(none)]> SELECT GREATEST('2015-01-01 00:00:00', NOW());
+----------------------------------------+
| GREATEST('2015-01-01 00:00:00', NOW()) |
+----------------------------------------+
| 2022-08-23 11:55:21                    |
+----------------------------------------+
1 row in set (0.001 sec)

And here's the 'bug', it seems that converting 0 to datetime(0) makes the problem go away.

*************************** 1. row ***************************
working: 2022-08-23 12:34:33
  wrong: 2022-08-23 12:34:33.000000
CREATE TEMPORARY TABLE MyInlineTable (id LONG, created DATETIME(0), updated DATETIME(0) );

INSERT INTO MyInlineTable VALUES
(1, NOW(),NOW());

SELECT
  GREATEST(created, COALESCE(updated, CONVERT('0', DATETIME(0)))) AS working,
  GREATEST(created, COALESCE(updated, 0)) AS wrong
FROM MyInlineTable \G

@mtournay
Copy link

Hi

indeed you're right : forcing type resolves issue, and it's what i did explain here

akeneo/pim-community-dev#17711

This is a huge difference between mysql/maria, and honestly I don't know what to think.
Obviously it's not to doctrine to manage that, but to sql script to be compatible with both engines

@isodude
Copy link

isodude commented Aug 23, 2022

I'm talking to him as well :)

We ended up solving the last mysteries with JSON so Akeneo should at least be somewhat compatible with MariaDB :)

Let's ignore this problem for this thread. And yes, it's impossible for Doctrine to just assume things here.

@nesl247
Copy link

nesl247 commented May 22, 2024

We're using postgres 15.5 and it seems to be storing with microseconds when using now() for example, so at this time, DateTimeTzImmutableType seems to be broken. We're receiving Could not convert database value \"2024-03-22 21:08:49.568415+00\" to Doctrine Type datetimetz_immutable. Expected format: Y-m-d H:i:sO.

@danielgelling
Copy link

danielgelling commented Sep 10, 2024

@akalineskou

I added microseconds to datetime (for postgres v10+), by extending the postgres platform to return the timeformat with the microseconds, added the platform service in doctrine config, extended the datetime type to return the correct sql declaration for postgres and overriding the type in doctrine config.

Is there a reason why you extend the DateTime type instead of just setting its declaration in the Platform?

This seems to work for me (but I'm no expert when it comes to Doctrine internals):

# config/packages/doctrine.yaml
doctrine:
    dbal:
        platform_service: App\Doctrine\Platform\PostgreSQLPlatform

Does this even work? I have added a custom database platform, but doctrine seems to keep using the Doctrine\DBAL\Platforms\MySQL80Platform class. Came across this old issue from 2012, not sure if it's still relevant though: doctrine/DoctrineBundle#4

EDIT: Aparently this feature was deprecated and has been removed in doctrine/dbal:^4.0, however the version constraint of doctrine/DoctrineBundle on dbal is loose and also allows configuration for dbal:^3.7.0. See: #6524 (comment)

Working example using driver middleware:

<?php

declare(strict_types=1);

namespace App\Doctrine\Middleware;

use App\Doctrine\Platform\MySQLPlatform;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\ServerVersionProvider;

final class PlatformMiddleware implements Middleware
{
    public function wrap(Driver $driver): Driver
    {
        return new class ($driver) extends AbstractDriverMiddleware {
            /**
             * {@inheritDoc}
             */
            public function getDatabasePlatform(
                ServerVersionProvider $versionProvider
            ): AbstractPlatform {
                return new MySQLPlatform();
            }
        };
    }
}

@danielgelling
Copy link

danielgelling commented Sep 10, 2024

I think this issue can be closed. As it is not desirable to add this to the MySQLPlatform provided by Doctrine. Because not all RDBMS support datetimes with precision, and would make it impossible to safely switch to a different database platform.

It should always be a conscious decision of the developer to choose a specific platform, where features like datetime precision should be considered.

Features can easily be added through driver middleware in the application itself. I have added a full example as a gist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests