Pomelo.entityframeworkcore.mysql: Problem with linq SUM

Created on 10 Jan 2017  路  14Comments  路  Source: PomeloFoundation/Pomelo.EntityFrameworkCore.MySql

Steps to reproduce

Simple model where field is type byte -> tinyint
public class Survey { public byte Rating {get;set;} }
Run following query:

var rating = context.Surveys.Sum(s => s.Rating);

The issue

Above query casing exception (see bellow). If cast rating to decimal

var rating = context.Surveys.Sum(s => (decimal)s.Rating);
there is no error, but application is stopping working with SQL Server, because it complains about casting opposite way.

Exception message:
Stack trace:

InvalidCastException: Unable to cast object of type 'System.Decimal' to type 'System.Int32'.

    MySql.Data.MySqlClient.Results.Row.GetInt32(int ordinal)
    lambda_method(Closure , DbDataReader )

closed-fixed type-bug

Most helpful comment

Promoting all Sum calls to decimal would be unexpected, I think. LINQ to Objects returns the same type as the input (for overloads it defines) but can throw, e.g., new[] { Int32.MaxValue, Int32.MaxValue }.Sum() throws an OverflowException.

If we added implicit conversion from decimal to int (in MySqlConnector), we'd get roughly the same behaviour: return int by default but throw OverflowException for large values.

Existing issue at mysql-net/MySqlConnector#54.

All 14 comments

@bgrainger Should this be an upstream issue?

@msmolka can you paste the output from running SHOW COLUMNS FROM Surveys on your MySQL Server? (Assuming the table name is Surveys.)

Hello
here is my output:

COLUMNS

| Field | Type | Null | Key | Default | Extra |
| - | - | - | - | - | - |
| Id | int(11) | NO | PRI | | auto_increment |
| FeedbackId | int(11) | NO | MUL | | |
| Rating | tinyint(3) unsigned | NO | | | |
| Type | tinyint(3) unsigned | NO | | | |
| CreatedDate | datetime(6) | NO | | CURRENT_TIMESTAMP(6) | |

At this moment to support both MySql and SqlServer I need to have conditional linq:

 var surveys = databaseProvider.Equals("MySql", StringComparison.OrdinalIgnoreCase)
                ? await context.Surveys.Where(s => (s.Feedback.CustomerId == this.CustomerId) && ((s.Type == 0) || (s.Type == 1)))
                    .SumAsync(f => (decimal)f.Rating, cancellationToken)
                : await context.Surveys.Where(s => (s.Feedback.CustomerId == this.CustomerId) && ((s.Type == 0) || (s.Type == 1)))
                    .SumAsync(f => f.Rating, cancellationToken);

It is not working without casting in MySql. Below is my model

    public class Survey
    {
        public DateTimeOffset CreatedDate { get; set; }
        public virtual Feedback Feedback { get; set; }
        public int FeedbackId { get; set; }
        public int Id { get; set; }
        public byte Rating { get; set; }
        public byte Type { get; set; }    
    }

and model builder

builder.ToTable("Survey");
builder.HasKey(u => u.Id);
builder.Property(u => u.Id).ValueGeneratedOnAdd();
builder.Property(u => u.CreatedDate).ValueGeneratedOnAdd();

I pushed a new test to try to reproduce this issue as reported: https://github.com/mysql-net/MySqlConnector/commit/89aed4a81f6c944f0bc2bc5eaf37e07f68daac9c

I created a table with a column Rating tinyint(3) unsigned not null then invoked MySqlDataReader.GetInt32(0) (which should call into Row.GetInt32, as given in your call stack. I was unable to reproduce an InvalidCastException from System.Decimal: AppVeyor, Travis.

I'm testing with MySQL 5.7 on Windows and Linux.

@msmolka What MySQL Server and version are you using?

I haven't repro'ed the problem with EF (since my knowledge of that is more limited). @caleblloyd would EF introduce any additional wrinkles here that could cause the connector to deserialize a tinyint(3) value as a decimal?

(Or am I reading the exception/call stack wrongly?)

Hello
I've attached simple project together with table create script. Tested on MariaDB and MySQL. I don't think it is related to MySQL version.

ConsoleApp2.zip

As you can see it is crashing on line 36

I think it is related to #52

I can reproduce with the sample app and will dig in further.

The executed statement is SELECT SUM(s.Rating) FROMSurveyASs;. MySQL appears to promote the type of this result from tinyint(3) to 0xF6 (decimal). This is why MySqlConnector throws an exception casting a decimal to an int.

Just checked the MySQL docs

For numeric arguments, the variance and standard deviation functions return a DOUBLE value. The SUM() and AVG() functions return a DECIMAL value for exact-value arguments (integer or DECIMAL), and a DOUBLE value for approximate-value arguments (FLOAT or DOUBLE).

Our EF layer is designed to save a single byte as an unsigned tinyint. So summing the tinyints in the DB is correctly (by MySQL's design) returning a decimal.

By LINQ's definition, we probably should be returning a Byte here though. I'll look at some casting options in the EF layer tomorrow.

By LINQ's definition, we probably should be returning a Byte here though.

Seems like it could be better to promote it to Int32 (to avoid truncation); that may be what SQL Server is doing here. (Enumerable.Sum doesn't appear to have an overload for byte for LINQ to Objects.)

I wasn't sure if LINQ supplied a generic for SUM that had the same return type as argument type.

If avoiding truncation is our primary concern, the result would be best left as a decimal for all integer sums.

If parity with SQL server is our primary concern, we'd need to see what they do for all of their different integer types.

@kagamine what's your thoughts?

Promoting all Sum calls to decimal would be unexpected, I think. LINQ to Objects returns the same type as the input (for overloads it defines) but can throw, e.g., new[] { Int32.MaxValue, Int32.MaxValue }.Sum() throws an OverflowException.

If we added implicit conversion from decimal to int (in MySqlConnector), we'd get roughly the same behaviour: return int by default but throw OverflowException for large values.

Existing issue at mysql-net/MySqlConnector#54.

lgtm

Fixed the MySqlConnector issue in 0.11.2.

Shipped in 1.1.1-prerelease-10012

Was this page helpful?
0 / 5 - 0 ratings