Skip to content

Commit 54b4af5

Browse files
authored
Fix DateTime expressions for HasDefaultValue() and add support for fractions of a second (#85)
* Fix and improve test runs. * Fix DateTime expressions for HasDefaultValue() and add support for fractions of a second. Refactor test infrastructure.
1 parent c473442 commit 54b4af5

21 files changed

+484
-219
lines changed

azure-pipelines.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ jobs:
108108
- pwsh: |
109109
$env:EFCoreJet_DefaultConnection = '$(defaultConnection)'
110110
dotnet test .\test\EFCore.Jet.Tests -c $(buildConfiguration) --no-build --logger trx --verbosity detailed
111-
displayName: 'Run Tests: EFCore.Jet.FunctionalTests'
111+
displayName: 'Run Tests: EFCore.Jet.Tests'
112112
continueOnError: false
113113
- task: PublishTestResults@2
114114
displayName: Publish Test Results

src/EFCore.Jet.Data/JetDataReader.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,26 @@ public override string GetDataTypeName(int ordinal)
144144

145145
public override DateTime GetDateTime(int ordinal)
146146
{
147+
// Some/all ODBC/OLE DB methods don't support returning fractions of seconds.
148+
// Since DATETIME values are really just DOUBLE values internally in Jet, we explicitly convert those vales
149+
// to DOUBLE in the most outer SELECT projections as a workaround.
147150
var value = _wrappedDataReader.GetValue(ordinal);
148151
return JetConfiguration.UseDefaultValueOnDBNullConversionError &&
149152
Convert.IsDBNull(value)
150153
? default
151-
: (DateTime) value;
154+
: value is double doubleValue
155+
? new DateTime(JetConfiguration.TimeSpanOffset.Ticks + (long) (doubleValue * TimeSpan.TicksPerDay))
156+
: (DateTime) value;
152157
}
153158

154159
public virtual TimeSpan GetTimeSpan(int ordinal)
155-
=> GetDateTime(ordinal) - JetConfiguration.TimeSpanOffset;
160+
{
161+
var dateTime = GetDateTime(ordinal);
162+
return JetConfiguration.UseDefaultValueOnDBNullConversionError &&
163+
dateTime == default
164+
? default
165+
: dateTime - JetConfiguration.TimeSpanOffset;
166+
}
156167

157168
public virtual DateTimeOffset GetDateTimeOffset(int ordinal)
158169
=> GetDateTime(ordinal);

src/EFCore.Jet/Migrations/JetMigrationsSqlGenerator.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -772,19 +772,13 @@ protected override void DefaultValue(
772772
typeMapping = Dependencies.TypeMappingSource.GetMappingForValue(defaultValue);
773773
}
774774

775-
// Jet does not support defaults for hh:mm:ss in create table statement
776-
bool isDateTimeValue =
777-
defaultValue.GetType()
778-
.UnwrapNullableType() == typeof(DateTime) ||
779-
defaultValue.GetType()
780-
.UnwrapNullableType() == typeof(DateTimeOffset);
781-
775+
defaultValue = defaultValue.GetType().IsTimeRelatedType()
776+
? JetDateTimeTypeMapping.GenerateNonNullSqlLiteral(defaultValue, true)
777+
: typeMapping.GenerateSqlLiteral(defaultValue);
778+
782779
builder
783780
.Append(" DEFAULT ")
784-
.Append(
785-
isDateTimeValue
786-
? JetDateTimeTypeMapping.GenerateSqlLiteral(defaultValue, true)
787-
: typeMapping.GenerateSqlLiteral(defaultValue));
781+
.Append(defaultValue);
788782
}
789783
}
790784

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Linq;
3+
using System.Linq.Expressions;
4+
using Microsoft.EntityFrameworkCore.Query;
5+
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
6+
7+
namespace EntityFrameworkCore.Jet.Query.Internal
8+
{
9+
public class JetDateTimeExpressionVisitor : ExpressionVisitor
10+
{
11+
private readonly ISqlExpressionFactory _sqlExpressionFactory;
12+
13+
public JetDateTimeExpressionVisitor(ISqlExpressionFactory sqlExpressionFactory)
14+
{
15+
_sqlExpressionFactory = sqlExpressionFactory;
16+
}
17+
18+
protected override Expression VisitExtension(Expression extensionExpression)
19+
=> extensionExpression switch
20+
{
21+
SelectExpression selectExpression => VisitSelect(selectExpression),
22+
_ => base.VisitExtension(extensionExpression)
23+
};
24+
25+
protected virtual SelectExpression VisitSelect(SelectExpression selectExpression)
26+
{
27+
//
28+
// Most outer SELECT expressions will convert types that can contain a time related value to DOUBLE:
29+
//
30+
31+
var newProjections = selectExpression.Projection.Select(
32+
projection => projection.Expression.TypeMapping.ClrType.IsTimeRelatedType()
33+
? projection.Update(
34+
_sqlExpressionFactory.Convert(
35+
projection.Expression,
36+
typeof(double),
37+
_sqlExpressionFactory.FindMapping(typeof(double))))
38+
: projection)
39+
.ToList();
40+
41+
var expression = selectExpression.Update(
42+
newProjections,
43+
selectExpression.Tables.ToList(),
44+
selectExpression.Predicate,
45+
selectExpression.GroupBy.ToList(),
46+
selectExpression.Having,
47+
selectExpression.Orderings.ToList(),
48+
selectExpression.Limit,
49+
selectExpression.Offset,
50+
selectExpression.IsDistinct,
51+
selectExpression.Alias);
52+
53+
return expression;
54+
}
55+
}
56+
}

