diff --git a/src/EFCore.Relational/Storage/HalfTypeMapping.cs b/src/EFCore.Relational/Storage/HalfTypeMapping.cs new file mode 100644 index 00000000000..85b6d74f68a --- /dev/null +++ b/src/EFCore.Relational/Storage/HalfTypeMapping.cs @@ -0,0 +1,61 @@ +// 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.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace Microsoft.EntityFrameworkCore.Storage +{ + /// + /// + /// Represents the mapping between a .NET type and a database type. + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// + /// See Implementation of database providers and extensions + /// for more information and examples. + /// + public class HalfTypeMapping : RelationalTypeMapping + { + /// + /// 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 static HalfTypeMapping Default { get; } = new("REAL"); + + /// + /// 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 HalfTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Single) + : base(storeType, typeof(Half), dbType, jsonValueReaderWriter: JsonHalfReaderWriter.Instance) + { + } + + /// + /// 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 HalfTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters) { } + + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => new HalfTypeMapping(parameters); + } +} diff --git a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs index b67f8571b42..39469544edc 100644 --- a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs +++ b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs @@ -87,6 +87,8 @@ public class SqliteDatabaseModelFactory : DatabaseModelFactory private static readonly HashSet _floatTypes = new(StringComparer.OrdinalIgnoreCase) { "SINGLE" }; + private static readonly HashSet _halfTypes = new(StringComparer.OrdinalIgnoreCase) { "HALF" }; + private static readonly HashSet _decimalTypes = new(StringComparer.OrdinalIgnoreCase) { "DECIMAL" }; private static readonly HashSet _ushortTypes = new(StringComparer.OrdinalIgnoreCase) @@ -127,6 +129,7 @@ public class SqliteDatabaseModelFactory : DatabaseModelFactory .Concat(_shortTypes.Select(t => KeyValuePair.Create(t, typeof(short)))) .Concat(_sbyteTypes.Select(t => KeyValuePair.Create(t, typeof(sbyte)))) .Concat(_floatTypes.Select(t => KeyValuePair.Create(t, typeof(float)))) + .Concat(_halfTypes.Select(t => KeyValuePair.Create(t, typeof(Half)))) .Concat(_decimalTypes.Select(t => KeyValuePair.Create(t, typeof(decimal)))) .Concat(_timeOnlyTypes.Select(t => KeyValuePair.Create(t, typeof(TimeOnly)))) .Concat(_ushortTypes.Select(t => KeyValuePair.Create(t, typeof(ushort)))) @@ -811,6 +814,19 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl _logger.OutOfRangeWarning(column.Name, table.Name, "float"); } + if (_halfTypes.Contains(baseColumnType)) + { + if (min >= (double)Half.MinValue + && max <= (double)Half.MaxValue) + { + column["ClrType"] = typeof(Half); + + continue; + } + + _logger.OutOfRangeWarning(column.Name, table.Name, "Half"); + } + if (_decimalTypes.Contains(baseColumnType)) { column["ClrType"] = typeof(decimal); diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteHalfTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteHalfTypeMapping.cs new file mode 100644 index 00000000000..100ca29b2b9 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteHalfTypeMapping.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data; + +namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal +{ + /// + /// 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 class SqliteHalfTypeMapping : HalfTypeMapping + { + /// + /// 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 static new SqliteHalfTypeMapping Default { get; } = new(SqliteTypeMappingSource.RealTypeName); + + /// + /// 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 SqliteHalfTypeMapping( + string storeType, + DbType? dbType = System.Data.DbType.Single) + : base(storeType, dbType) + { + } + + /// + /// 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 SqliteHalfTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + } + + /// + /// Creates a copy of this mapping. + /// + /// The parameters for this mapping. + /// The newly created mapping. + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SqliteHalfTypeMapping(parameters); + + /// + /// 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 GenerateNonNullSqlLiteral(object value) + => new DoubleTypeMapping(StoreType).GenerateSqlLiteral((double)(Half)value); + } +} diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs index 8e9e80978f0..4e16918ad23 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs @@ -82,6 +82,9 @@ private static readonly HashSet SpatialiteTypes { typeof(decimal), SqliteDecimalTypeMapping.Default }, { typeof(double), Real }, { typeof(float), new FloatTypeMapping(RealTypeName) }, +#if NET5_0_OR_GREATER + { typeof(Half), new HalfTypeMapping(RealTypeName) }, +#endif { typeof(Guid), SqliteGuidTypeMapping.Default }, { typeof(JsonElement), SqliteJsonTypeMapping.Default } }; diff --git a/src/EFCore/Storage/Json/JsonHalfReaderWriter.cs b/src/EFCore/Storage/Json/JsonHalfReaderWriter.cs new file mode 100644 index 00000000000..72901575940 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonHalfReaderWriter.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json +{ + /// + /// Reads and writes JSON for Half type values. + /// + public sealed class JsonHalfReaderWriter : JsonValueReaderWriter + { + private const string HalfFormatConst = "{0:0.0###}"; + + private static readonly PropertyInfo InstanceProperty = typeof(JsonHalfReaderWriter).GetProperty(nameof(Instance))!; + + /// + /// 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 static JsonHalfReaderWriter Instance { get; } = new(); + + private JsonHalfReaderWriter() + { + + } + + /// + public override Expression ConstructorExpression + => Expression.Property(null, InstanceProperty); + + /// + public override Half FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => + Half.Parse(manager.CurrentReader.GetString()!, CultureInfo.InvariantCulture); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, Half value) => + writer.WriteStringValue(string.Format(CultureInfo.InvariantCulture, HalfFormatConst, value)); + } +} diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs b/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs index b1908031a13..58735ced8a3 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs @@ -154,6 +154,13 @@ public virtual void Bind() var value1 = (double)(float)value; BindDouble(value1); } +#if NET5_0_OR_GREATER + else if (type == typeof(Half)) + { + var value1 = (double)(Half)value; + BindDouble(value1); + } +#endif else if (type == typeof(Guid)) { var guid = (Guid)value; @@ -245,6 +252,9 @@ public virtual void Bind() { typeof(decimal), SqliteType.Text }, { typeof(double), SqliteType.Real }, { typeof(float), SqliteType.Real }, +#if NET5_0_OR_GREATER + { typeof(Half), SqliteType.Real }, +#endif { typeof(Guid), SqliteType.Text }, { typeof(int), SqliteType.Integer }, { typeof(long), SqliteType.Integer }, diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs b/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs index 8ea8d3ac24b..5ed78203a80 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs @@ -99,6 +99,11 @@ public virtual double GetDouble(int ordinal) public virtual float GetFloat(int ordinal) => (float)GetDouble(ordinal); +#if NET5_0_OR_GREATER + public virtual Half GetHalf(int ordinal) + => (Half)GetDouble(ordinal); +#endif + public virtual Guid GetGuid(int ordinal) { var sqliteType = GetSqliteType(ordinal); @@ -203,6 +208,13 @@ public virtual string GetString(int ordinal) return (T)(object)GetFloat(ordinal); } +#if NET5_0_OR_GREATER + if (typeof(T) == typeof(Half)) + { + return (T)(object)GetHalf(ordinal); + } +#endif + if (typeof(T) == typeof(Guid)) { return (T)(object)GetGuid(ordinal); diff --git a/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingSourceTest.cs b/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingSourceTest.cs index 30cc206a72e..26ab64ead6b 100644 --- a/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingSourceTest.cs +++ b/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingSourceTest.cs @@ -50,6 +50,7 @@ public class SqliteTypeMappingSourceTest : RelationalTypeMappingSourceTestBase [InlineData("TEXT", typeof(TimeSpan?), DbType.Time)] [InlineData("TEXT", typeof(decimal?), DbType.Decimal)] [InlineData("REAL", typeof(float?), DbType.Single)] + [InlineData("REAL", typeof(Half), DbType.Single)] [InlineData("REAL", typeof(double?), DbType.Double)] [InlineData("INTEGER", typeof(ByteEnum?), DbType.Byte)] [InlineData("INTEGER", typeof(ShortEnum?), DbType.Int16)] @@ -176,6 +177,7 @@ public void Does_mappings_for_store_type(string storeType, Type clrType, DbType? [InlineData("REAL", typeof(float), DbType.Single)] [InlineData("UNREALISTIC", typeof(float), DbType.Single)] [InlineData("RUBBISH", typeof(float), DbType.Single)] + [InlineData("RUBBISH", typeof(Half), DbType.Single)] [InlineData("REAL", typeof(double), DbType.Double)] [InlineData("UNREALISTIC", typeof(double), DbType.Double)] [InlineData("RUBBISH", typeof(double), DbType.Double)] @@ -304,6 +306,7 @@ public void Does_default_mappings_for_values() Assert.Equal("TEXT", CreateRelationalTypeMappingSource(model).GetMappingForValue(1.0m).StoreType); Assert.Equal("REAL", CreateRelationalTypeMappingSource(model).GetMappingForValue(1.0).StoreType); Assert.Equal("REAL", CreateRelationalTypeMappingSource(model).GetMappingForValue(1.0f).StoreType); + Assert.Equal("REAL", CreateRelationalTypeMappingSource(model).GetMappingForValue((Half)1.0).StoreType); } [ConditionalFact] diff --git a/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs b/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs index f51b7d0733e..2eac229e7b3 100644 --- a/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs +++ b/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs @@ -72,6 +72,7 @@ protected override DbCommand CreateTestCommand() [InlineData(typeof(SqliteDecimalTypeMapping), typeof(decimal))] [InlineData(typeof(SqliteGuidTypeMapping), typeof(Guid))] [InlineData(typeof(SqliteULongTypeMapping), typeof(ulong))] + [InlineData(typeof(EntityFrameworkCore.Sqlite.Storage.Internal.SqliteHalfTypeMapping), typeof(Half))] public override void Create_and_clone_with_converter(Type mappingType, Type type) => base.Create_and_clone_with_converter(mappingType, type);