Skip to content

Commit fed905b

Browse files
authored
test: add EF decimal parameterization test (#522)
1 parent fa14989 commit fed905b

File tree

7 files changed

+187
-4
lines changed

7 files changed

+187
-4
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ jobs:
6565
ports: [ "2135:2135", "2136:2136", "8765:8765" ]
6666
env:
6767
YDB_LOCAL_SURVIVE_RESTART: true
68+
YDB_FEATURE_FLAGS: enable_parameterized_decimal,enable_table_datetime64
6869
options: '--name ydb-local -h localhost'
6970

7071
steps:

src/EFCore.Ydb/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
- Fixed Decimal precision/scale mapping in EF provider.
12
- Supported Guid (Uuid YDB type).
23
- PrivateAssets="none" is set to flow the EF Core analyzer to users referencing this package [issue](https://github.com/aspnet/EntityFrameworkCore/pull/11350).
34

src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDecimalTypeMapping.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Data.Common;
12
using Microsoft.EntityFrameworkCore.Storage;
23

34
namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping;
@@ -29,4 +30,15 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
2930
protected override string ProcessStoreType(
3031
RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase
3132
) => $"Decimal({parameters.Precision ?? DefaultPrecision}, {parameters.Scale ?? DefaultScale})";
33+
34+
protected override void ConfigureParameter(DbParameter parameter)
35+
{
36+
base.ConfigureParameter(parameter);
37+
38+
if (Precision is { } p)
39+
parameter.Precision = (byte)p;
40+
41+
if (Scale is { } s)
42+
parameter.Scale = (byte)s;
43+
}
3244
}

src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.Data;
45
using System.Text.Json;
@@ -13,6 +14,8 @@ public sealed class YdbTypeMappingSource(
1314
RelationalTypeMappingSourceDependencies relationalDependencies
1415
) : RelationalTypeMappingSource(dependencies, relationalDependencies)
1516
{
17+
private static readonly ConcurrentDictionary<RelationalTypeMappingInfo, RelationalTypeMapping> DecimalCache = new();
18+
1619
#region Mappings
1720

1821
private static readonly YdbBoolTypeMapping Bool = YdbBoolTypeMapping.Default;
@@ -66,8 +69,6 @@ RelationalTypeMappingSourceDependencies relationalDependencies
6669
{ "Float", [Float] },
6770
{ "Double", [Double] },
6871

69-
{ "Decimal", [Decimal] },
70-
7172
{ "Guid", [Guid] },
7273

7374
{ "Date", [Date] },
@@ -97,7 +98,6 @@ RelationalTypeMappingSourceDependencies relationalDependencies
9798

9899
{ typeof(float), Float },
99100
{ typeof(double), Double },
100-
{ typeof(decimal), Decimal },
101101

102102
{ typeof(Guid), Guid },
103103

@@ -111,7 +111,14 @@ RelationalTypeMappingSourceDependencies relationalDependencies
111111
};
112112

113113
protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
114-
=> base.FindMapping(mappingInfo) ?? FindBaseMapping(mappingInfo)?.Clone(mappingInfo);
114+
{
115+
if (mappingInfo.ClrType == typeof(decimal))
116+
{
117+
return DecimalCache.GetOrAdd(mappingInfo, static mi => Decimal.Clone(mi));
118+
}
119+
120+
return base.FindMapping(mappingInfo) ?? FindBaseMapping(mappingInfo)?.Clone(mappingInfo);
121+
}
115122

116123
private static RelationalTypeMapping? FindBaseMapping(in RelationalTypeMappingInfo mappingInfo)
117124
{
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.EntityFrameworkCore.TestUtilities;
4+
5+
namespace EntityFrameworkCore.Ydb.FunctionalTests.Query;
6+
7+
public class DecimalParameterQueryYdbFixture : SharedStoreFixtureBase<DecimalParameterQueryYdbFixture.TestContext>
8+
{
9+
protected override string StoreName => "DecimalParameterTest";
10+
11+
protected override ITestStoreFactory TestStoreFactory => YdbTestStoreFactory.Instance;
12+
13+
public class TestContext(DbContextOptions options) : DbContext(options);
14+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using EntityFrameworkCore.Ydb.Extensions;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.EntityFrameworkCore.Infrastructure;
4+
using Microsoft.EntityFrameworkCore.Storage;
5+
using Xunit;
6+
7+
namespace EntityFrameworkCore.Ydb.FunctionalTests.Query;
8+
9+
public class DecimalParameterizedYdbTheoryTest(DecimalParameterQueryYdbFixture fixture)
10+
: IClassFixture<DecimalParameterQueryYdbFixture>
11+
{
12+
private DbContextOptions<ParametricDecimalContext> BuildOptions()
13+
{
14+
using var baseCtx = fixture.CreateContext();
15+
var cs = baseCtx.Database.GetDbConnection().ConnectionString;
16+
17+
return new DbContextOptionsBuilder<ParametricDecimalContext>()
18+
.UseYdb(cs)
19+
.ReplaceService<IModelCacheKeyFactory, ParametricDecimalContext.CacheKeyFactory>()
20+
.Options;
21+
}
22+
23+
public static IEnumerable<object[]> AdoLikeCases =>
24+
[
25+
[22, 9, 1.23456789m],
26+
[30, 10, 123.4567890123m],
27+
[18, 2, 12345678.91m],
28+
[10, 0, 9999999999m],
29+
[22, 9, -0.123456789m],
30+
[5, 2, 12.34m],
31+
[30, 10, 0.0000000001m]
32+
];
33+
34+
public static IEnumerable<object[]> OverflowCases =>
35+
[
36+
[15, 2, 123456789012345.67m],
37+
[10, 0, 12345678901m],
38+
[22, 9, 1.0000000001m],
39+
[18, 2, 1.239m],
40+
[18, 2, 100000000000000000m],
41+
[22, 9, 12345678901234567890.123456789m],
42+
[22, 9, -12345678901234567890.123456789m],
43+
[4, 2, 123.456m],
44+
[1, 0, 10m],
45+
[5, 0, 100000m]
46+
];
47+
48+
private ParametricDecimalContext NewCtx(int p, int s)
49+
=> new(BuildOptions(), p, s);
50+
51+
private static Task DropItemsTableAsync(DbContext ctx, int p, int s)
52+
{
53+
var helper = ctx.GetService<ISqlGenerationHelper>();
54+
var tableName = $"Items_{p}_{s}";
55+
var sql = $"DROP TABLE IF EXISTS {helper.DelimitIdentifier(tableName)}";
56+
57+
return ctx.Database.ExecuteSqlRawAsync(sql);
58+
}
59+
60+
[Theory]
61+
[MemberData(nameof(AdoLikeCases))]
62+
public async Task Decimal_roundtrips_or_rounds_like_ado(int p, int s, decimal value)
63+
{
64+
await using var ctx = NewCtx(p, s);
65+
await ctx.Database.EnsureCreatedAsync();
66+
67+
try
68+
{
69+
var e = new ParamItem { Price = value };
70+
ctx.Add(e);
71+
await ctx.SaveChangesAsync();
72+
73+
var got = await ctx.Items.SingleAsync(x => x.Id == e.Id);
74+
75+
Assert.Equal(value, got.Price);
76+
77+
var tms = ctx.GetService<IRelationalTypeMappingSource>();
78+
var et = ctx.Model.FindEntityType(typeof(ParamItem))!;
79+
var prop = et.FindProperty(nameof(ParamItem.Price))!;
80+
var mapping = tms.FindMapping(prop)!;
81+
Assert.Equal($"Decimal({p}, {s})", mapping.StoreType);
82+
}
83+
finally
84+
{
85+
await DropItemsTableAsync(ctx, p, s);
86+
}
87+
}
88+
89+
[Theory]
90+
[MemberData(nameof(OverflowCases))]
91+
public async Task Decimal_overflow_bubbles_up(int p, int s, decimal value)
92+
{
93+
await using var ctx = NewCtx(p, s);
94+
await ctx.Database.EnsureCreatedAsync();
95+
96+
try
97+
{
98+
ctx.Add(new ParamItem { Price = value });
99+
await Assert.ThrowsAsync<DbUpdateException>(() => ctx.SaveChangesAsync());
100+
}
101+
finally
102+
{
103+
await DropItemsTableAsync(ctx, p, s);
104+
}
105+
}
106+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.Infrastructure;
3+
4+
namespace EntityFrameworkCore.Ydb.FunctionalTests.Query;
5+
6+
public sealed class ParametricDecimalContext : DbContext
7+
{
8+
private readonly int _p;
9+
private readonly int _s;
10+
11+
public ParametricDecimalContext(DbContextOptions<ParametricDecimalContext> options, int p, int s)
12+
: base(options)
13+
{
14+
_p = p;
15+
_s = s;
16+
}
17+
18+
public DbSet<ParamItem> Items => Set<ParamItem>();
19+
20+
protected override void OnModelCreating(ModelBuilder modelBuilder) =>
21+
modelBuilder.Entity<ParamItem>(b =>
22+
{
23+
b.ToTable($"Items_{_p}_{_s}");
24+
b.HasKey(x => x.Id);
25+
b.Property(x => x.Price).HasPrecision(_p, _s);
26+
});
27+
28+
internal sealed class CacheKeyFactory : IModelCacheKeyFactory
29+
{
30+
public object Create(DbContext context, bool designTime)
31+
{
32+
var ctx = (ParametricDecimalContext)context;
33+
return (context.GetType(), designTime, ctx._p, ctx._s);
34+
}
35+
}
36+
}
37+
38+
public sealed class ParamItem
39+
{
40+
public int Id { get; set; }
41+
public decimal Price { get; set; }
42+
}

0 commit comments

Comments
 (0)