src/EFCore.Jet/Query/Internal/JetQueryTranslationPostprocessor.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ public JetQueryTranslationPostprocessor(
1616
}
1717

1818
public override Expression Process(Expression query)
19-
=> new SearchConditionConvertingExpressionVisitor(SqlExpressionFactory).Visit(base.Process(query));
19+
{
20+
query = base.Process(query);
21+
22+
query = new SearchConditionConvertingExpressionVisitor(SqlExpressionFactory).Visit(query);
23+
query = new JetDateTimeExpressionVisitor(SqlExpressionFactory).Visit(query);
24+
25+
return query;
26+
}
2027
}
2128
}

src/EFCore.Jet/Storage/Internal/JetDateTimeOffsetTypeMapping.cs

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@
77

88
namespace EntityFrameworkCore.Jet.Storage.Internal
99
{
10-
public class JetDateTimeOffsetTypeMapping : DateTimeOffsetTypeMapping
10+
public class JetDateTimeOffsetTypeMapping : JetDateTimeTypeMapping
1111
{
12-
private const string DateTimeOffsetFormatConst = @"{0:MM'/'dd'/'yyyy HH\:mm\:ss}";
13-
1412
public JetDateTimeOffsetTypeMapping(
1513
[NotNull] string storeType)
1614
: base(
1715
storeType,
18-
System.Data.DbType.DateTime) // delibrately use DbType.DateTime, because OleDb will throw a
19-
// "No mapping exists from DbType DateTimeOffset to a known OleDbType."
20-
// exception when using DbType.DateTimeOffset.
16+
System.Data.DbType.DateTime,
17+
typeof(DateTimeOffset)) // delibrately use DbType.DateTime, because OleDb will throw a
18+
// "No mapping exists from DbType DateTimeOffset to a known OleDbType."
19+
// exception when using DbType.DateTimeOffset.
2120
{
2221
}
2322

@@ -31,35 +30,19 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
3130

3231
protected override void ConfigureParameter(DbParameter parameter)
3332
{
34-
base.ConfigureParameter(parameter);
35-
36-
// Check: Is this really necessary for Jet?
37-
/*
38-
if (DbType == System.Data.DbType.Date ||
39-
DbType == System.Data.DbType.DateTime ||
40-
DbType == System.Data.DbType.DateTime2 ||
41-
DbType == System.Data.DbType.DateTimeOffset ||
42-
DbType == System.Data.DbType.Time)
43-
{
44-
((OleDbParameter) parameter).OleDbType = OleDbType.DBTimeStamp;
45-
}
46-
*/
47-
4833
// OLE DB can't handle the DateTimeOffset type.
4934
if (parameter.Value is DateTimeOffset dateTimeOffset)
5035
{
5136
parameter.Value = dateTimeOffset.UtcDateTime;
5237
}
53-
}
5438

55-
protected override string SqlLiteralFormatString => "#" + DateTimeOffsetFormatConst + "#";
39+
base.ConfigureParameter(parameter);
40+
}
5641

