diff --git a/src/EFCore.Ydb/CHANGELOG.md b/src/EFCore.Ydb/CHANGELOG.md index cd153026..c1633f3f 100644 --- a/src/EFCore.Ydb/CHANGELOG.md +++ b/src/EFCore.Ydb/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed bug: incompatible coalesce types ([#531](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/531)). - Upgraded ADO.NET provider version: `0.17.0` → `0.24.0`. - Fixed Decimal precision/scale mapping in EF provider. - Supported Guid (Uuid YDB type). diff --git a/src/EFCore.Ydb/src/Query/Internal/Translators/YdbQueryableAggregateMethodTranslator.cs b/src/EFCore.Ydb/src/Query/Internal/Translators/YdbQueryableAggregateMethodTranslator.cs index 75e78927..efe895ed 100644 --- a/src/EFCore.Ydb/src/Query/Internal/Translators/YdbQueryableAggregateMethodTranslator.cs +++ b/src/EFCore.Ydb/src/Query/Internal/Translators/YdbQueryableAggregateMethodTranslator.cs @@ -133,7 +133,7 @@ public class YdbQueryableAggregateMethodTranslator( [sumSqlExpression], nullable: true, argumentsPropagateNullability: ArrayUtil.FalseArrays[1], - typeof(decimal)), + typeof(long)), sumInputType, sumSqlExpression.TypeMapping); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs index 2b0d6b2b..4149a0f5 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs @@ -10,7 +10,7 @@ public class YdbDecimalTypeMapping : DecimalTypeMapping public new static YdbDecimalTypeMapping Default => new(); - public YdbDecimalTypeMapping() : this( + private YdbDecimalTypeMapping() : this( new RelationalTypeMappingParameters( new CoreTypeMappingParameters(typeof(decimal)), storeType: "Decimal", @@ -41,4 +41,7 @@ protected override void ConfigureParameter(DbParameter parameter) if (Scale is { } s) parameter.Scale = (byte)s; } + + protected override string GenerateNonNullSqlLiteral(object value) => + $"Decimal('{base.GenerateNonNullSqlLiteral(value)}', {Precision ?? DefaultPrecision}, {Scale ?? DefaultScale})"; } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs index 323103b9..fc7d7e49 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -125,12 +125,7 @@ RelationalTypeMappingSourceDependencies relationalDependencies var clrType = mappingInfo.ClrType; var storeTypeName = mappingInfo.StoreTypeName; - if (storeTypeName is null) - { - return clrType is null ? null : ClrTypeMapping.GetValueOrDefault(clrType); - } - - if (!StoreTypeMapping.TryGetValue(storeTypeName, out var mappings)) + if (storeTypeName is null || !StoreTypeMapping.TryGetValue(storeTypeName, out var mappings)) { return clrType is null ? null : ClrTypeMapping.GetValueOrDefault(clrType); } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterQueryYdbFixture.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterQueryYdbFixture.cs deleted file mode 100644 index fd157db6..00000000 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterQueryYdbFixture.cs +++ /dev/null @@ -1,14 +0,0 @@ -using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.TestUtilities; - -namespace EntityFrameworkCore.Ydb.FunctionalTests.Query; - -public class DecimalParameterQueryYdbFixture : SharedStoreFixtureBase -{ - protected override string StoreName => "DecimalParameterTest"; - - protected override ITestStoreFactory TestStoreFactory => YdbTestStoreFactory.Instance; - - public class TestContext(DbContextOptions options) : DbContext(options); -} diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs new file mode 100644 index 00000000..a5522380 --- /dev/null +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs @@ -0,0 +1,143 @@ +using EntityFrameworkCore.Ydb.Extensions; +using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Xunit; + +namespace EntityFrameworkCore.Ydb.FunctionalTests.Query; + +public class DecimalParameterizedYdbTest +{ + private static DbContextOptions BuildOptions() => + new DbContextOptionsBuilder() + .UseYdb("Host=localhost;Port=2136") + .EnableServiceProviderCaching(false) + .LogTo(Console.WriteLine) + .Options; + + public static TheoryData SuccessCases => new() + { + { 22, 9, 1.23456789m }, + { 30, 10, 123.4567890123m }, + { 18, 2, 12345678.91m }, + { 10, 0, 9999999999m }, + { 22, 9, -0.123456789m }, + { 5, 2, 12.34m }, + { 30, 10, 0.0000000001m } + }; + + public static TheoryData OverflowCases => new() + { + { 15, 2, 123456789012345.67m }, + { 10, 0, 12345678901m }, + { 22, 9, 1.0000000001m }, + { 18, 2, 1.239m }, + { 18, 2, 100000000000000000m }, + { 22, 9, 12345678901234567890.123456789m }, + { 22, 9, -12345678901234567890.123456789m }, + { 4, 2, 123.456m }, + { 1, 0, 10m }, + { 5, 0, 100000m } + }; + + private static ParametricDecimalContext NewCtx(int p, int s) => new(BuildOptions(), p, s); + + [Theory] + [MemberData(nameof(SuccessCases))] + public async Task Should_RoundtripDecimal_When_ValueFitsPrecisionAndScale(int p, int s, decimal value) + { + await using var testStore = YdbTestStoreFactory.Instance.Create("DecimalParameterizedYdb"); + + await using var ctx = NewCtx(p, s); + await testStore.CleanAsync(ctx); + await ctx.Database.EnsureCreatedAsync(); + try + { + var e = new ParamItem { Price = value }; + ctx.Add(e); + await ctx.SaveChangesAsync(); + var got = await ctx.Items.SingleAsync(x => x.Id == e.Id); + Assert.Equal(value, got.Price); + var tms = ctx.GetService(); + var et = ctx.Model.FindEntityType(typeof(ParamItem))!; + var prop = et.FindProperty(nameof(ParamItem.Price))!; + var mapping = tms.FindMapping(prop)!; + Assert.Equal($"Decimal({p}, {s})", mapping.StoreType); + } + finally + { + await ctx.Database.EnsureDeletedAsync(); + } + } + + [Theory] + [MemberData(nameof(OverflowCases))] + public async Task Should_ThrowOverflow_When_ValueExceedsPrecisionOrScale(int p, int s, decimal value) + { + await using var testStore = YdbTestStoreFactory.Instance.Create("DecimalParameterizedYdb"); + + await using var ctx = NewCtx(p, s); + await testStore.CleanAsync(ctx); + await ctx.Database.EnsureCreatedAsync(); + try + { + ctx.Add(new ParamItem { Price = value }); + await Assert.ThrowsAsync(() => ctx.SaveChangesAsync()); + } + finally + { + await ctx.Database.EnsureDeletedAsync(); + } + } + + [Theory] + [MemberData(nameof(SuccessCases))] + public async Task Should_SumDecimal_When_ValueFitsPrecisionAndScale(int p, int s, decimal value) + { + await using var testStore = YdbTestStoreFactory.Instance.Create("DecimalParameterizedYdb"); + + const int multiplier = 5; + await using var ctx = NewCtx(p, s); + await testStore.CleanAsync(ctx); + await ctx.Database.EnsureCreatedAsync(); + try + { + for (var i = 0; i < multiplier; i++) + ctx.Add(new ParamItem { Price = value }); + await ctx.SaveChangesAsync(); + var got = await ctx.Items.Select(x => x.Price).SumAsync(); + + Assert.Equal(value * multiplier, got); + + var tms = ctx.GetService(); + var et = ctx.Model.FindEntityType(typeof(ParamItem))!; + var prop = et.FindProperty(nameof(ParamItem.Price))!; + var mapping = tms.FindMapping(prop)!; + Assert.Equal($"Decimal({p}, {s})", mapping.StoreType); + } + finally + { + await ctx.Database.EnsureDeletedAsync(); + } + } + + public sealed class ParametricDecimalContext(DbContextOptions options, int p, int s) + : DbContext(options) + { + public DbSet Items => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity(b => + { + b.ToTable($"Items_{p}_{s}"); + b.HasKey(x => x.Id); + b.Property(x => x.Price).HasPrecision(p, s); + }); + } + + public sealed class ParamItem + { + public int Id { get; init; } + public decimal Price { get; init; } + } +} diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTheoryTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTheoryTest.cs deleted file mode 100644 index 270fd552..00000000 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTheoryTest.cs +++ /dev/null @@ -1,106 +0,0 @@ -using EntityFrameworkCore.Ydb.Extensions; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; -using Xunit; - -namespace EntityFrameworkCore.Ydb.FunctionalTests.Query; - -public class DecimalParameterizedYdbTheoryTest(DecimalParameterQueryYdbFixture fixture) - : IClassFixture -{ - private DbContextOptions BuildOptions() - { - using var baseCtx = fixture.CreateContext(); - var cs = baseCtx.Database.GetDbConnection().ConnectionString; - - return new DbContextOptionsBuilder() - .UseYdb(cs) - .ReplaceService() - .Options; - } - - public static IEnumerable AdoLikeCases => - [ - [22, 9, 1.23456789m], - [30, 10, 123.4567890123m], - [18, 2, 12345678.91m], - [10, 0, 9999999999m], - [22, 9, -0.123456789m], - [5, 2, 12.34m], - [30, 10, 0.0000000001m] - ]; - - public static IEnumerable OverflowCases => - [ - [15, 2, 123456789012345.67m], - [10, 0, 12345678901m], - [22, 9, 1.0000000001m], - [18, 2, 1.239m], - [18, 2, 100000000000000000m], - [22, 9, 12345678901234567890.123456789m], - [22, 9, -12345678901234567890.123456789m], - [4, 2, 123.456m], - [1, 0, 10m], - [5, 0, 100000m] - ]; - - private ParametricDecimalContext NewCtx(int p, int s) - => new(BuildOptions(), p, s); - - private static Task DropItemsTableAsync(DbContext ctx, int p, int s) - { - var helper = ctx.GetService(); - var tableName = $"Items_{p}_{s}"; - var sql = $"DROP TABLE IF EXISTS {helper.DelimitIdentifier(tableName)}"; - - return ctx.Database.ExecuteSqlRawAsync(sql); - } - - [Theory] - [MemberData(nameof(AdoLikeCases))] - public async Task Decimal_roundtrips_or_rounds_like_ado(int p, int s, decimal value) - { - await using var ctx = NewCtx(p, s); - await ctx.Database.EnsureCreatedAsync(); - - try - { - var e = new ParamItem { Price = value }; - ctx.Add(e); - await ctx.SaveChangesAsync(); - - var got = await ctx.Items.SingleAsync(x => x.Id == e.Id); - - Assert.Equal(value, got.Price); - - var tms = ctx.GetService(); - var et = ctx.Model.FindEntityType(typeof(ParamItem))!; - var prop = et.FindProperty(nameof(ParamItem.Price))!; - var mapping = tms.FindMapping(prop)!; - Assert.Equal($"Decimal({p}, {s})", mapping.StoreType); - } - finally - { - await DropItemsTableAsync(ctx, p, s); - } - } - - [Theory] - [MemberData(nameof(OverflowCases))] - public async Task Decimal_overflow_bubbles_up(int p, int s, decimal value) - { - await using var ctx = NewCtx(p, s); - await ctx.Database.EnsureCreatedAsync(); - - try - { - ctx.Add(new ParamItem { Price = value }); - await Assert.ThrowsAsync(() => ctx.SaveChangesAsync()); - } - finally - { - await DropItemsTableAsync(ctx, p, s); - } - } -} diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/ParametricDecimalContext.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/ParametricDecimalContext.cs deleted file mode 100644 index 4f4f26be..00000000 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/ParametricDecimalContext.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -namespace EntityFrameworkCore.Ydb.FunctionalTests.Query; - -public sealed class ParametricDecimalContext : DbContext -{ - private readonly int _p; - private readonly int _s; - - public ParametricDecimalContext(DbContextOptions options, int p, int s) - : base(options) - { - _p = p; - _s = s; - } - - public DbSet Items => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) => - modelBuilder.Entity(b => - { - b.ToTable($"Items_{_p}_{_s}"); - b.HasKey(x => x.Id); - b.Property(x => x.Price).HasPrecision(_p, _s); - }); - - internal sealed class CacheKeyFactory : IModelCacheKeyFactory - { - public object Create(DbContext context, bool designTime) - { - var ctx = (ParametricDecimalContext)context; - return (context.GetType(), designTime, ctx._p, ctx._s); - } - } -} - -public sealed class ParamItem -{ - public int Id { get; set; } - public decimal Price { get; set; } -}