From 5013f47f2486577f0a348c3b64376e9ca702463d Mon Sep 17 00:00:00 2001 From: Eamon Hetherton Date: Fri, 19 Nov 2021 09:34:53 +1000 Subject: [PATCH] add tests and fix #26742 test and fix --- .../SqlServerDateTimeOffsetTypeMapping.cs | 3 +- .../Internal/SqlServerDateTimeTypeMapping.cs | 7 +- .../Internal/SqlServerTimeSpanTypeMapping.cs | 70 +++++++- .../Internal/SqlServerTypeMappingSource.cs | 3 +- .../Storage/RelationalTypeMappingTest.cs | 2 +- .../BuiltInDataTypesSqlServerTest.cs | 45 +++-- .../Migrations/MigrationsSqlServerTest.cs | 86 ++++++++- .../NorthwindDbFunctionsQuerySqlServerTest.cs | 2 +- .../Query/QueryBugsTest.cs | 127 ++++++++++++- .../Storage/SqlServerTypeMappingSourceTest.cs | 167 ++++++++++++++++++ .../Storage/SqlServerTypeMappingTest.cs | 67 ++++++- 11 files changed, 549 insertions(+), 30 deletions(-) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeOffsetTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeOffsetTypeMapping.cs index 79d05970d37..7a182f5b386 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeOffsetTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeOffsetTypeMapping.cs @@ -110,7 +110,8 @@ protected override void ConfigureParameter(DbParameter parameter) if (Precision.HasValue) { - parameter.Precision = unchecked((byte)Precision.Value); + // Workaround for inconsistant definition of precision/scale between EF and SQLClient for VarTime types + parameter.Scale = unchecked((byte)Precision.Value); } } } diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs index 758a41c7821..b5d819660ce 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs @@ -25,7 +25,7 @@ public class SqlServerDateTimeTypeMapping : DateTimeTypeMapping // so the order of the entries in this array is important private readonly string[] _dateTime2Formats = { - "'{0:yyyy-MM-ddTHH:mm:ss}'", + "'{0:yyyy-MM-ddTHH:mm:ssK}'", "'{0:yyyy-MM-ddTHH:mm:ss.fK}'", "'{0:yyyy-MM-ddTHH:mm:ss.ffK}'", "'{0:yyyy-MM-ddTHH:mm:ss.fffK}'", @@ -89,7 +89,8 @@ protected override void ConfigureParameter(DbParameter parameter) if (Precision.HasValue) { - parameter.Precision = unchecked((byte)Precision.Value); + // Workaround for inconsistant definition of precision/scale between EF and SQLClient for VarTime types + parameter.Scale = unchecked((byte)Precision.Value); } } @@ -133,6 +134,6 @@ protected override string SqlLiteralFormatString return _dateTime2Formats[7]; } } - } + } } } diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs index 26435265aad..7981d161452 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs @@ -1,8 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Data; using System.Data.Common; +using System.Globalization; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.Storage; @@ -16,14 +18,36 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal /// public class SqlServerTimeSpanTypeMapping : TimeSpanTypeMapping { + // Note: this array will be accessed using the precision as an index + // so the order of the entries in this array is important + private readonly string[] _timeFormats = + { + @"'{0:hh\:mm\:ss}'", + @"'{0:hh\:mm\:ss\.F}'", + @"'{0:hh\:mm\:ss\.FF}'", + @"'{0:hh\:mm\:ss\.FFF}'", + @"'{0:hh\:mm\:ss\.FFFF}'", + @"'{0:hh\:mm\:ss\.FFFFF}'", + @"'{0:hh\:mm\:ss\.FFFFFF}'", + @"'{0:hh\:mm\:ss\.FFFFFFF}'" + }; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public SqlServerTimeSpanTypeMapping(string storeType, DbType? dbType = System.Data.DbType.Time) - : base(storeType, dbType) + public SqlServerTimeSpanTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Time, + StoreTypePostfix storeTypePostfix = StoreTypePostfix.Precision) + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(TimeSpan)), + storeType, + storeTypePostfix, + dbType)) { } @@ -61,6 +85,48 @@ protected override void ConfigureParameter(DbParameter parameter) { ((SqlParameter)parameter).SqlDbType = SqlDbType.Time; } + if (Precision.HasValue) + { + parameter.Scale = unchecked((byte)Precision.Value); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override string SqlLiteralFormatString + { + get + { + if (Precision.HasValue) + { + var precision = Precision.Value; + if (precision <= 7 + && precision >= 0) + { + return _timeFormats[precision]; + } + } + + return _timeFormats[7]; + } + } + + /// + /// Generates the SQL representation of a literal value without conversion. + /// + /// The literal value. + /// + /// The generated string. + /// + protected override string GenerateNonNullSqlLiteral(object value) + { + return value is TimeSpan timeSpan && timeSpan.Milliseconds == 0 + ? string.Format(CultureInfo.InvariantCulture, _timeFormats[0], value) //handle trailing decimal seperator when no fractional seconds + : string.Format(CultureInfo.InvariantCulture, SqlLiteralFormatString, value); } } } diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index d1b2cf75289..215ff5d9d0f 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs @@ -352,7 +352,8 @@ public SqlServerTypeMappingSource( "datetime2", "datetimeoffset", "double precision", - "float" + "float", + "time" }; /// diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs index 88275d07c80..9caf50fc8ca 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMappingTest.cs @@ -529,7 +529,7 @@ public virtual void String_literal_generated_correctly() [ConditionalFact] public virtual void Timespan_literal_generated_correctly() { - Test_GenerateSqlLiteral_helper(new TimeSpanTypeMapping("time"), new TimeSpan(7, 14, 30), "'07:14:30'"); + Test_GenerateSqlLiteral_helper(new TimeSpanTypeMapping("time"), new TimeSpan(0, 7, 14, 30, 123), "'07:14:30.1230000'"); } [ConditionalFact] diff --git a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs index 569c403e591..f4a999e88fe 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs @@ -1584,15 +1584,16 @@ public virtual void Can_insert_and_read_back_all_mapped_data_types_with_scale() var parameters = DumpParameters(); Assert.Equal( @"@p0='77' -@p1='2017-01-02T12:11:12.3210000' (Precision = 3) -@p2='2016-01-02T11:11:12.7650000+00:00' (Precision = 3) +@p1='2017-01-02T12:11:12.3210000' (Scale = 3) +@p2='2016-01-02T11:11:12.7650000+00:00' (Scale = 3) @p3='102' (Precision = 3) @p4='101' (Precision = 3) @p5='103' (Precision = 3) @p6='85.55000305175781' (Size = 25) @p7='85.5' (Size = 3) @p8='83.33000183105469' (Size = 25) -@p9='83.3' (Size = 3)", +@p9='83.3' (Size = 3) +@p10='12:34:56.7890123' (Scale = 3)", parameters, ignoreLineEndingDifferences: true); @@ -1612,6 +1613,7 @@ private static void AssertMappedScaledDataTypes(MappedScaledDataTypes entity, in Assert.Equal( new DateTimeOffset(new DateTime(2016, 1, 2, 11, 11, 12, 765), TimeSpan.Zero), entity.DateTimeOffsetAsDatetimeoffset3); Assert.Equal(new DateTime(2017, 1, 2, 12, 11, 12, 321), entity.DateTimeAsDatetime23); + Assert.Equal(TimeSpan.Parse("12:34:56.789", System.Globalization.CultureInfo.InvariantCulture), entity.TimeSpanAsTime3); Assert.Equal(101m, entity.DecimalAsDecimal3); Assert.Equal(102m, entity.DecimalAsDec3); Assert.Equal(103m, entity.DecimalAsNumeric3); @@ -1629,7 +1631,8 @@ private static MappedScaledDataTypes CreateMappedScaledDataTypes(int id) DateTimeAsDatetime23 = new DateTime(2017, 1, 2, 12, 11, 12, 321), DecimalAsDecimal3 = 101m, DecimalAsDec3 = 102m, - DecimalAsNumeric3 = 103m + DecimalAsNumeric3 = 103m, + TimeSpanAsTime3 = TimeSpan.Parse("12:34:56.7890123", System.Globalization.CultureInfo.InvariantCulture) }; [ConditionalFact] @@ -1645,15 +1648,16 @@ public virtual void Can_insert_and_read_back_all_mapped_data_types_with_scale_se var parameters = DumpParameters(); Assert.Equal( @"@p0='77' -@p1='2017-01-02T12:11:12.3210000' (Precision = 3) -@p2='2016-01-02T11:11:12.7650000+00:00' (Precision = 3) +@p1='2017-01-02T12:11:12.3210000' (Scale = 3) +@p2='2016-01-02T11:11:12.7650000+00:00' (Scale = 3) @p3='102' (Precision = 3) @p4='101' (Precision = 3) @p5='103' (Precision = 3) @p6='85.55000305175781' (Size = 25) @p7='85.5' (Size = 3) @p8='83.33000183105469' (Size = 25) -@p9='83.3' (Size = 3)", +@p9='83.3' (Size = 3) +@p10='12:34:56.7890000' (Scale = 3)", parameters, ignoreLineEndingDifferences: true); @@ -1676,6 +1680,7 @@ private static void AssertMappedScaledSeparatelyDataTypes(MappedScaledSeparately Assert.Equal(101m, entity.DecimalAsDecimal3); Assert.Equal(102m, entity.DecimalAsDec3); Assert.Equal(103m, entity.DecimalAsNumeric3); + Assert.Equal(TimeSpan.Parse("12:34:56.789", System.Globalization.CultureInfo.InvariantCulture), entity.TimeSpanAsTime3); } private static MappedScaledSeparatelyDataTypes CreateMappedScaledSeparatelyDataTypes(int id) @@ -1690,7 +1695,8 @@ private static MappedScaledSeparatelyDataTypes CreateMappedScaledSeparatelyDataT DateTimeAsDatetime23 = new DateTime(2017, 1, 2, 12, 11, 12, 321), DecimalAsDecimal3 = 101m, DecimalAsDec3 = 102m, - DecimalAsNumeric3 = 103m + DecimalAsNumeric3 = 103m, + TimeSpanAsTime3 = TimeSpan.Parse("12:34:56.789", System.Globalization.CultureInfo.InvariantCulture) }; [ConditionalFact] @@ -2492,8 +2498,8 @@ public virtual void Can_insert_and_read_back_all_mapped_data_types_with_scale_wi var parameters = DumpParameters(); Assert.Equal( - @"@p0='2017-01-02T12:11:12.1230000' (Precision = 3) -@p1='2016-01-02T11:11:12.5670000+00:00' (Precision = 3) + @"@p0='2017-01-02T12:11:12.1230000' (Scale = 3) +@p1='2016-01-02T11:11:12.5670000+00:00' (Scale = 3) @p2='102' (Precision = 3) @p3='101' (Precision = 3) @p4='103' (Precision = 3) @@ -2501,7 +2507,8 @@ public virtual void Can_insert_and_read_back_all_mapped_data_types_with_scale_wi @p6='85.5' (Size = 3) @p7='83.33000183105469' (Size = 25) @p8='83.3' (Size = 3) -@p9='77'", +@p9='77' +@p10='12:34:56.7890123' (Scale = 3)", parameters, ignoreLineEndingDifferences: true); @@ -2524,6 +2531,7 @@ private static void AssertMappedScaledDataTypesWithIdentity(MappedScaledDataType Assert.Equal(101m, entity.DecimalAsDecimal3); Assert.Equal(102m, entity.DecimalAsDec3); Assert.Equal(103m, entity.DecimalAsNumeric3); + Assert.Equal(TimeSpan.Parse("12:34:56.789", System.Globalization.CultureInfo.InvariantCulture), entity.TimeSpanAsTime3); } private static MappedScaledDataTypesWithIdentity CreateMappedScaledDataTypesWithIdentity(int id) @@ -2538,7 +2546,8 @@ private static MappedScaledDataTypesWithIdentity CreateMappedScaledDataTypesWith DateTimeAsDatetime23 = new DateTime(2017, 1, 2, 12, 11, 12, 123), DecimalAsDecimal3 = 101m, DecimalAsDec3 = 102m, - DecimalAsNumeric3 = 103m + DecimalAsNumeric3 = 103m, + TimeSpanAsTime3 = TimeSpan.Parse("12:34:56.7890123", System.Globalization.CultureInfo.InvariantCulture) }; [ConditionalFact] @@ -3242,6 +3251,7 @@ public virtual void Columns_have_expected_data_types() MappedScaledDataTypes.FloatAsFloat25 ---> [float] [Precision = 53] MappedScaledDataTypes.FloatAsFloat3 ---> [real] [Precision = 24] MappedScaledDataTypes.Id ---> [int] [Precision = 10 Scale = 0] +MappedScaledDataTypes.TimeSpanAsTime3 ---> [time] [Precision = 3] MappedScaledDataTypesWithIdentity.DateTimeAsDatetime23 ---> [datetime2] [Precision = 3] MappedScaledDataTypesWithIdentity.DateTimeOffsetAsDatetimeoffset3 ---> [datetimeoffset] [Precision = 3] MappedScaledDataTypesWithIdentity.DecimalAsDec3 ---> [decimal] [Precision = 3 Scale = 0] @@ -3253,6 +3263,7 @@ public virtual void Columns_have_expected_data_types() MappedScaledDataTypesWithIdentity.FloatAsFloat3 ---> [real] [Precision = 24] MappedScaledDataTypesWithIdentity.Id ---> [int] [Precision = 10 Scale = 0] MappedScaledDataTypesWithIdentity.Int ---> [int] [Precision = 10 Scale = 0] +MappedScaledDataTypesWithIdentity.TimeSpanAsTime3 ---> [time] [Precision = 3] MappedScaledSeparatelyDataTypes.DateTimeAsDatetime23 ---> [datetime2] [Precision = 3] MappedScaledSeparatelyDataTypes.DateTimeOffsetAsDatetimeoffset3 ---> [datetimeoffset] [Precision = 3] MappedScaledSeparatelyDataTypes.DecimalAsDec3 ---> [decimal] [Precision = 3 Scale = 0] @@ -3263,6 +3274,7 @@ public virtual void Columns_have_expected_data_types() MappedScaledSeparatelyDataTypes.FloatAsFloat25 ---> [float] [Precision = 53] MappedScaledSeparatelyDataTypes.FloatAsFloat3 ---> [real] [Precision = 24] MappedScaledSeparatelyDataTypes.Id ---> [int] [Precision = 10 Scale = 0] +MappedScaledSeparatelyDataTypes.TimeSpanAsTime3 ---> [time] [Precision = 3] MappedSizedDataTypes.BytesAsBinary3 ---> [nullable binary] [MaxLength = 3] MappedSizedDataTypes.BytesAsBinaryVarying3 ---> [nullable varbinary] [MaxLength = 3] MappedSizedDataTypes.BytesAsVarbinary3 ---> [nullable varbinary] [MaxLength = 3] @@ -4137,6 +4149,9 @@ protected class MappedScaledDataTypes [Column(TypeName = "numeric(3)")] public decimal DecimalAsNumeric3 { get; set; } + + [Column(TypeName = "time(3)")] + public TimeSpan TimeSpanAsTime3 { get; set; } } protected class MappedScaledSeparatelyDataTypes @@ -4169,6 +4184,9 @@ protected class MappedScaledSeparatelyDataTypes [Column(TypeName = "numeric")] public decimal DecimalAsNumeric3 { get; set; } + + [Column(TypeName = "time(3)")] + public TimeSpan TimeSpanAsTime3 { get; set; } } protected class DoubleDataTypes @@ -4621,6 +4639,9 @@ protected class MappedScaledDataTypesWithIdentity [Column(TypeName = "numeric(3)")] public decimal DecimalAsNumeric3 { get; set; } + + [Column(TypeName = "time(3)")] + public TimeSpan TimeSpanAsTime3 { get; set; } } protected class MappedPrecisionAndScaledDataTypesWithIdentity diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index f5064bd8ea9..77269e59d5c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -312,8 +312,90 @@ public override async Task Add_column_with_defaultValue_datetime() { await base.Add_column_with_defaultValue_datetime(); - AssertSql( - @"ALTER TABLE [People] ADD [Birthday] datetime2 NOT NULL DEFAULT '2015-04-12T17:05:00.0000000';"); + AssertSql(@"ALTER TABLE [People] ADD [Birthday] datetime2 NOT NULL DEFAULT '2015-04-12T17:05:00.0000000';"); + } + + [ConditionalTheory] + [InlineData(0, "", 1234567)] + [InlineData(1, ".1", 1234567)] + [InlineData(2, ".12", 1234567)] + [InlineData(3, ".123", 1234567)] + [InlineData(4, ".1234", 1234567)] + [InlineData(5, ".12345", 1234567)] + [InlineData(6, ".123456", 1234567)] + [InlineData(7, ".1234567", 1234567)] + [InlineData(7, ".1200000", 1200000)] //should this really output trailing zeros? + public async Task Add_column_with_defaultValue_datetime_with_explicit_precision(int precision, string fractionalSeconds, int ticksToAdd) + { + await Test( + builder => builder.Entity("People").Property("Id"), + builder => { }, + builder => builder.Entity("People").Property("Birthday").HasPrecision(precision) + .HasDefaultValue(new DateTime(2015, 4, 12, 17, 5, 0).AddTicks(ticksToAdd)), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal(2, table.Columns.Count); + var birthdayColumn = Assert.Single(table.Columns, c => c.Name == "Birthday"); + Assert.False(birthdayColumn.IsNullable); + }); + + AssertSql($@"ALTER TABLE [People] ADD [Birthday] datetime2({precision}) NOT NULL DEFAULT '2015-04-12T17:05:00{fractionalSeconds}';"); + } + [ConditionalTheory] + [InlineData(0, "", 1234567)] + [InlineData(1, ".1", 1234567)] + [InlineData(2, ".12", 1234567)] + [InlineData(3, ".123", 1234567)] + [InlineData(4, ".1234", 1234567)] + [InlineData(5, ".12345", 1234567)] + [InlineData(6, ".123456", 1234567)] + [InlineData(7, ".1234567", 1234567)] + [InlineData(7, ".1200000", 1200000)] //should this really output trailing zeros? + public async Task Add_column_with_defaultValue_datetimeoffset_with_explicit_precision(int precision, string fractionalSeconds, int ticksToAdd) + { + await Test( + builder => builder.Entity("People").Property("Id"), + builder => { }, + builder => builder.Entity("People").Property("Birthday").HasPrecision(precision) + .HasDefaultValue(new DateTimeOffset(new DateTime(2015, 4, 12, 17, 5, 0).AddTicks(ticksToAdd), TimeSpan.FromHours(10))), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal(2, table.Columns.Count); + var birthdayColumn = Assert.Single(table.Columns, c => c.Name == "Birthday"); + Assert.False(birthdayColumn.IsNullable); + }); + + AssertSql($@"ALTER TABLE [People] ADD [Birthday] datetimeoffset({precision}) NOT NULL DEFAULT '2015-04-12T17:05:00{fractionalSeconds}+10:00';"); + } + + [ConditionalTheory] + [InlineData(0, "", 1234567)] + [InlineData(1, ".1", 1234567)] + [InlineData(2, ".12", 1234567)] + [InlineData(3, ".123", 1234567)] + [InlineData(4, ".1234", 1234567)] + [InlineData(5, ".12345", 1234567)] + [InlineData(6, ".123456", 1234567)] + [InlineData(7, ".1234567", 1234567)] + [InlineData(7, ".12", 1200000)] + public async Task Add_column_with_defaultValue_time_with_explicit_precision(int precision, string fractionalSeconds, int ticksToAdd) + { + await Test( + builder => builder.Entity("People").Property("Id"), + builder => { }, + builder => builder.Entity("People").Property("Age").HasPrecision(precision) + .HasDefaultValue(TimeSpan.Parse("12:34:56", System.Globalization.CultureInfo.InvariantCulture).Add(TimeSpan.FromTicks(ticksToAdd))), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal(2, table.Columns.Count); + var birthdayColumn = Assert.Single(table.Columns, c => c.Name == "Age"); + Assert.False(birthdayColumn.IsNullable); + }); + + AssertSql($@"ALTER TABLE [People] ADD [Age] time({precision}) NOT NULL DEFAULT '12:34:56{fractionalSeconds}';"); } [ConditionalFact] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs index 6864d304a54..ce9e412a96e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs @@ -1137,7 +1137,7 @@ await AssertCount( ss => ss.Set(), ss => ss.Set(), c => new TimeSpan(23, 59, 0) > EF.Functions.TimeFromParts(23, 59, 59, c.OrderID % 60, 2), - c => new TimeSpan(23, 59, 0) > new TimeSpan(23, 59, 59, c.OrderID % 60, 2)); + c => new TimeSpan(23, 59, 0) > new TimeSpan(23, 59, 59, c.OrderID % 60)); AssertSql( @"SELECT COUNT(*) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 03369fbe834..78845a6a700 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -7041,7 +7041,7 @@ public enum BusinessType18346 public virtual async Task Thread_safety_in_relational_command_cache() { var contextFactory = await InitializeAsync( - onConfiguring: options =>((IDbContextOptionsBuilderInfrastructure)options).AddOrUpdateExtension( + onConfiguring: options => ((IDbContextOptionsBuilderInfrastructure)options).AddOrUpdateExtension( options.Options.FindExtension() .WithConnection(null) .WithConnectionString(SqlServerTestStore.CreateConnectionString(StoreName)))); @@ -10345,6 +10345,131 @@ public class CollectionViewModel25225 #endregion + #region Issue26742 + [ConditionalTheory] + [InlineData(null, "")] + //[InlineData(0, " (Scale = 0)")] //https://github.com/dotnet/SqlClient/issues/1380 cause this test to fail, not EF + [InlineData(1, " (Scale = 1)")] + [InlineData(2, " (Scale = 2)")] + [InlineData(3, " (Scale = 3)")] + [InlineData(4, " (Scale = 4)")] + [InlineData(5, " (Scale = 5)")] + [InlineData(6, " (Scale = 6)")] + [InlineData(7, " (Scale = 7)")] + public virtual async Task Query_generates_correct_datetime2_parameter_definition(int? fractionalSeconds, string postfix) + { + var contextFactory = await InitializeAsync(onModelCreating: modelBuilder => + { + if (fractionalSeconds.HasValue) + { + modelBuilder.Entity().Property(p => p.DateTime).HasPrecision(fractionalSeconds.Value); + } + }); + + var parameter = new DateTime(2021, 11, 12, 13, 14, 15).AddTicks(1234567); + + using (var context = contextFactory.CreateContext()) + { + _ = context.Entities.Where(x => x.DateTime == parameter).Select(e => e.DateTime).FirstOrDefault(); + + AssertSql( + $@"@__parameter_0='2021-11-12T13:14:15.1234567'{postfix} + +SELECT TOP(1) [e].[DateTime] +FROM [Entities] AS [e] +WHERE [e].[DateTime] = @__parameter_0"); + } + } + + [ConditionalTheory] + [InlineData(null, "")] + //[InlineData(0, " (Scale = 0)")] //https://github.com/dotnet/SqlClient/issues/1380 cause this test to fail, not EF + [InlineData(1, " (Scale = 1)")] + [InlineData(2, " (Scale = 2)")] + [InlineData(3, " (Scale = 3)")] + [InlineData(4, " (Scale = 4)")] + [InlineData(5, " (Scale = 5)")] + [InlineData(6, " (Scale = 6)")] + [InlineData(7, " (Scale = 7)")] + public virtual async Task Query_generates_correct_datetimeoffset_parameter_definition(int? fractionalSeconds, string postfix) + { + var contextFactory = await InitializeAsync(onModelCreating: modelBuilder => + { + if (fractionalSeconds.HasValue) + { + modelBuilder.Entity().Property(p => p.DateTimeOffset).HasPrecision(fractionalSeconds.Value); + } + }); + + var parameter = new DateTimeOffset(new DateTime(2021, 11, 12, 13, 14, 15).AddTicks(1234567), TimeSpan.FromHours(10)); + + using (var context = contextFactory.CreateContext()) + { + _ = context.Entities.Where(x => x.DateTimeOffset == parameter).Select(e => e.DateTimeOffset).FirstOrDefault(); + + AssertSql( + $@"@__parameter_0='2021-11-12T13:14:15.1234567+10:00'{postfix} + +SELECT TOP(1) [e].[DateTimeOffset] +FROM [Entities] AS [e] +WHERE [e].[DateTimeOffset] = @__parameter_0"); + } + } + + [ConditionalTheory] + [InlineData(null, "")] + //[InlineData(0, " (Scale = 0)")] //https://github.com/dotnet/SqlClient/issues/1380 cause this test to fail, not EF + [InlineData(1, " (Scale = 1)")] + [InlineData(2, " (Scale = 2)")] + [InlineData(3, " (Scale = 3)")] + [InlineData(4, " (Scale = 4)")] + [InlineData(5, " (Scale = 5)")] + [InlineData(6, " (Scale = 6)")] + [InlineData(7, " (Scale = 7)")] + public virtual async Task Query_generates_correct_timespan_parameter_definition(int? fractionalSeconds, string postfix) + { + var contextFactory = await InitializeAsync(onModelCreating: modelBuilder => + { + if (fractionalSeconds.HasValue) + { + modelBuilder.Entity().Property(p => p.TimeSpan).HasPrecision(fractionalSeconds.Value); + } + }); + + var parameter = TimeSpan.Parse("12:34:56.7890123", System.Globalization.CultureInfo.InvariantCulture); + + using (var context = contextFactory.CreateContext()) + { + _ = context.Entities.Where(x => x.TimeSpan == parameter).Select(e => e.TimeSpan).FirstOrDefault(); + + AssertSql( + $@"@__parameter_0='12:34:56.7890123'{postfix} + +SELECT TOP(1) [e].[TimeSpan] +FROM [Entities] AS [e] +WHERE [e].[TimeSpan] = @__parameter_0"); + } + } + protected class MyContext_26742 : DbContext + { + public DbSet Entities { get; set; } + + public MyContext_26742(DbContextOptions options) + : base(options) + { + } + + public class Entity + { + public int Id { get; set; } + public TimeSpan TimeSpan { get; set; } + public DateTime DateTime { get; set; } + public DateTimeOffset DateTimeOffset { get; set; } + } + } + + #endregion + protected override string StoreName => "QueryBugsTest"; protected TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs index b2d16b6ce62..edeac13c094 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs @@ -1073,6 +1073,173 @@ public void Can_map_string_base_type_name_and_size(string typeName) Assert.Equal(typeName + "(2018)", mapping.StoreType); } + [ConditionalTheory] + [InlineData("datetime2(0)", 0)] + [InlineData("datetime2(1)", 1)] + [InlineData("datetime2(2)", 2)] + [InlineData("datetime2(3)", 3)] + [InlineData("datetime2(4)", 4)] + [InlineData("datetime2(5)", 5)] + [InlineData("datetime2(6)", 6)] + [InlineData("datetime2(7)", 7)] + [InlineData("datetime2", null)] + public void Can_map_datetime_base_type_columnType_with_precision(string typeName, int? precision) + { + var builder = CreateModelBuilder(); + + var property = builder.Entity() + .Property(e => e.DateTimeWithPrecision) + .HasColumnType(typeName) + .Metadata; + + var mapping = CreateRelationalTypeMappingSource().FindMapping((IProperty)property); + + Assert.Same(typeof(DateTime), mapping.ClrType); + Assert.Equal(precision, mapping.Precision); + Assert.Null(mapping.Scale); + Assert.Equal(typeName, mapping.StoreType, true); + } + + [ConditionalTheory] + [InlineData("datetime2(0)", 0)] + [InlineData("datetime2(1)", 1)] + [InlineData("datetime2(2)", 2)] + [InlineData("datetime2(3)", 3)] + [InlineData("datetime2(4)", 4)] + [InlineData("datetime2(5)", 5)] + [InlineData("datetime2(6)", 6)] + [InlineData("datetime2(7)", 7)] + public void Can_map_datetime_base_type_precision(string typeName, int precision) + { + var builder = CreateModelBuilder(); + + var property = builder.Entity() + .Property(e => e.DateTimeWithPrecision) + .HasPrecision(precision) + .Metadata; + + var mapping = CreateRelationalTypeMappingSource().FindMapping((IProperty)property); + + Assert.Same(typeof(DateTime), mapping.ClrType); + Assert.Equal(precision, mapping.Precision); + Assert.Null(mapping.Scale); + Assert.Equal(typeName, mapping.StoreType, true); + } + + [ConditionalTheory] + [InlineData("datetimeoffset(0)", 0)] + [InlineData("datetimeoffset(1)", 1)] + [InlineData("datetimeoffset(2)", 2)] + [InlineData("datetimeoffset(3)", 3)] + [InlineData("datetimeoffset(4)", 4)] + [InlineData("datetimeoffset(5)", 5)] + [InlineData("datetimeoffset(6)", 6)] + [InlineData("datetimeoffset(7)", 7)] + [InlineData("datetimeoffset", null)] + public void Can_map_datetimeoffset_base_type_columnType_with_precision(string typeName, int? precision) + { + var builder = CreateModelBuilder(); + + var property = builder.Entity() + .Property(e => e.DateTimeOffsetWithPrecision) + .HasColumnType(typeName) + .Metadata; + + var mapping = CreateRelationalTypeMappingSource().FindMapping((IProperty)property); + + Assert.Same(typeof(DateTimeOffset), mapping.ClrType); + Assert.Equal(precision, mapping.Precision); + Assert.Null(mapping.Scale); + Assert.Equal(typeName, mapping.StoreType, true); + } + + [ConditionalTheory] + [InlineData("datetimeoffset(0)", 0)] + [InlineData("datetimeoffset(1)", 1)] + [InlineData("datetimeoffset(2)", 2)] + [InlineData("datetimeoffset(3)", 3)] + [InlineData("datetimeoffset(4)", 4)] + [InlineData("datetimeoffset(5)", 5)] + [InlineData("datetimeoffset(6)", 6)] + [InlineData("datetimeoffset(7)", 7)] + public void Can_map_datetimeoffset_base_type_precision(string typeName, int precision) + { + var builder = CreateModelBuilder(); + + var property = builder.Entity() + .Property(e => e.DateTimeOffsetWithPrecision) + .HasPrecision(precision) + .Metadata; + + var mapping = CreateRelationalTypeMappingSource().FindMapping((IProperty)property); + + Assert.Same(typeof(DateTimeOffset), mapping.ClrType); + Assert.Equal(precision, mapping.Precision); + Assert.Null(mapping.Scale); + Assert.Equal(typeName, mapping.StoreType, true); + } + + [ConditionalTheory] + [InlineData("time(0)", 0)] + [InlineData("time(1)", 1)] + [InlineData("time(2)", 2)] + [InlineData("time(3)", 3)] + [InlineData("time(4)", 4)] + [InlineData("time(5)", 5)] + [InlineData("time(6)", 6)] + [InlineData("time(7)", 7)] + [InlineData("time", null)] + public void Can_map_time_base_type_columnType_with_precision(string typeName, int? precision) + { + var builder = CreateModelBuilder(); + + var property = builder.Entity() + .Property(e => e.TimeSpanWithPrecision) + .HasColumnType(typeName) + .Metadata; + + var mapping = CreateRelationalTypeMappingSource().FindMapping((IProperty)property); + + Assert.Same(typeof(TimeSpan), mapping.ClrType); + Assert.Equal(precision, mapping.Precision); + Assert.Null(mapping.Scale); + Assert.Equal(typeName, mapping.StoreType, true); + } + + [ConditionalTheory] + [InlineData("time(0)", 0)] + [InlineData("time(1)", 1)] + [InlineData("time(2)", 2)] + [InlineData("time(3)", 3)] + [InlineData("time(4)", 4)] + [InlineData("time(5)", 5)] + [InlineData("time(6)", 6)] + [InlineData("time(7)", 7)] + public void Can_map_time_base_type_precision(string typeName, int precision) + { + var builder = CreateModelBuilder(); + + var property = builder.Entity() + .Property(e => e.TimeSpanWithPrecision) + .HasPrecision(precision) + .Metadata; + + var mapping = CreateRelationalTypeMappingSource().FindMapping((IProperty)property); + + Assert.Same(typeof(TimeSpan), mapping.ClrType); + Assert.Equal(precision, mapping.Precision); + Assert.Null(mapping.Scale); + Assert.Equal(typeName, mapping.StoreType, true); + } + private class VarTimeEntity + { + public int Id { get; set; } + public DateTime DateTimeWithPrecision { get; set; } + public DateTimeOffset DateTimeOffsetWithPrecision { get; set; } + public TimeSpan TimeSpanWithPrecision { get; set; } + } + + [ConditionalTheory] [InlineData("binary varying")] [InlineData("binary")] diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs index a1aee82b7bd..00b747f889d 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs @@ -203,16 +203,35 @@ public override void DateTimeOffset_literal_generated_correctly() { Test_GenerateSqlLiteral_helper( GetMapping(typeof(DateTimeOffset)), - new DateTimeOffset(2015, 3, 12, 13, 36, 37, 371, new TimeSpan(-7, 0, 0)), - "'2015-03-12T13:36:37.3710000-07:00'"); + new DateTimeOffset(2015, 3, 12, 13, 36, 37, new TimeSpan(-7, 0, 0)).AddTicks(1234567), + "'2015-03-12T13:36:37.1234567-07:00'"); + + Test_GenerateSqlLiteral_helper( + GetMapping(typeof(DateTimeOffset)), + new DateTimeOffset(2015, 3, 12, 13, 36, 37, new TimeSpan(-7, 0, 0)).AddTicks(1230000), + "'2015-03-12T13:36:37.1230000-07:00'"); //should this really output trailing zeros? + } + + [ConditionalFact] + public override void Timespan_literal_generated_correctly() + { + Test_GenerateSqlLiteral_helper( + GetMapping(typeof(TimeSpan)), + new TimeSpan(7, 14, 30), + "'07:14:30'"); + + Test_GenerateSqlLiteral_helper( + GetMapping(typeof(TimeSpan)), + new TimeSpan(0, 7, 14, 30, 120), + "'07:14:30.12'"); } public override void DateTime_literal_generated_correctly() { Test_GenerateSqlLiteral_helper( GetMapping(typeof(DateTime)), - new DateTime(2015, 3, 12, 13, 36, 37, 371, DateTimeKind.Utc), - "'2015-03-12T13:36:37.3710000Z'"); + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37.1234567Z'"); Test_GenerateSqlLiteral_helper( GetMapping("date"), @@ -231,8 +250,44 @@ public override void DateTime_literal_generated_correctly() Test_GenerateSqlLiteral_helper( GetMapping("datetime2"), - new DateTime(2015, 3, 12, 13, 36, 37, 371, DateTimeKind.Utc), - "'2015-03-12T13:36:37.3710000Z'"); + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37.1234567Z'"); + Test_GenerateSqlLiteral_helper( + GetMapping("datetime2(0)"), + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37Z'"); + Test_GenerateSqlLiteral_helper( + GetMapping("datetime2(1)"), + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37.1Z'"); + Test_GenerateSqlLiteral_helper( + GetMapping("datetime2(2)"), + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37.12Z'"); + Test_GenerateSqlLiteral_helper( + GetMapping("datetime2(3)"), + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37.123Z'"); + Test_GenerateSqlLiteral_helper( + GetMapping("datetime2(4)"), + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37.1234Z'"); + Test_GenerateSqlLiteral_helper( + GetMapping("datetime2(5)"), + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37.12345Z'"); + Test_GenerateSqlLiteral_helper( + GetMapping("datetime2(6)"), + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37.123456Z'"); + Test_GenerateSqlLiteral_helper( + GetMapping("datetime2(7)"), + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1234567), + "'2015-03-12T13:36:37.1234567Z'"); + Test_GenerateSqlLiteral_helper( + GetMapping("datetime2(7)"), + new DateTime(2015, 3, 12, 13, 36, 37, DateTimeKind.Utc).AddTicks(1200000), + "'2015-03-12T13:36:37.1200000Z'"); //should this really output trailing zeros? } public override void Float_literal_generated_correctly()