57-
public override string GenerateProviderValueSqlLiteral([CanBeNull] object value)
58-
=> value == null
59-
? "NULL"
60-
: GenerateNonNullSqlLiteral(
61-
value is DateTimeOffset dateTimeOffset
62-
? dateTimeOffset.UtcDateTime
63-
: value);
42+
protected override string GenerateNonNullSqlLiteral(object value)
43+
=> base.GenerateNonNullSqlLiteral(
44+
value is DateTimeOffset dateTimeOffset
45+
? dateTimeOffset.UtcDateTime
46+
: value);
6447
}
6548
}

src/EFCore.Jet/Storage/Internal/JetDateTimeTypeMapping.cs

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@
33
using System;
44
using System.Data;
55
using System.Data.Common;
6+
using System.Globalization;
7+
using System.Text;
8+
using EntityFrameworkCore.Jet.Data;
69
using JetBrains.Annotations;
710
using Microsoft.EntityFrameworkCore.Storage;
811

912
namespace EntityFrameworkCore.Jet.Storage.Internal
1013
{
11-
public class JetDateTimeTypeMapping : DateTimeTypeMapping
14+
public class JetDateTimeTypeMapping : RelationalTypeMapping
1215
{
13-
private const string DateTimeFormatConst = @"{0:MM'/'dd'/'yyyy HH\:mm\:ss}";
14-
private const string DateTimeShortFormatConst = "{0:MM'/'dd'/'yyyy}";
15-
16+
private const int MaxDateTimeDoublePrecision = 10;
17+
private static readonly JetDoubleTypeMapping _doubleTypeMapping = new JetDoubleTypeMapping("double");
18+
1619
public JetDateTimeTypeMapping(
1720
[NotNull] string storeType,
18-
DbType? dbType = null)
19-
: base(storeType, dbType)
21+
DbType? dbType = null,
22+
[CanBeNull] Type clrType = null)
23+
: base(storeType, clrType ?? typeof(DateTime), dbType ?? System.Data.DbType.DateTime)
2024
{
2125
}
2226

@@ -31,31 +35,75 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
3135
protected override void ConfigureParameter(DbParameter parameter)
3236
{
3337
base.ConfigureParameter(parameter);
38+
39+
if (parameter.Value is DateTime dateTime)
40+
{
41+
parameter.Value = GetDateTimeDoubleValue(dateTime);
42+
parameter.ResetDbType();
43+
}
44+
}
45+
46+
protected override string GenerateNonNullSqlLiteral(object value)
47+
=> GenerateNonNullSqlLiteral(value, false);
48+
49+
public static string GenerateNonNullSqlLiteral(object value, bool defaultClauseCompatible)
50+
{
51+
var dateTime = (DateTime) value;
3452

35-
// Check: Is this really necessary for Jet?
36-
/*
37-
if (DbType == System.Data.DbType.Date ||
38-
DbType == System.Data.DbType.DateTime ||
39-
DbType == System.Data.DbType.DateTime2 ||
40-
DbType == System.Data.DbType.DateTimeOffset ||
41-
DbType == System.Data.DbType.Time)
53+
dateTime = CheckDateTimeValue(dateTime);
54+
55+
if (!defaultClauseCompatible)
4256
{
43-
((OleDbParameter) parameter).OleDbType = OleDbType.DBTimeStamp;
57+
var literal = new StringBuilder()
58+
.AppendFormat(CultureInfo.InvariantCulture, "#{0:yyyy-MM-dd}", dateTime);
59+
60+
var time = dateTime.TimeOfDay;
61+
if (time != TimeSpan.Zero)
62+
{
63+
literal.AppendFormat(CultureInfo.InvariantCulture, @" {0:hh\:mm\:ss}", time);
64+
}
65+
66+
literal.Append("#");
67+
68+
if (time != TimeSpan.Zero)
69+
{
70+
var fractionsTicks = time.Ticks % TimeSpan.TicksPerSecond;
71+
if (fractionsTicks > 0)
72+
{
73+
var jetTimeDoubleFractions = Math.Round((double) fractionsTicks / TimeSpan.TicksPerDay, MaxDateTimeDoublePrecision);
74+
75+
literal
76+
.Insert(0, "(")
77+
.Append(" + ")
78+
.Append(_doubleTypeMapping.GenerateSqlLiteral(jetTimeDoubleFractions))
79+
.Append(")");
80+
}
81+
}
82+
83+
return literal.ToString();
4484
}
45-
*/
85+
86+
return _doubleTypeMapping.GenerateSqlLiteral(GetDateTimeDoubleValue(dateTime));
4687
}
4788

48-
protected override string SqlLiteralFormatString => "#" + DateTimeFormatConst + "#";
89+
private static double GetDateTimeDoubleValue(DateTime dateTime)
90+
=> Math.Round(
91+
(double) (CheckDateTimeValue(dateTime) - JetConfiguration.TimeSpanOffset).Ticks / TimeSpan.TicksPerDay,
92+
MaxDateTimeDoublePrecision);
4993

50-
public static string GenerateSqlLiteral([NotNull] object o, bool shortForm)
94+
private static DateTime CheckDateTimeValue(DateTime dateTime)
5195
{
52-
if (o == null)
53-
throw new ArgumentNullException(nameof(o));
96+
if (dateTime < JetConfiguration.TimeSpanOffset)
97+
{
98+
if (dateTime != default)
99+
{
100+
throw new InvalidOperationException($"The {nameof(DateTime)} value '{dateTime}' is smaller than the minimum supported value of '{JetConfiguration.TimeSpanOffset}'.");
101+
}
102+
103+
dateTime = JetConfiguration.TimeSpanOffset;
104+
}
54105

55-
return
56-
shortForm
57-
? string.Format("#" + DateTimeShortFormatConst + "#", o)
58-
: string.Format("#" + DateTimeFormatConst + "#", o);
106+
return dateTime;
59107
}
60108
}
61109
}
Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
22

