Implementation Translate DateDiff#10264
Conversation
|
I really needed that! |
|
Ref: #9585 |
| /// <param name="dateStart">The start date</param> | ||
| /// <param name="dateEnd">The end date</param> | ||
| /// <param name="datePart">datePart</param> | ||
| public DateDiffExpression([NotNull] Expression dateStart, [NotNull] Expression dateEnd, [CanBeNull] Expression datePart) |
There was a problem hiding this comment.
There is no need to create DateDiffExpression.
Utilize existing SqlFunctionExpression
smitpatel
left a comment
There was a problem hiding this comment.
The methods should be similar to SqlMethods from Linq to SQL rather than inventing our own. Also put in memory implementation.
|
@patricdeveloper - See discussion at #10241 It specifies how to use DateDiff in current released version. |
|
DateTime.Now.Subtract(DateTime.Now).TotalDays > 0
Forget it! |
06c6510 to
ac635ca
Compare
| /// <param name="startDate">Starting date for the calculation.</param> | ||
| /// <param name="endDate">Ending date for the calculation.</param> | ||
| /// <returns>Number of year boundaries crossed between the dates.</returns> | ||
| public static int? DateDiffYear( |
There was a problem hiding this comment.
This should go to Core. like EFFunctions.Like
Also tests would go to specs tests and SqlServer would be only provider translating it.
d90706e to
aebfc93
Compare
ralmsdeveloper
left a comment
There was a problem hiding this comment.
@smitpatel Done!
I also changed the title to make it more pleasant. 😏
|
@anpete - Can you take a look at the function introduced on EF.Functions to see if it is according to what is our plan. |
|
@smitpatel This being the default in EF.Functions will be much better, especially for people who are now coming to .Net Core |
|
@patricdeveloper - We understand #10241 is work-around only. Ideally we want to add quite a few useful functions which are used by many customers in EF.Functions for easier access. But till we reach there, UDFs are providing a very good work-around. Its not ideal solution but it basically unblock you. |
| DateTime? startDate, | ||
| DateTime? endDate) | ||
| { | ||
| if (startDate.HasValue && endDate.HasValue) |
| DateTimeOffset? startDate, | ||
| DateTimeOffset? endDate) | ||
| { | ||
| if (startDate.HasValue && endDate.HasValue) |
There was a problem hiding this comment.
Same for other methods further down
|
@ralmsdeveloper Looks good! Should we implement support for SQLite? It looks like at least something should be possible based on http://www.sqlite.org/lang_datefunc.html cc @bricelam |
|
Thanks @anpete . I took a look at http://www.sqlite.org/lang_datefunc.html, and I think it's possible to do that too! |
aebfc93 to
7d37a15
Compare
|
I wrote a lot of possible date and time translations for SQLite here once... |
|
Thanks @bricelam, I looked and liked it, it will be very useful. |
|
@anpete Anything else I can do here? |
|
I think we would prefer to get SQLite as part of this if possible. 😁 |
| { | ||
| fixture.TestSqlLoggerFactory.Clear(); | ||
| } | ||
| => fixture.TestSqlLoggerFactory.Clear(); |
There was a problem hiding this comment.
Ok, @smitpatel, I did this to delete warnings, which appeared after the change in .editorconfig 😁
smitpatel
left a comment
There was a problem hiding this comment.
Follow exact translations from #4108 (comment)
With only additional change of explicit cast if its needed.
No translation related to DateTimeOffset on SQLite.
|
|
||
| Assert.Equal(0, count); | ||
| } | ||
| } |
There was a problem hiding this comment.
Add test for DateDiffNanosecond
| { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddHours), new[] { typeof(double) }), "hours" }, | ||
| { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMinutes), new[] { typeof(double) }), "minutes" }, | ||
| { typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddSeconds), new[] { typeof(double) }), "seconds" } | ||
| // TODO: Future implementation, how to do the translation de TIME ZONE? |
There was a problem hiding this comment.
Remove these comments. Till we actually decide to support it, there is not much value putting it here.
| private static readonly string _sqliteUtc = "'utc'"; | ||
|
|
||
| private static readonly MethodInfo _concatExpression | ||
| = typeof(string).GetRuntimeMethod(nameof(string.Concat), new[] { typeof(Expression), typeof(string) }); |
There was a problem hiding this comment.
No method of string exists which takes expression & string params. It would match with object one instead.
There was a problem hiding this comment.
Please do not look at this yet ...
|
|
||
| if (firstArgument.NodeType == ExpressionType.Convert) | ||
| { | ||
| firstArgument = new ExplicitCastExpression(firstArgument, typeof(string)); |
There was a problem hiding this comment.
Why do we need explicit cast here? Convert to string should be fine. in SQL it will be string value anyway. And we may need convert to align types for add.
| var concatDateAdd = | ||
| firstArgument.NodeType == ExpressionType.Extension | ||
| ? Expression.Add(firstArgument, new SqlFragmentExpression($"' {datePart}'"), _concatExpression) | ||
| : (Expression)new SqlFragmentExpression($"'{firstArgument.ToString()} {datePart}'"); |
There was a problem hiding this comment.
firstArgument.ToString() is big NO.
|
|
||
| if (_methodInfoDateDiffMapping.TryGetValue(methodCallExpression.Method, out var datePartCalculate)) | ||
| { | ||
| return new ExplicitCastExpression( |
| public virtual Expression Translate(MemberExpression memberExpression) | ||
| { | ||
| if (memberExpression.Expression != null | ||
| && (memberExpression.Member.Name == nameof(DateTime.Date) |
There was a problem hiding this comment.
avoid this check. Switch is already doing that.
| { | ||
| switch (memberName) | ||
| { | ||
| case nameof(DateTime.Year): |
There was a problem hiding this comment.
make this a dictionary and use trygetvalue. You can do similar change in sqlserver if you want.
| /// </summary> | ||
| public class SqliteDateTimeNowTranslator : IMemberTranslator | ||
| { | ||
| private static string _sqliteFormatDate = "'%Y-%m-%d %H:%M:%S'"; |
There was a problem hiding this comment.
@bricelam - Can you check what should be the full date format since it is different from what we generate in SQL for SQLite.
| return new ExplicitCastExpression( | ||
| Expression.Divide( | ||
| Expression.Subtract( | ||
| new SqlFunctionExpression( |
There was a problem hiding this comment.
Translate them exactly how this sql is formed. #4108 (comment)
There was a problem hiding this comment.
@smitpatel, but that's what's being done.
This comment was edited in a few minutes, the CAST I had already implemented.
If you look at: #10264 (comment)
This is already being done. The translation is being done.
There was a problem hiding this comment.
The current implementation (at the time of reviewing) for date diff was finding seconds for 2 dates and subtracting them diving by some number to change units.
like (Date1.Seconds - Date2.Seconds)/ (const to convert to year) to find out DateDiffYear. But client side code is Date1.Year - Date2.Year
The translation should be same for server/client both as much as possible.
So translation becomes
Cast(strftime("%Y", date1) as int) - Cast(strftime("%Y", date2) as int)
There was a problem hiding this comment.
@smitpatel,
For this case of year and second, do only one subtraction, more for the rest, of course, has to be done calculation!
the output for sql, is good now!
--DateDiffDay
SELECT "p"."Id", "p"."Data"
FROM "Test" AS "p"
WHERE CAST((strftime('%s', "p"."Data") - strftime('%s', strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime'))) / (60 * 60 * 24) AS INTEGER) > 0
--DateDiffMonth
SELECT "p"."Id", "p"."Data"
FROM "Test" AS "p"
WHERE CAST((strftime('%s', "p"."Data") - strftime('%s', strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime'))) / (60 * 60 * 24 * 365/12) AS INTEGER) > 0
--DateDiffYear
SELECT "p"."Id", "p"."Data"
FROM "Test" AS "p"
WHERE (CAST(strftime('%Y', "p"."Data") AS INTEGER) - CAST(strftime('%Y', strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime')) AS INTEGER)) > 0
--DateDiffSecond
SELECT "p"."Id", "p"."Data"
FROM "Test" AS "p"
WHERE (CAST(strftime('%S', "p"."Data") AS INTEGER) - CAST(strftime('%S', strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime')) AS INTEGER)) > 0There was a problem hiding this comment.
There is still mismatch between client version and server output.
If you like then I can build on the top of your PR to implement SQLite translators.
There was a problem hiding this comment.
@smitpatel,
My intention was for everything to be perfect.
I did some basic tests and it works for me, I do not see inconsistency, could you guide me?
And yes, feel free to complement the PR!
There was a problem hiding this comment.
For example lets take dates
`> var date1 = new DateTime(2016, 2, 27);
var date2 = new DateTime(2016, 3, 1);`
The DateDiffMonth on client side is 12 * (date1.Year - date2.Year) + date1.Month - date2.Month Which would give us result of -1. There is difference of -1 month as we can see there.
When converting this using %s and dividing by no. of seconds in a month. we generate this SQL
sqlite> select (CAST (strftime("%s", "2016-02-27 00:00:00.0000000") AS REAL) - CAST (strftime("%s", "2016-03-01 00:00:00.0000000") AS REAL))/(60 * 60 * 24 * 365 / 12);
Which gives us answer as -0.0986301369863014
Both answers are far apart because in first case even though the dates are 3 days apart only month changes and our client method just consider months. Nothing else.
So if convert everything to seconds and start using it on server then we would be generate results which would change based on client/server eval. Since translation is something which we are introducing we should align the results to make it deterministic.
I believe the client implementation is taken from SqlMethods class from Linq2Sql so we would preserve it and write our translation accordingly.
So above would generate in sql following
select 12 * (CAST (strftime("%Y", "2016-02-27 00:00:00.0000000") AS INT) - CAST (strftime("%Y", "2016-03-01 00:00:00.0000000") AS INT)) + CAST (strftime("%m", "2016-02-27 00:00:00.0000000") AS INT) - CAST (strftime("%m", "2016-03-01 00:00:00.0000000") AS INT)
It is somewhat ugly but it aligns us with client.
If you look at the translation then it is basically putting pieces together what client would have done otherwise using MemberTranslator.
There was a problem hiding this comment.
I wanted to do this, but I thought it was strange.
More really that's the way!
@smitpatel thank you very much, I am adapting ...
|
@smitpatel, thanks for looking. |
|
ping: @smitpatel "ping"🙈 this will not work, when I pass an expression that needs an integer return on the client side. new SqlFunctionExpression(
"strftime",
typeof(string),
new Expression[] {
new SqlFragmentExpression("%d"),
new SqlFunctionExpression(
"strftime",
typeof(string),
new Expression[]{
new SqlFragmentExpression("%Y-%m-%d %H:%M:%S"),
/*date,*/
Expression.Add(
new SqlFunctionExpression(
"strftime",
typeof(string),
new []
{
new SqlFragmentExpression("%d")
/*date,*/
}),
new SqlFragmentExpression(" days"))
})
});Example: public virtual Expression Translate(MethodCallExpression methodCallExpression)
{
if (_methodInfoDatePartMapping.TryGetValue(methodCallExpression.Method, out var datePart))
{
var firstArgument = methodCallExpression.Arguments[0];
....
Expression.Add(
new SqlFunctionExpression(
"strftime",
typeof(string),
new []
{
new SqlFragmentExpression("%d")
firstArgument <<<<<<--------------
}),
new SqlFragmentExpression(" days"));
...
}
return null;
}Why is my variable firstArgument: That will cause an exception of the type: That's why I created the ExplicitConcatExpression class, to make CAST suitable for that! |
|
Can you post what is exactly client side linq query? That will give me idea of what is MethodCallExpression. |
|
if I do this: The MethodCallExpression method will automatically contain an expression whose return is integer. Therefore, it is not possible to concatenate with "days" (methodCallExpression.Arguments [0] || days). Do you understand what I want to explain? public virtual Expression Translate(MethodCallExpression methodCallExpression)
{
if (_methodInfoDatePartMapping.TryGetValue(methodCallExpression.Method, out var datePart))
{
var firstArgument = methodCallExpression.Arguments[0];
//hack - That's not what we will use !!!!
if (firstArgument.NodeType == ExpressionType.Convert)
{
// This is just to run with Expression.Add, and this works!
// Until I or you have a better idea.
firstArgument = new ExplicitCastExpression(firstArgument, typeof(string));
}
var expressionAdd = firstArgument.NodeType == ExpressionType.Extension
? Expression.Add(firstArgument, new SqlFragmentExpression($"' {datePart}'"), _concat)
: (Expression)new SqlFragmentExpression($"'{firstArgument} {datePart}'");
return new SqlFunctionExpression(
functionName: _sqliteFunctionDateFormat,
returnType: methodCallExpression.Type,
arguments: new[]
{
new SqlFragmentExpression(_sqliteFormatDate),
methodCallExpression.Object,
expressionAdd
});
}
return null;
} |
|
While so in the essence you would translate Essentially, we would explicit cast only when materializing value because we need to read the value as numeric type instead of string but for all internal operations are in string so we can unwrap explicit cast. translated sql for above would be |
|
@smitpatel SELECT "p"."Id", "p"."Date"
FROM "Test" AS "p"
WHERE CAST(strftime('%d', strftime('%Y-%m-%d %H:%M:%S', "p"."Date", strftime('%d', "p"."Date") || ' days')) AS INTEGER) > 1
SELECT "p"."Id", "p"."Date"
FROM "Test" AS "p"
WHERE CAST(strftime('%d', strftime('%Y-%m-%d %H:%M:%S', "p"."Date", '1 days')) AS INTEGER) > 1And for the month DataDiff! SELECT COUNT(*)
FROM "Orders" AS "c"
WHERE (12 * ((CAST(strftime('%Y', "c"."OrderDate") AS INTEGER) - CAST(strftime('%Y', strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime')) AS INTEGER)) + (CAST(strftime('%m', "c"."OrderDate") AS INTEGER) - CAST(strftime('%m', strftime('%Y-%m-%d %H:%M:%S', 'now', 'localtime')) AS INTEGER)))) = 0 |
|
@ralmsdeveloper - Can you rebase on latest dev and squash all the commits into one? I will update the SQLite implementation on top of this PR preserving your contribution. |
03ca5ad to
4238487
Compare
|
Ping: @smitpatel |
|
@ralmsdeveloper - Can you rebase this on latest dev and squash all commits into one? I will update SQLite implementation. |
|
Yes, I'll do it now! |
4238487 to
109ad94
Compare
* Suporte DateDiff para SQL Server * Suporte DateDiff para SQLite
* DateDiff Support for SQL Server * DateDiff support for SQLite
109ad94 to
f970bb7
Compare
* DateDiff Support for SQL Server * DateDiff support for SQLite
|
@ralmsdeveloper - Please do no work any longer in this PR. It would cause merge conflicts for me on rebase. I will fix if there are any tests issues. I am working on this right now. |
|
Closing this in favour of #10528 |
EF.Functions.DateDiff
SQL