From 93021917215e2642acf510c6a085808398825a93 Mon Sep 17 00:00:00 2001 From: ajcvickers Date: Fri, 18 Sep 2020 16:09:36 -0700 Subject: [PATCH] Handle square brackets around explicitly specified SQL Server type names Fixes #22569 In EF Core 3.1, it was possible to specify a SQL Server column type using square brackets. For example, "[money]" instead of "money". EF Core didn't understand this, so we treated it as an unknown decimal type, and passed it through to SQL Server as-is. In EF Core 5.0, we added support for precision and scale specified independently of the type. We don't do this when we know the type is `money` because doing so is not valid. However, since we don't recognize "[money]" it gets precision and scale added. The fix is to recognize SQL Server types with square brackets. Note that only type names without spaces can have square brackets. --- .../Internal/SqlServerTypeMappingSource.cs | 8 +- .../BuiltInDataTypesSqlServerTest.cs | 343 ++++++++++++++++++ 2 files changed, 350 insertions(+), 1 deletion(-) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index ea0c1bf0f6a..5667e47ba32 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs @@ -229,10 +229,16 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn { var clrType = mappingInfo.ClrType; var storeTypeName = mappingInfo.StoreTypeName; - var storeTypeNameBase = mappingInfo.StoreTypeNameBase; if (storeTypeName != null) { + var storeTypeNameBase = mappingInfo.StoreTypeNameBase; + if (storeTypeNameBase.StartsWith("[", StringComparison.Ordinal) + && storeTypeNameBase.EndsWith("]", StringComparison.Ordinal)) + { + storeTypeNameBase = storeTypeNameBase.Substring(1, storeTypeNameBase.Length - 2); + } + if (clrType == typeof(float) && mappingInfo.Precision != null && mappingInfo.Precision <= 24 diff --git a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs index 145c91bac2f..6e963826306 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs @@ -845,6 +845,170 @@ private static MappedDataTypes CreateMappedDataTypes(int id) SqlVariantInt = 887876 }; + [ConditionalFact] + public virtual void Can_insert_and_read_back_all_mapped_data_types_with_square_brackets() + { + var entity = CreateMappedSquareDataTypes(77); + using (var context = CreateContext()) + { + context.Set().Add(entity); + + Assert.Equal(1, context.SaveChanges()); + } + + var parameters = DumpParameters(); + Assert.Equal( + @"@p0='77' +@p1='True' +@p2='80' (Size = 1) +@p3='0x61626364' (Nullable = false) (Size = 8000) +@p4='0x595A5B5C' (Nullable = false) (Size = 8000) +@p5='73' +@p6='H' (Nullable = false) (Size = 1) +@p7='D' (Nullable = false) (Size = 1) +@p8='G' (Nullable = false) (Size = 1) (DbType = AnsiString) +@p9='A' (Nullable = false) (Size = 1) (DbType = AnsiString) +@p10='2015-01-02T10:11:12.0000000' (DbType = Date) +@p11='2019-01-02T14:11:12.0000000' (DbType = DateTime) +@p12='2017-01-02T12:11:12.1234567' +@p13='2018-01-02T13:11:12.0000000' (DbType = DateTime) +@p14='2016-01-02T11:11:12.1234567+00:00' +@p15='101.1' +@p16='102.2' +@p17='81.1' +@p18='103.3' +@p19='82.2' +@p20='83.3' +@p21='Value4' (Nullable = false) (Size = 20) +@p22='Value2' (Nullable = false) (Size = 8000) (DbType = AnsiString) +@p23='84.4' +@p24='a8f9f951-145f-4545-ac60-b92ff57ada47' +@p25='78' +@p26='-128' +@p27='128' (Size = 1) +@p28='79' +@p29='887876' +@p30='Bang!' (Nullable = false) (Size = 5) +@p31='Gumball Rules OK!' (Nullable = false) (Size = 4000) +@p32='" + + entity.StringAsNvarcharMax + + @"' (Nullable = false) (Size = -1) +@p33='Gumball Rules!' (Nullable = false) (Size = 8000) (DbType = AnsiString) +@p34='" + + entity.StringAsVarcharMax + + @"' (Nullable = false) (Size = -1) (DbType = AnsiString) +@p35='11:15:12' +@p36='65535' +@p37='-1' +@p38='4294967295' +@p39='-1' +@p40='-1' +@p41='18446744073709551615' (Precision = 20)", + parameters, + ignoreLineEndingDifferences: true); + + using (var context = CreateContext()) + { + AssertMappedSquareDataTypes(context.Set().Single(e => e.Int == 77), 77); + } + } + + private static void AssertMappedSquareDataTypes(MappedSquareDataTypes entity, int id) + { + var expected = CreateMappedSquareDataTypes(id); + Assert.Equal(id, entity.Int); + Assert.Equal(78, entity.LongAsBigInt); + Assert.Equal(79, entity.ShortAsSmallint); + Assert.Equal(80, entity.ByteAsTinyint); + Assert.Equal(uint.MaxValue, entity.UintAsInt); + Assert.Equal(ulong.MaxValue, entity.UlongAsBigint); + Assert.Equal(ushort.MaxValue, entity.UShortAsSmallint); + Assert.Equal(sbyte.MinValue, entity.SByteAsTinyint); + Assert.True(entity.BoolAsBit); + Assert.Equal(81.1m, entity.DecimalAsMoney); + Assert.Equal(82.2m, entity.DecimalAsSmallmoney); + Assert.Equal(83.3, entity.DoubleAsFloat); + Assert.Equal(84.4f, entity.FloatAsReal); + Assert.Equal(new DateTime(2015, 1, 2), entity.DateTimeAsDate); + Assert.Equal( + new DateTimeOffset(new DateTime(2016, 1, 2, 11, 11, 12).AddTicks(1234567), TimeSpan.Zero), + entity.DateTimeOffsetAsDatetimeoffset); + Assert.Equal(new DateTime(2017, 1, 2, 12, 11, 12).AddTicks(1234567), entity.DateTimeAsDatetime2); + Assert.Equal(new DateTime(2018, 1, 2, 13, 11, 00), entity.DateTimeAsSmalldatetime); + Assert.Equal(new DateTime(2019, 1, 2, 14, 11, 12), entity.DateTimeAsDatetime); + Assert.Equal(new TimeSpan(11, 15, 12), entity.TimeSpanAsTime); + Assert.Equal(expected.StringAsVarcharMax, entity.StringAsVarcharMax); + Assert.Equal(expected.StringAsNvarcharMax, entity.StringAsNvarcharMax); + Assert.Equal("Gumball Rules!", entity.StringAsText); + Assert.Equal("Gumball Rules OK!", entity.StringAsNtext); + Assert.Equal(new byte[] { 89, 90, 91, 92 }, entity.BytesAsVarbinaryMax); + Assert.Equal(new byte[] { 97, 98, 99, 100 }, entity.BytesAsImage); + Assert.Equal(101m, entity.Decimal); + Assert.Equal(102m, entity.DecimalAsDec); + Assert.Equal(103m, entity.DecimalAsNumeric); + Assert.Equal(new Guid("A8F9F951-145F-4545-AC60-B92FF57ADA47"), entity.GuidAsUniqueidentifier); + Assert.Equal(uint.MaxValue, entity.UintAsBigint); + Assert.Equal(ulong.MaxValue, entity.UlongAsDecimal200); + Assert.Equal(ushort.MaxValue, entity.UShortAsInt); + Assert.Equal(sbyte.MinValue, entity.SByteAsSmallint); + Assert.Equal('A', entity.CharAsVarcharMax); + Assert.Equal('D', entity.CharAsNvarcharMax); + Assert.Equal('G', entity.CharAsText); + Assert.Equal('H', entity.CharAsNtext); + Assert.Equal('I', entity.CharAsInt); + Assert.Equal(StringEnum16.Value2, entity.EnumAsVarcharMax); + Assert.Equal(StringEnumU16.Value4, entity.EnumAsNvarchar20); + Assert.Equal("Bang!", entity.SqlVariantString); + Assert.Equal(887876, entity.SqlVariantInt); + } + + private static MappedSquareDataTypes CreateMappedSquareDataTypes(int id) + => new MappedSquareDataTypes + { + Int = id, + LongAsBigInt = 78L, + ShortAsSmallint = 79, + ByteAsTinyint = 80, + UintAsInt = uint.MaxValue, + UlongAsBigint = ulong.MaxValue, + UShortAsSmallint = ushort.MaxValue, + SByteAsTinyint = sbyte.MinValue, + BoolAsBit = true, + DecimalAsMoney = 81.1m, + DecimalAsSmallmoney = 82.2m, + DoubleAsFloat = 83.3, + FloatAsReal = 84.4f, + DateTimeAsDate = new DateTime(2015, 1, 2, 10, 11, 12), + DateTimeOffsetAsDatetimeoffset = new DateTimeOffset(new DateTime(2016, 1, 2, 11, 11, 12).AddTicks(1234567), TimeSpan.Zero), + DateTimeAsDatetime2 = new DateTime(2017, 1, 2, 12, 11, 12).AddTicks(1234567), + DateTimeAsSmalldatetime = new DateTime(2018, 1, 2, 13, 11, 12), + DateTimeAsDatetime = new DateTime(2019, 1, 2, 14, 11, 12), + TimeSpanAsTime = new TimeSpan(11, 15, 12), + StringAsVarcharMax = string.Concat(Enumerable.Repeat("C", 8001)), + StringAsNvarcharMax = string.Concat(Enumerable.Repeat("D", 4001)), + StringAsText = "Gumball Rules!", + StringAsNtext = "Gumball Rules OK!", + BytesAsVarbinaryMax = new byte[] { 89, 90, 91, 92 }, + BytesAsImage = new byte[] { 97, 98, 99, 100 }, + Decimal = 101.1m, + DecimalAsDec = 102.2m, + DecimalAsNumeric = 103.3m, + GuidAsUniqueidentifier = new Guid("A8F9F951-145F-4545-AC60-B92FF57ADA47"), + UintAsBigint = uint.MaxValue, + UlongAsDecimal200 = ulong.MaxValue, + UShortAsInt = ushort.MaxValue, + SByteAsSmallint = sbyte.MinValue, + CharAsVarcharMax = 'A', + CharAsNvarcharMax = 'D', + CharAsText = 'G', + CharAsNtext = 'H', + CharAsInt = 'I', + EnumAsNvarchar20 = StringEnumU16.Value4, + EnumAsVarcharMax = StringEnum16.Value2, + SqlVariantString = "Bang!", + SqlVariantInt = 887876 + }; + [ConditionalFact] public virtual void Can_insert_and_read_back_all_mapped_nullable_data_types() { @@ -3118,6 +3282,48 @@ public virtual void Columns_have_expected_data_types() MappedSizedSeparatelyDataTypes.StringAsNchar3 ---> [nullable nchar] [MaxLength = 3] MappedSizedSeparatelyDataTypes.StringAsNvarchar3 ---> [nullable nvarchar] [MaxLength = 3] MappedSizedSeparatelyDataTypes.StringAsVarchar3 ---> [nullable varchar] [MaxLength = 3] +MappedSquareDataTypes.BoolAsBit ---> [bit] +MappedSquareDataTypes.ByteAsTinyint ---> [tinyint] [Precision = 3 Scale = 0] +MappedSquareDataTypes.BytesAsImage ---> [image] [MaxLength = 2147483647] +MappedSquareDataTypes.BytesAsVarbinaryMax ---> [varbinary] [MaxLength = -1] +MappedSquareDataTypes.CharAsInt ---> [int] [Precision = 10 Scale = 0] +MappedSquareDataTypes.CharAsNtext ---> [ntext] [MaxLength = 1073741823] +MappedSquareDataTypes.CharAsNvarcharMax ---> [nvarchar] [MaxLength = 1] +MappedSquareDataTypes.CharAsText ---> [text] [MaxLength = 2147483647] +MappedSquareDataTypes.CharAsVarcharMax ---> [varchar] [MaxLength = 1] +MappedSquareDataTypes.DateTimeAsDate ---> [date] [Precision = 0] +MappedSquareDataTypes.DateTimeAsDatetime ---> [datetime] [Precision = 3] +MappedSquareDataTypes.DateTimeAsDatetime2 ---> [datetime2] [Precision = 7] +MappedSquareDataTypes.DateTimeAsSmalldatetime ---> [smalldatetime] [Precision = 0] +MappedSquareDataTypes.DateTimeOffsetAsDatetimeoffset ---> [datetimeoffset] [Precision = 7] +MappedSquareDataTypes.Decimal ---> [decimal] [Precision = 18 Scale = 0] +MappedSquareDataTypes.DecimalAsDec ---> [decimal] [Precision = 18 Scale = 0] +MappedSquareDataTypes.DecimalAsMoney ---> [money] [Precision = 19 Scale = 4] +MappedSquareDataTypes.DecimalAsNumeric ---> [numeric] [Precision = 18 Scale = 0] +MappedSquareDataTypes.DecimalAsSmallmoney ---> [smallmoney] [Precision = 10 Scale = 4] +MappedSquareDataTypes.DoubleAsFloat ---> [float] [Precision = 53] +MappedSquareDataTypes.EnumAsNvarchar20 ---> [nvarchar] [MaxLength = 20] +MappedSquareDataTypes.EnumAsVarcharMax ---> [varchar] [MaxLength = -1] +MappedSquareDataTypes.FloatAsReal ---> [real] [Precision = 24] +MappedSquareDataTypes.GuidAsUniqueidentifier ---> [uniqueidentifier] +MappedSquareDataTypes.Int ---> [int] [Precision = 10 Scale = 0] +MappedSquareDataTypes.LongAsBigInt ---> [bigint] [Precision = 19 Scale = 0] +MappedSquareDataTypes.SByteAsSmallint ---> [smallint] [Precision = 5 Scale = 0] +MappedSquareDataTypes.SByteAsTinyint ---> [tinyint] [Precision = 3 Scale = 0] +MappedSquareDataTypes.ShortAsSmallint ---> [smallint] [Precision = 5 Scale = 0] +MappedSquareDataTypes.SqlVariantInt ---> [sql_variant] [MaxLength = 0] +MappedSquareDataTypes.SqlVariantString ---> [sql_variant] [MaxLength = 0] +MappedSquareDataTypes.StringAsNtext ---> [ntext] [MaxLength = 1073741823] +MappedSquareDataTypes.StringAsNvarcharMax ---> [nvarchar] [MaxLength = -1] +MappedSquareDataTypes.StringAsText ---> [text] [MaxLength = 2147483647] +MappedSquareDataTypes.StringAsVarcharMax ---> [varchar] [MaxLength = -1] +MappedSquareDataTypes.TimeSpanAsTime ---> [time] [Precision = 7] +MappedSquareDataTypes.UintAsBigint ---> [bigint] [Precision = 19 Scale = 0] +MappedSquareDataTypes.UintAsInt ---> [int] [Precision = 10 Scale = 0] +MappedSquareDataTypes.UlongAsBigint ---> [bigint] [Precision = 19 Scale = 0] +MappedSquareDataTypes.UlongAsDecimal200 ---> [decimal] [Precision = 20 Scale = 0] +MappedSquareDataTypes.UShortAsInt ---> [int] [Precision = 10 Scale = 0] +MappedSquareDataTypes.UShortAsSmallint ---> [smallint] [Precision = 5 Scale = 0] MaxLengthDataTypes.ByteArray5 ---> [nullable varbinary] [MaxLength = 5] MaxLengthDataTypes.ByteArray9000 ---> [nullable varbinary] [MaxLength = -1] MaxLengthDataTypes.Id ---> [int] [Precision = 10 Scale = 0] @@ -3308,6 +3514,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Property(e => e.Int).ValueGeneratedNever(); }); + modelBuilder.Entity( + b => + { + b.HasKey(e => e.Int); + b.Property(e => e.Int).ValueGeneratedNever(); + }); + modelBuilder.Entity( b => { @@ -3331,6 +3544,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .ValueGeneratedNever(); MakeRequired(modelBuilder); + MakeRequired(modelBuilder); MakeRequired(modelBuilder); modelBuilder.Entity(); @@ -3585,6 +3799,135 @@ protected class MappedDataTypes public object SqlVariantInt { get; set; } } + protected class MappedSquareDataTypes + { + [Column(TypeName = "[int]")] + public int Int { get; set; } + + [Column(TypeName = "[bigint]")] + public long LongAsBigInt { get; set; } + + [Column(TypeName = "[smallint]")] + public short ShortAsSmallint { get; set; } + + [Column(TypeName = "[tinyint]")] + public byte ByteAsTinyint { get; set; } + + [Column(TypeName = "[int]")] + public uint UintAsInt { get; set; } + + [Column(TypeName = "[bigint]")] + public ulong UlongAsBigint { get; set; } + + [Column(TypeName = "[smallint]")] + public ushort UShortAsSmallint { get; set; } + + [Column(TypeName = "[tinyint]")] + public sbyte SByteAsTinyint { get; set; } + + [Column(TypeName = "[bit]")] + public bool BoolAsBit { get; set; } + + [Column(TypeName = "[money]")] + public decimal DecimalAsMoney { get; set; } + + [Column(TypeName = "[smallmoney]")] + public decimal DecimalAsSmallmoney { get; set; } + + [Column(TypeName = "[float]")] + public double DoubleAsFloat { get; set; } + + [Column(TypeName = "[real]")] + public float FloatAsReal { get; set; } + + [Column(TypeName = "[date]")] + public DateTime DateTimeAsDate { get; set; } + + [Column(TypeName = "[datetimeoffset]")] + public DateTimeOffset DateTimeOffsetAsDatetimeoffset { get; set; } + + [Column(TypeName = "[datetime2]")] + public DateTime DateTimeAsDatetime2 { get; set; } + + [Column(TypeName = "[smalldatetime]")] + public DateTime DateTimeAsSmalldatetime { get; set; } + + [Column(TypeName = "[datetime]")] + public DateTime DateTimeAsDatetime { get; set; } + + [Column(TypeName = "[time]")] + public TimeSpan TimeSpanAsTime { get; set; } + + [Column(TypeName = "[varchar](max)")] + public string StringAsVarcharMax { get; set; } + + [Column(TypeName = "[nvarchar](max)")] + public string StringAsNvarcharMax { get; set; } + + [Column(TypeName = "[text]")] + public string StringAsText { get; set; } + + [Column(TypeName = "[ntext]")] + public string StringAsNtext { get; set; } + + [Column(TypeName = "[varbinary](max)")] + public byte[] BytesAsVarbinaryMax { get; set; } + + [Column(TypeName = "[image]")] + public byte[] BytesAsImage { get; set; } + + [Column(TypeName = "[decimal]")] + public decimal Decimal { get; set; } + + [Column(TypeName = "[dec]")] + public decimal DecimalAsDec { get; set; } + + [Column(TypeName = "[numeric]")] + public decimal DecimalAsNumeric { get; set; } + + [Column(TypeName = "[uniqueidentifier]")] + public Guid GuidAsUniqueidentifier { get; set; } + + [Column(TypeName = "[bigint]")] + public uint UintAsBigint { get; set; } + + [Column(TypeName = "[decimal](20,0)")] + public ulong UlongAsDecimal200 { get; set; } + + [Column(TypeName = "[int]")] + public ushort UShortAsInt { get; set; } + + [Column(TypeName = "[smallint]")] + public sbyte SByteAsSmallint { get; set; } + + [Column(TypeName = "[varchar](max)")] + public char CharAsVarcharMax { get; set; } + + [Column(TypeName = "[nvarchar](max)")] + public char CharAsNvarcharMax { get; set; } + + [Column(TypeName = "[text]")] + public char CharAsText { get; set; } + + [Column(TypeName = "[ntext]")] + public char CharAsNtext { get; set; } + + [Column(TypeName = "[int]")] + public char CharAsInt { get; set; } + + [Column(TypeName = "[varchar](max)")] + public StringEnum16 EnumAsVarcharMax { get; set; } + + [Column(TypeName = "[nvarchar](20)")] + public StringEnumU16 EnumAsNvarchar20 { get; set; } + + [Column(TypeName = "[sql_variant]")] + public object SqlVariantString { get; set; } + + [Column(TypeName = "[sql_variant]")] + public object SqlVariantInt { get; set; } + } + protected class MappedSizedDataTypes { public int Id { get; set; }