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

TimeSpan type does not map correctly to TIME MySQL type #1046

Closed
ptsneves opened this issue Mar 19, 2020 · 3 comments
Closed

TimeSpan type does not map correctly to TIME MySQL type #1046

ptsneves opened this issue Mar 19, 2020 · 3 comments

Comments

@ptsneves
Copy link

ptsneves commented Mar 19, 2020

Create a model with a Timespan

class MyModel {
   TimeSpan t;
}
---
MyModel m() {t = TimeSpan.MaxValue};
db_context.Add(m);
await db_context.SaveChangesAsync()

The TimeSpan type creates a TIME typed column which has a smaller constraint than TimeSpan. Specifically TIME only goes from '-838:59:59.000000' to '838:59:59.000000' . while TimeSpan can reach 256204778:48:05.477580.

Unhandled exception. Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
 ---> MySql.Data.MySqlClient.MySqlException (0x80004005): Incorrect TIME value: '256204778:48:05.477580'
 ---> MySql.Data.MySqlClient.MySqlException (0x80004005): Incorrect TIME value: '256204778:48:05.477580'
   at MySqlConnector.Core.ServerSession.ReceiveReplyAsyncAwaited(ValueTask`1 task) in C:\projects\mysqlconnector\src\MySqlConnector\Core\ServerSession.cs:line 774
   at MySqlConnector.Core.ResultSet.ReadResultSetHeaderAsync(IOBehavior ioBehavior) in C:\projects\mysqlconnector\src\MySqlConnector\Core\ResultSet.cs:line 49
   at MySql.Data.MySqlClient.MySqlDataReader.ActivateResultSet() in C:\projects\mysqlconnector\src\MySqlConnector\MySql.Data.MySqlClient\MySqlDataReader.cs:line 130
   at MySql.Data.MySqlClient.MySqlDataReader.CreateAsync(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, IDictionary`2 cachedProcedures, IMySqlCommand command, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) in C:\projects\mysqlconnector\src\MySqlConnector\MySql.Data.MySqlClient\MySqlDataReader.cs:line 391
   at MySqlConnector.Core.CommandExecutor.ExecuteReaderAsync(IReadOnlyList`1 commands, ICommandPayloadCreator payloadCreator, CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken) in C:\projects\mysqlconnector\src\MySqlConnector\Core\CommandExecutor.cs:line 62
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(DbContext _, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Pomelo.EntityFrameworkCore.MySql.Storage.Internal.MySqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
@mguinness
Copy link
Collaborator

mguinness commented Mar 19, 2020

Take a look at Value Conversions, specifically TimeSpanToStringConverter and TimeSpanToTicksConverter if you need a timespan greater than a month.

@ptsneves
Copy link
Author

ptsneves commented Mar 20, 2020

@mguinness Thanks for the tip. Am i correct in interpreting the timespan converter as being translated to number of ticks, that is a Long in the database?
Also if there is nothing you can do i will close the bug report.

@lauxjpn
Copy link
Collaborator

lauxjpn commented Mar 20, 2020

The TimeSpan type creates a TIME typed column which has a smaller constraint than TimeSpan. Specifically TIME only goes from '-838:59:59.000000' to '838:59:59.000000' . while TimeSpan can reach 256204778:48:05.477580.

This is expected. We currently handle TimeSpan as values being smaller than 24 hours. So even new TimeSpan(1, 0, 0, 0) will not work. This mimics the behavior of SQL Server.

As @mguinness pointed out, using TimeSpanToTicksConverter is the way to go here. It can be used like this:

entity.Property(e => e.BestServedBefore)
    .HasColumnType("bigint")
    .HasConversion(new TimeSpanToTicksConverter());

It is equivalent to the following code:

entity.Property(e => e.BestServedBefore)
    .HasColumnType("bigint")
    .HasConversion(v => v.Ticks, v => new TimeSpan(v));

Here is a fully functional example:

using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
using Pomelo.EntityFrameworkCore.MySql.Storage;

namespace IssueConsoleTemplate
{
    public class IceCream
    {
        public int IceCreamId { get; set; }
        public string Name { get; set; }
        public TimeSpan BestServedBefore { get; set; }
    }

    public class Context : DbContext
    {
        public DbSet<IceCream> IceCreams { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseMySql(
                    "server=127.0.0.1;port=3306;user=root;password=;database=Issue1046",
                    b => b
                        .ServerVersion(new ServerVersion("8.0.20-mysql"))
                        .CharSetBehavior(CharSetBehavior.NeverAppend))
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<IceCream>(
                entity =>
                {
                    entity.Property(e => e.BestServedBefore)
                        .HasColumnType("bigint")
                        .HasConversion(new TimeSpanToTicksConverter());
                    
                    entity.HasData(
                        new IceCream
                        {
                            IceCreamId = 1,
                            Name = "Vanilla",
                            BestServedBefore = new TimeSpan(0, 12, 0, 0)
                        },
                        new IceCream
                        {
                            IceCreamId = 2,
                            Name = "Chocolate",
                            BestServedBefore = new TimeSpan(0, 23, 59, 59)
                        },
                        new IceCream
                        {
                            IceCreamId = 3,
                            Name = "Artificial Vanilla",
                            
                            // This will not work out-of-the box.
                            // Usage of a converter is necessary.
                            BestServedBefore = new TimeSpan(42, 11, 0, 0)
                        }
                    );
                });
        }
    }

    internal class Program
    {
        private static void Main()
        {
            using var context = new Context();

            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            var iceCreams = context.IceCreams
                .OrderBy(i => i.IceCreamId)
                .ToList();

            Debug.Assert(iceCreams.Count == 3);
            Debug.Assert(iceCreams[0].BestServedBefore == new TimeSpan(0, 12, 0, 0));
            Debug.Assert(iceCreams[1].BestServedBefore == new TimeSpan(0, 23, 59, 59));
            Debug.Assert(iceCreams[2].BestServedBefore == new TimeSpan(42, 11, 0, 0));
        }
    }
}

ptsneves added a commit to ptsneves/drohub that referenced this issue Apr 21, 2020
…er to ticks

The default timespan type in the database is Time, which is not adequate
for a timespan because it reflects a date, with all date quirks including
arbitrary limitations on the maximum timespan that can be recorded. As
recommended by PomeloFoundation we should use a converter and force the
table to be bigint. See for details, including the link to Microsoft.

In this commit we needed to adapt the datatypes that convert minutes to
TimeSpan to be long, as the number of minutes in the maximum timespan can
be bigger than int.MaxValue. A test on the limits of AllowedFlightTime was
also added to make sure the actual TimeSpan limits map directly to the database
representation.

[1] PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1046
ptsneves added a commit to ptsneves/drohub that referenced this issue Dec 17, 2021
…er to ticks

The default timespan type in the database is Time, which is not adequate
for a timespan because it reflects a date, with all date quirks including
arbitrary limitations on the maximum timespan that can be recorded. As
recommended by PomeloFoundation we should use a converter and force the
table to be bigint. See for details, including the link to Microsoft.

In this commit we needed to adapt the datatypes that convert minutes to
TimeSpan to be long, as the number of minutes in the maximum timespan can
be bigger than int.MaxValue. A test on the limits of AllowedFlightTime was
also added to make sure the actual TimeSpan limits map directly to the database
representation.

[1] PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1046
ptsneves added a commit to ptsneves/drohub that referenced this issue Dec 18, 2021
…er to ticks

The default timespan type in the database is Time, which is not adequate
for a timespan because it reflects a date, with all date quirks including
arbitrary limitations on the maximum timespan that can be recorded. As
recommended by PomeloFoundation we should use a converter and force the
table to be bigint. See for details, including the link to Microsoft.

In this commit we needed to adapt the datatypes that convert minutes to
TimeSpan to be long, as the number of minutes in the maximum timespan can
be bigger than int.MaxValue. A test on the limits of AllowedFlightTime was
also added to make sure the actual TimeSpan limits map directly to the database
representation.

[1] PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1046
ptsneves added a commit to ptsneves/drohub that referenced this issue Dec 18, 2021
…er to ticks

The default timespan type in the database is Time, which is not adequate
for a timespan because it reflects a date, with all date quirks including
arbitrary limitations on the maximum timespan that can be recorded. As
recommended by PomeloFoundation we should use a converter and force the
table to be bigint. See for details, including the link to Microsoft.

In this commit we needed to adapt the datatypes that convert minutes to
TimeSpan to be long, as the number of minutes in the maximum timespan can
be bigger than int.MaxValue. A test on the limits of AllowedFlightTime was
also added to make sure the actual TimeSpan limits map directly to the database
representation.

[1] PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1046
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

3 participants