diff --git a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs index 374563a31dd..72e97df1132 100644 --- a/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs +++ b/src/EFCore.Sqlite.Core/Scaffolding/Internal/SqliteDatabaseModelFactory.cs @@ -88,6 +88,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) @@ -130,6 +132,7 @@ public class SqliteDatabaseModelFactory : DatabaseModelFactory .Concat(_floatTypes.Select(t => KeyValuePair.Create(t, typeof(float)))) .Concat(_decimalTypes.Select(t => KeyValuePair.Create(t, typeof(decimal)))) .Concat(_timeOnlyTypes.Select(t => KeyValuePair.Create(t, typeof(TimeOnly)))) + .Concat(_halfTypes.Select(t => KeyValuePair.Create(t, typeof(Half)))) .Concat(_ushortTypes.Select(t => KeyValuePair.Create(t, typeof(ushort)))) .Concat(_uintTypes.Select(t => KeyValuePair.Create(t, typeof(uint)))) .Concat(_ulongTypes.Select(t => KeyValuePair.Create(t, typeof(ulong)))) @@ -437,6 +440,11 @@ private void ParseClrDefaults(DatabaseTable table) // Ignored } } + else if (type == typeof(Half) + && double.TryParse(defaultValueSql, NumberStyles.Float, CultureInfo.InvariantCulture, out var halfValue)) + { + column.DefaultValue = (Half)halfValue; + } else if (defaultValueSql.StartsWith('\'') && defaultValueSql.EndsWith('\'')) { @@ -819,6 +827,19 @@ protected virtual void InferClrTypes(DbConnection connection, DatabaseTable tabl continue; } + 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 (defaultClrTpe != typeof(double)) { column["ClrType"] = typeof(double); 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..b5bd4804578 --- /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.Globalization; + +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 : 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 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) + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(Half)), + storeType)) + { + } + + /// + /// 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) + => ((float)(Half)value).ToString("R", CultureInfo.InvariantCulture); +} diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs index d28b9373970..3b52a534341 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs @@ -80,6 +80,7 @@ private static readonly HashSet SpatialiteTypes { typeof(decimal), SqliteDecimalTypeMapping.Default }, { typeof(double), Real }, { typeof(float), new FloatTypeMapping(RealTypeName) }, + { typeof(Half), SqliteHalfTypeMapping.Default }, { typeof(Guid), SqliteGuidTypeMapping.Default }, { typeof(JsonTypePlaceholder), SqliteJsonTypeMapping.Default } }; diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs b/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs index 40cdbeb32c6..75f6e67e86b 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteValueBinder.cs @@ -160,6 +160,13 @@ public virtual void Bind() var value1 = (double)(float)value; BindDouble(value1); } +#if NET6_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; @@ -251,6 +258,9 @@ public virtual void Bind() { typeof(decimal), SqliteType.Text }, { typeof(double), SqliteType.Real }, { typeof(float), SqliteType.Real }, +#if NET6_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 f8a6ea29378..8a9dd84080d 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteValueReader.cs @@ -230,6 +230,13 @@ public virtual string GetString(int ordinal) return (T)(object)GetFloat(ordinal); } +#if NET6_0_OR_GREATER + if (typeof(T) == typeof(Half)) + { + return (T)(object)(Half)GetDouble(ordinal); + } +#endif + if (typeof(T) == typeof(Guid)) { return (T)(object)GetGuid(ordinal); @@ -347,6 +354,13 @@ public virtual string GetString(int ordinal) return (T)(object)GetFloat(ordinal); } +#if NET6_0_OR_GREATER + if (type == typeof(Half)) + { + return (T)(object)(Half)GetDouble(ordinal); + } +#endif + if (type == 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 8298b65f460..29cce1c1afe 100644 --- a/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingSourceTest.cs +++ b/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingSourceTest.cs @@ -27,6 +27,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), null), InlineData("REAL", typeof(double), DbType.Double), InlineData("INTEGER", typeof(ByteEnum), DbType.Byte), InlineData("INTEGER", typeof(ShortEnum), DbType.Int16), @@ -50,6 +51,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?), null), InlineData("REAL", typeof(double?), DbType.Double), InlineData("INTEGER", typeof(ByteEnum?), DbType.Byte), InlineData("INTEGER", typeof(ShortEnum?), DbType.Int16), @@ -176,6 +178,9 @@ 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("REAL", typeof(Half), null), + InlineData("UNREALISTIC", typeof(Half), null), + InlineData("RUBBISH", typeof(Half), null), InlineData("REAL", typeof(double), DbType.Double), InlineData("UNREALISTIC", typeof(double), DbType.Double), InlineData("RUBBISH", typeof(double), DbType.Double), @@ -245,6 +250,9 @@ 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("REAL", typeof(Half?), null), + InlineData("UNREALISTIC", typeof(Half?), null), + InlineData("RUBBISH", typeof(Half?), null), InlineData("REAL", typeof(double?), DbType.Double), InlineData("UNREALISTIC", typeof(double?), DbType.Double), InlineData("RUBBISH", typeof(double?), DbType.Double), diff --git a/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs b/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs index 429505aa4d1..d4b62987c5a 100644 --- a/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs +++ b/test/EFCore.Sqlite.Tests/Storage/SqliteTypeMappingTest.cs @@ -67,7 +67,8 @@ protected override DbCommand CreateTestCommand() [ConditionalTheory, InlineData(typeof(SqliteDateTimeOffsetTypeMapping), typeof(DateTimeOffset)), InlineData(typeof(SqliteDateTimeTypeMapping), typeof(DateTime)), InlineData(typeof(SqliteDecimalTypeMapping), typeof(decimal)), - InlineData(typeof(SqliteGuidTypeMapping), typeof(Guid)), InlineData(typeof(SqliteULongTypeMapping), typeof(ulong))] + InlineData(typeof(SqliteGuidTypeMapping), typeof(Guid)), InlineData(typeof(SqliteHalfTypeMapping), typeof(Half)), + InlineData(typeof(SqliteULongTypeMapping), typeof(ulong))] public override void Create_and_clone_with_converter(Type mappingType, Type type) => base.Create_and_clone_with_converter(mappingType, type); @@ -138,6 +139,16 @@ public override void ULong_literal_generated_correctly() Test_GenerateSqlLiteral_helper(typeMapping, long.MaxValue + 1ul, "-9223372036854775808"); } + [ConditionalFact] + public void Half_literal_generated_correctly() + { + var typeMapping = SqliteHalfTypeMapping.Default; + + Test_GenerateSqlLiteral_helper(typeMapping, Half.MinValue, "-65504"); + Test_GenerateSqlLiteral_helper(typeMapping, Half.MaxValue, "65504"); + Test_GenerateSqlLiteral_helper(typeMapping, (Half)3.14f, "3.140625"); + } + protected override DbContextOptions ContextOptions { get; } = new DbContextOptionsBuilder() .UseInternalServiceProvider(new ServiceCollection().AddEntityFrameworkSqlite().BuildServiceProvider(validateScopes: true)) diff --git a/test/Microsoft.Data.Sqlite.Tests/SqliteDataReaderTest.cs b/test/Microsoft.Data.Sqlite.Tests/SqliteDataReaderTest.cs index f86a34c7dfc..f5d738d90cb 100644 --- a/test/Microsoft.Data.Sqlite.Tests/SqliteDataReaderTest.cs +++ b/test/Microsoft.Data.Sqlite.Tests/SqliteDataReaderTest.cs @@ -1208,6 +1208,25 @@ public void GetFloat_throws_when_closed() public void GetFloat_throws_when_non_query() => X_throws_when_non_query(r => r.GetFloat(0)); +#if NET6_0_OR_GREATER + [Fact] + public void GetFieldValue_of_Half_works() + => GetX_works( + "SELECT 3;", + r => r.GetFieldValue(0), + (Half)3f); + + [Fact] + public void GetFieldValue_of_Half_throws_when_null() + => GetX_throws_when_null(r => r.GetFieldValue(0)); + + [Fact] + public void GetFieldValue_of_NullableHalf_works() + => GetFieldValue_works( + "SELECT 3.14;", + (Half?)3.14f); +#endif + [Theory, InlineData("2.0", 2.0), InlineData("9e999", double.PositiveInfinity),