33
using System;
4-
using System.Data.Common;
54
using EntityFrameworkCore.Jet.Data;
65
using JetBrains.Annotations;
76
using Microsoft.EntityFrameworkCore.Storage;
87

98
namespace EntityFrameworkCore.Jet.Storage.Internal
109
{
11-
public class JetTimeSpanTypeMapping : TimeSpanTypeMapping
10+
public class JetTimeSpanTypeMapping : JetDateTimeTypeMapping
1211
{
1312
public JetTimeSpanTypeMapping(
1413
[NotNull] string storeType)
15-
: base(storeType, System.Data.DbType.Time)
14+
: base(storeType, System.Data.DbType.Time, typeof(TimeSpan))
1615
{
1716
}
1817

@@ -23,27 +22,8 @@ protected JetTimeSpanTypeMapping(RelationalTypeMappingParameters parameters)
2322

2423
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
2524
=> new JetTimeSpanTypeMapping(parameters);
26-
27-
protected override void ConfigureParameter(DbParameter parameter)
28-
{
29-
base.ConfigureParameter(parameter);
30-
31-
// Check: Is this really necessary for Jet?
32-
/*
33-
if (DbType == System.Data.DbType.Date ||
34-
DbType == System.Data.DbType.DateTime ||
35-
DbType == System.Data.DbType.DateTime2 ||
36-
DbType == System.Data.DbType.DateTimeOffset ||
37-
DbType == System.Data.DbType.Time)
38-
{
39-
((OleDbParameter) parameter).OleDbType = OleDbType.DBTimeStamp;
40-
}
41-
*/
42-
}
43-
25+
4426
protected override string GenerateNonNullSqlLiteral(object value)
45-
{
46-
return $"{JetConfiguration.TimeSpanOffset + (TimeSpan) value:#MM'/'dd'/'yyyy hh\\:mm\\:ss#}";
47-
}
27+
=> base.GenerateNonNullSqlLiteral(JetConfiguration.TimeSpanOffset + (TimeSpan) value);
4828
}
4929
}

0 commit comments

Comments
 (0)