From fcce943e19852e6da0e7b6b7980af62b7deb4410 Mon Sep 17 00:00:00 2001 From: Nikita Mulyukin Date: Thu, 9 Oct 2025 18:12:40 +0300 Subject: [PATCH 01/11] fix bug https://github.com/ydb-platform/ydb-dotnet-sdk/issues/531 --- .../Query/Internal/YdbSqlExpressionFactory.cs | 30 ++++++++++++++++++- .../Internal/Mapping/YdbDecimalTypeMapping.cs | 28 +++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs index bdf3eb3c..eaccebff 100644 --- a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs +++ b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs @@ -1,8 +1,10 @@ +using System; +using System.Data; using System.Diagnostics.CodeAnalysis; +using EntityFrameworkCore.Ydb.Storage.Internal.Mapping; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; - namespace EntityFrameworkCore.Ydb.Query.Internal; public class YdbSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) : SqlExpressionFactory(dependencies) @@ -10,4 +12,30 @@ public class YdbSqlExpressionFactory(SqlExpressionFactoryDependencies dependenci [return: NotNullIfNotNull("sqlExpression")] public override SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, RelationalTypeMapping? typeMapping) => base.ApplyTypeMapping(sqlExpression, typeMapping); + + public override SqlExpression Coalesce(SqlExpression left, SqlExpression right, + RelationalTypeMapping? typeMapping = null) + { + // For .Sum(x => x.Decimal) EF generates coalesce(sum(x.Decimal), 0.0)) because SUM must have value + var funcExpression = left as SqlFunctionExpression; + var constExpression = right as SqlConstantExpression; + + if (funcExpression != null && constExpression != null && constExpression.TypeMapping != null + && + funcExpression.Name.Equals("SUM", StringComparison.OrdinalIgnoreCase) + && + constExpression.TypeMapping.DbType == DbType.Decimal + && + constExpression.Value != null) + { + var correctRight = new SqlConstantExpression(constExpression.Value, + YdbDecimalTypeMapping.WithMaxPrecision); // in the feature change static max precision/scale to + // to dynamically created correct precision/scale + // it depends on db scheme and can not correctly define only in code + + return base.Coalesce(left, correctRight, typeMapping); + } + + return base.Coalesce(left, right, 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..b6ecfced 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs @@ -7,9 +7,32 @@ public class YdbDecimalTypeMapping : DecimalTypeMapping { private const byte DefaultPrecision = 22; private const byte DefaultScale = 9; + + private const byte MaxPrecision = 35; public new static YdbDecimalTypeMapping Default => new(); + static YdbDecimalTypeMapping() + { + WithMaxPrecision = GetWithMaxPrecision(); + } + + public static YdbDecimalTypeMapping WithMaxPrecision { get; } + + private static YdbDecimalTypeMapping GetWithMaxPrecision() + { + var result = new YdbDecimalTypeMapping(new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + typeof(decimal)), + storeType: "Decimal", + dbType: System.Data.DbType.Decimal, + precision: MaxPrecision, + scale: DefaultScale) + ); + + return result; + } + public YdbDecimalTypeMapping() : this( new RelationalTypeMappingParameters( new CoreTypeMappingParameters(typeof(decimal)), @@ -41,4 +64,9 @@ protected override void ConfigureParameter(DbParameter parameter) if (Scale is { } s) parameter.Scale = (byte)s; } + + protected override string GenerateNonNullSqlLiteral(object value) + { + return $"Decimal('{base.GenerateNonNullSqlLiteral(value)}', {this.Precision ?? MaxPrecision}, {this.Scale ?? DefaultScale})"; + } } From 4990e45a2a920405e506c08c43935eb706cc32e6 Mon Sep 17 00:00:00 2001 From: Nikita Mulyukin Date: Mon, 13 Oct 2025 20:07:31 +0300 Subject: [PATCH 02/11] correct expression type for long sum aggregate --- .../Translators/YdbQueryableAggregateMethodTranslator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From 066d1dbbd18c55e53897b7d47a92e3cb4438679f Mon Sep 17 00:00:00 2001 From: Nikita Mulyukin Date: Thu, 16 Oct 2025 19:12:05 +0300 Subject: [PATCH 03/11] fix comments --- .../Query/Internal/YdbSqlExpressionFactory.cs | 19 ++++++----- .../Internal/Mapping/YdbDecimalTypeMapping.cs | 34 ++++++------------- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs index eaccebff..87e4388b 100644 --- a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs +++ b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs @@ -17,22 +17,25 @@ public override SqlExpression Coalesce(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) { // For .Sum(x => x.Decimal) EF generates coalesce(sum(x.Decimal), 0.0)) because SUM must have value - var funcExpression = left as SqlFunctionExpression; - var constExpression = right as SqlConstantExpression; - - if (funcExpression != null && constExpression != null && constExpression.TypeMapping != null + + if (left is SqlFunctionExpression funcExpression + && + right is SqlConstantExpression constExpression && constExpression.TypeMapping != null && funcExpression.Name.Equals("SUM", StringComparison.OrdinalIgnoreCase) && + funcExpression.Arguments != null + && constExpression.TypeMapping.DbType == DbType.Decimal && constExpression.Value != null) { + // get column expression for SUM function expression + var columnExpression = funcExpression.Arguments[0] as ColumnExpression; + var correctRight = new SqlConstantExpression(constExpression.Value, - YdbDecimalTypeMapping.WithMaxPrecision); // in the feature change static max precision/scale to - // to dynamically created correct precision/scale - // it depends on db scheme and can not correctly define only in code - + YdbDecimalTypeMapping.GetWithMaxPrecision(columnExpression?.TypeMapping?.Scale)); + return base.Coalesce(left, correctRight, typeMapping); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs index b6ecfced..3fe4d5f6 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs @@ -12,26 +12,15 @@ public class YdbDecimalTypeMapping : DecimalTypeMapping public new static YdbDecimalTypeMapping Default => new(); - static YdbDecimalTypeMapping() - { - WithMaxPrecision = GetWithMaxPrecision(); - } - - public static YdbDecimalTypeMapping WithMaxPrecision { get; } - - private static YdbDecimalTypeMapping GetWithMaxPrecision() - { - var result = new YdbDecimalTypeMapping(new RelationalTypeMappingParameters( - new CoreTypeMappingParameters( - typeof(decimal)), - storeType: "Decimal", - dbType: System.Data.DbType.Decimal, - precision: MaxPrecision, - scale: DefaultScale) - ); - - return result; - } + public static YdbDecimalTypeMapping GetWithMaxPrecision(int? scale) => + new(new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + typeof(decimal)), + storeType: "Decimal", + dbType: System.Data.DbType.Decimal, + precision: MaxPrecision, + scale: scale ?? DefaultScale) + ); public YdbDecimalTypeMapping() : this( new RelationalTypeMappingParameters( @@ -65,8 +54,5 @@ protected override void ConfigureParameter(DbParameter parameter) parameter.Scale = (byte)s; } - protected override string GenerateNonNullSqlLiteral(object value) - { - return $"Decimal('{base.GenerateNonNullSqlLiteral(value)}', {this.Precision ?? MaxPrecision}, {this.Scale ?? DefaultScale})"; - } + protected override string GenerateNonNullSqlLiteral(object value) => $"Decimal('{base.GenerateNonNullSqlLiteral(value)}', {this.Precision ?? MaxPrecision}, {this.Scale ?? DefaultScale})"; } From 0fb040c6049ff4de9d8a57830c9b7138db83048f Mon Sep 17 00:00:00 2001 From: Nikita Mulyukin Date: Thu, 16 Oct 2025 21:21:12 +0300 Subject: [PATCH 04/11] fix lint --- .../src/Query/Internal/YdbSqlExpressionFactory.cs | 9 +++++---- .../Storage/Internal/Mapping/YdbDecimalTypeMapping.cs | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs index 87e4388b..cac78a38 100644 --- a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs +++ b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; + namespace EntityFrameworkCore.Ydb.Query.Internal; public class YdbSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) : SqlExpressionFactory(dependencies) @@ -18,8 +19,8 @@ public override SqlExpression Coalesce(SqlExpression left, SqlExpression right, { // For .Sum(x => x.Decimal) EF generates coalesce(sum(x.Decimal), 0.0)) because SUM must have value - if (left is SqlFunctionExpression funcExpression - && + if (left is SqlFunctionExpression funcExpression + && right is SqlConstantExpression constExpression && constExpression.TypeMapping != null && funcExpression.Name.Equals("SUM", StringComparison.OrdinalIgnoreCase) @@ -34,11 +35,11 @@ public override SqlExpression Coalesce(SqlExpression left, SqlExpression right, var columnExpression = funcExpression.Arguments[0] as ColumnExpression; var correctRight = new SqlConstantExpression(constExpression.Value, - YdbDecimalTypeMapping.GetWithMaxPrecision(columnExpression?.TypeMapping?.Scale)); + YdbDecimalTypeMapping.GetWithMaxPrecision(columnExpression?.TypeMapping?.Scale)); return base.Coalesce(left, correctRight, typeMapping); } - + return base.Coalesce(left, right, typeMapping); } } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs index 3fe4d5f6..60fec025 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs @@ -7,7 +7,7 @@ public class YdbDecimalTypeMapping : DecimalTypeMapping { private const byte DefaultPrecision = 22; private const byte DefaultScale = 9; - + private const byte MaxPrecision = 35; public new static YdbDecimalTypeMapping Default => new(); @@ -54,5 +54,6 @@ protected override void ConfigureParameter(DbParameter parameter) parameter.Scale = (byte)s; } - protected override string GenerateNonNullSqlLiteral(object value) => $"Decimal('{base.GenerateNonNullSqlLiteral(value)}', {this.Precision ?? MaxPrecision}, {this.Scale ?? DefaultScale})"; + protected override string GenerateNonNullSqlLiteral(object value) => + $"Decimal('{base.GenerateNonNullSqlLiteral(value)}', {Precision ?? DefaultScale}, {Scale ?? DefaultScale})"; } From 974a99f67b4b49363c15c3ee1de70f6008680365 Mon Sep 17 00:00:00 2001 From: Nikita Mulyukin Date: Thu, 16 Oct 2025 21:23:16 +0300 Subject: [PATCH 05/11] fix typo --- .../src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs index 60fec025..bed36dc0 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs @@ -55,5 +55,5 @@ protected override void ConfigureParameter(DbParameter parameter) } protected override string GenerateNonNullSqlLiteral(object value) => - $"Decimal('{base.GenerateNonNullSqlLiteral(value)}', {Precision ?? DefaultScale}, {Scale ?? DefaultScale})"; + $"Decimal('{base.GenerateNonNullSqlLiteral(value)}', {Precision ?? DefaultPrecision}, {Scale ?? DefaultScale})"; } From eace600e2e44488b04da7e6d5f43cc7a9fb5c15d Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Mon, 20 Oct 2025 17:59:19 +0300 Subject: [PATCH 06/11] Fixed bug: incompatible coalesce types #534 --- src/EFCore.Ydb/CHANGELOG.md | 1 + .../Query/Internal/YdbSqlExpressionFactory.cs | 5 +- .../Internal/Mapping/YdbDecimalTypeMapping.cs | 15 +- .../Storage/Internal/YdbTypeMappingSource.cs | 7 +- .../Query/DecimalParameterQueryYdbFixture.cs | 14 -- .../Query/DecimalParameterizedYdbTest.cs | 136 ++++++++++++++++++ .../DecimalParameterizedYdbTheoryTest.cs | 106 -------------- .../Query/ParametricDecimalContext.cs | 42 ------ 8 files changed, 147 insertions(+), 179 deletions(-) delete mode 100644 src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterQueryYdbFixture.cs create mode 100644 src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs delete mode 100644 src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTheoryTest.cs delete mode 100644 src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/ParametricDecimalContext.cs diff --git a/src/EFCore.Ydb/CHANGELOG.md b/src/EFCore.Ydb/CHANGELOG.md index cd153026..d7311e73 100644 --- a/src/EFCore.Ydb/CHANGELOG.md +++ b/src/EFCore.Ydb/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed bug: incompatible coalesce types ([#534](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/YdbSqlExpressionFactory.cs b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs index cac78a38..73002abb 100644 --- a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs +++ b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs @@ -18,10 +18,9 @@ public override SqlExpression Coalesce(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) { // For .Sum(x => x.Decimal) EF generates coalesce(sum(x.Decimal), 0.0)) because SUM must have value - if (left is SqlFunctionExpression funcExpression && - right is SqlConstantExpression constExpression && constExpression.TypeMapping != null + right is SqlConstantExpression { TypeMapping: not null } constExpression && funcExpression.Name.Equals("SUM", StringComparison.OrdinalIgnoreCase) && @@ -35,7 +34,7 @@ public override SqlExpression Coalesce(SqlExpression left, SqlExpression right, var columnExpression = funcExpression.Arguments[0] as ColumnExpression; var correctRight = new SqlConstantExpression(constExpression.Value, - YdbDecimalTypeMapping.GetWithMaxPrecision(columnExpression?.TypeMapping?.Scale)); + YdbDecimalTypeMapping.CreateMaxPrecision(columnExpression?.TypeMapping?.Scale)); return base.Coalesce(left, correctRight, typeMapping); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs index bed36dc0..5df84208 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs @@ -7,22 +7,21 @@ public class YdbDecimalTypeMapping : DecimalTypeMapping { private const byte DefaultPrecision = 22; private const byte DefaultScale = 9; - private const byte MaxPrecision = 35; public new static YdbDecimalTypeMapping Default => new(); - public static YdbDecimalTypeMapping GetWithMaxPrecision(int? scale) => - new(new RelationalTypeMappingParameters( - new CoreTypeMappingParameters( - typeof(decimal)), + public static YdbDecimalTypeMapping CreateMaxPrecision(int? scale) => new( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(decimal)), storeType: "Decimal", dbType: System.Data.DbType.Decimal, precision: MaxPrecision, - scale: scale ?? DefaultScale) - ); + scale: scale ?? DefaultScale + ) + ); - public YdbDecimalTypeMapping() : this( + private YdbDecimalTypeMapping() : this( new RelationalTypeMappingParameters( new CoreTypeMappingParameters(typeof(decimal)), storeType: "Decimal", 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..719fdb45 --- /dev/null +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs @@ -0,0 +1,136 @@ +using EntityFrameworkCore.Ydb.Extensions; +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) + .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 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 ctx.Database.EnsureDeletedAsync(); + } + } + + [Theory] + [MemberData(nameof(OverflowCases))] + public async Task Should_ThrowOverflow_When_ValueExceedsPrecisionOrScale(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 ctx.Database.EnsureDeletedAsync(); + } + } + + [Theory] + [MemberData(nameof(SuccessCases))] + public async Task Should_SumDecimal_When_ValueFitsPrecisionAndScale(int p, int s, decimal value) + { + const int multiplier = 5; + await using var ctx = NewCtx(p, s); + 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.SumAsync(x => x.Price); + + 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; } -} From 8bdb7783c5731c2e46eb5a41d8650ecc95ba2567 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Mon, 20 Oct 2025 18:00:40 +0300 Subject: [PATCH 07/11] fix CHANGELOG.md --- src/EFCore.Ydb/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.Ydb/CHANGELOG.md b/src/EFCore.Ydb/CHANGELOG.md index d7311e73..c1633f3f 100644 --- a/src/EFCore.Ydb/CHANGELOG.md +++ b/src/EFCore.Ydb/CHANGELOG.md @@ -1,4 +1,4 @@ -- Fixed bug: incompatible coalesce types ([#534](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/531)). +- 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). From 876a259677b35cd2f686b7141022fb9cc3beb7a5 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Mon, 20 Oct 2025 18:47:13 +0300 Subject: [PATCH 08/11] fix --- .../Query/Internal/YdbSqlExpressionFactory.cs | 31 ------------------- .../Internal/Mapping/YdbDecimalTypeMapping.cs | 11 ------- .../Query/DecimalParameterizedYdbTest.cs | 3 +- 3 files changed, 2 insertions(+), 43 deletions(-) diff --git a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs index 73002abb..bdf3eb3c 100644 --- a/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs +++ b/src/EFCore.Ydb/src/Query/Internal/YdbSqlExpressionFactory.cs @@ -1,7 +1,4 @@ -using System; -using System.Data; using System.Diagnostics.CodeAnalysis; -using EntityFrameworkCore.Ydb.Storage.Internal.Mapping; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; @@ -13,32 +10,4 @@ public class YdbSqlExpressionFactory(SqlExpressionFactoryDependencies dependenci [return: NotNullIfNotNull("sqlExpression")] public override SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, RelationalTypeMapping? typeMapping) => base.ApplyTypeMapping(sqlExpression, typeMapping); - - public override SqlExpression Coalesce(SqlExpression left, SqlExpression right, - RelationalTypeMapping? typeMapping = null) - { - // For .Sum(x => x.Decimal) EF generates coalesce(sum(x.Decimal), 0.0)) because SUM must have value - if (left is SqlFunctionExpression funcExpression - && - right is SqlConstantExpression { TypeMapping: not null } constExpression - && - funcExpression.Name.Equals("SUM", StringComparison.OrdinalIgnoreCase) - && - funcExpression.Arguments != null - && - constExpression.TypeMapping.DbType == DbType.Decimal - && - constExpression.Value != null) - { - // get column expression for SUM function expression - var columnExpression = funcExpression.Arguments[0] as ColumnExpression; - - var correctRight = new SqlConstantExpression(constExpression.Value, - YdbDecimalTypeMapping.CreateMaxPrecision(columnExpression?.TypeMapping?.Scale)); - - return base.Coalesce(left, correctRight, typeMapping); - } - - return base.Coalesce(left, right, typeMapping); - } } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs index 5df84208..4149a0f5 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs @@ -7,20 +7,9 @@ public class YdbDecimalTypeMapping : DecimalTypeMapping { private const byte DefaultPrecision = 22; private const byte DefaultScale = 9; - private const byte MaxPrecision = 35; public new static YdbDecimalTypeMapping Default => new(); - public static YdbDecimalTypeMapping CreateMaxPrecision(int? scale) => new( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(decimal)), - storeType: "Decimal", - dbType: System.Data.DbType.Decimal, - precision: MaxPrecision, - scale: scale ?? DefaultScale - ) - ); - private YdbDecimalTypeMapping() : this( new RelationalTypeMappingParameters( new CoreTypeMappingParameters(typeof(decimal)), diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs index 719fdb45..96bd083b 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs @@ -12,6 +12,7 @@ private static DbContextOptions BuildOptions() => new DbContextOptionsBuilder() .UseYdb("Host=localhost;Port=2136") .EnableServiceProviderCaching(false) + .LogTo(Console.WriteLine) .Options; public static TheoryData SuccessCases => new() @@ -99,7 +100,7 @@ public async Task Should_SumDecimal_When_ValueFitsPrecisionAndScale(int p, int s for (var i = 0; i < multiplier; i++) ctx.Add(new ParamItem { Price = value }); await ctx.SaveChangesAsync(); - var got = await ctx.Items.SumAsync(x => x.Price); + var got = await ctx.Items.Select(x => x.Price).SumAsync(); Assert.Equal(value * multiplier, got); From 1e287078e0d51fa8def1e3f24cedf82af5ca574b Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Mon, 20 Oct 2025 19:11:42 +0300 Subject: [PATCH 09/11] fix --- .../Query/DecimalParameterizedYdbTest.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs index 96bd083b..14c11df1 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs @@ -48,17 +48,13 @@ public async Task Should_RoundtripDecimal_When_ValueFitsPrecisionAndScale(int p, { 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))!; @@ -97,7 +93,7 @@ public async Task Should_SumDecimal_When_ValueFitsPrecisionAndScale(int p, int s await ctx.Database.EnsureCreatedAsync(); try { - for (var i = 0; i < multiplier; i++) + 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(); @@ -123,7 +119,7 @@ public sealed class ParametricDecimalContext(DbContextOptions modelBuilder.Entity(b => { - b.ToTable($"Items_{p}_{s}"); + b.ToTable($"Items_{p}_{s}_{Guid.NewGuid():N}"); b.HasKey(x => x.Id); b.Property(x => x.Price).HasPrecision(p, s); }); From 6193e616dc36de7e6415894a3f59983b9a81c742 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Mon, 20 Oct 2025 20:43:38 +0300 Subject: [PATCH 10/11] fix tests --- .../Query/DecimalParameterizedYdbTest.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs index 14c11df1..0d09d587 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs @@ -1,4 +1,5 @@ using EntityFrameworkCore.Ydb.Extensions; +using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; @@ -46,7 +47,10 @@ private static DbContextOptions BuildOptions() => [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 { @@ -71,7 +75,10 @@ public async Task Should_RoundtripDecimal_When_ValueFitsPrecisionAndScale(int p, [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 { @@ -88,8 +95,11 @@ public async Task Should_ThrowOverflow_When_ValueExceedsPrecisionOrScale(int p, [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 { @@ -119,7 +129,7 @@ public sealed class ParametricDecimalContext(DbContextOptions modelBuilder.Entity(b => { - b.ToTable($"Items_{p}_{s}_{Guid.NewGuid():N}"); + b.ToTable($"Items_{p}_{s}"); b.HasKey(x => x.Id); b.Property(x => x.Price).HasPrecision(p, s); }); @@ -130,4 +140,6 @@ public sealed class ParamItem public int Id { get; init; } public decimal Price { get; init; } } + + public Task DisposeAsync() => throw new NotImplementedException(); } From 2ca422284ab9e6da64c0896c9138500d71a7d667 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Mon, 20 Oct 2025 20:58:20 +0300 Subject: [PATCH 11/11] fix test --- .../Query/DecimalParameterizedYdbTest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs index 0d09d587..a5522380 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs @@ -140,6 +140,4 @@ public sealed class ParamItem public int Id { get; init; } public decimal Price { get; init; } } - - public Task DisposeAsync() => throw new NotImplementedException(); }