Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f06f8e2
test: add EF decimal parameterization test
LiamHamsters Sep 3, 2025
7dc882c
fix lint
LiamHamsters Sep 3, 2025
2d391b0
test: decimal mapping (22,9 & 30,10)
LiamHamsters Sep 4, 2025
7e86899
fix lint
LiamHamsters Sep 4, 2025
a8dcb20
test: decimal param round-trip (22,9 & 30,10)
LiamHamsters Sep 5, 2025
859cc3b
test: decimal 22,9/30,10 round-trip; close connections
LiamHamsters Sep 6, 2025
351293a
fix lint
LiamHamsters Sep 6, 2025
5ccf50c
test(ef): isolate DB to /ef-tests
LiamHamsters Sep 7, 2025
e1a5a30
test: fix decimal parameter mapping
LiamHamsters Sep 7, 2025
934ee53
chore(test): final polish of EF YDB decimal tests
LiamHamsters Sep 8, 2025
e959dbd
Decimal mapping: fix parameter Precision/Scale in YdbDecimalTypeMappi…
LiamHamsters Sep 8, 2025
cbdf698
fix(decimal): honor precision/scale in YdbTypeMappingSource
LiamHamsters Sep 8, 2025
f8b1a3a
fix lint
LiamHamsters Sep 8, 2025
7690920
fix
LiamHamsters Sep 8, 2025
46be20d
decimal: cache Decimal type mappings with ConcurrentDictionary
LiamHamsters Sep 9, 2025
f02d583
fix(decimal): round values to scale in ConfigureParameter to avoid ov…
LiamHamsters Sep 10, 2025
a1ff208
test(decimal): expand AdoLikeCases and OverflowCases with more scenarios
LiamHamsters Sep 10, 2025
12dc451
checking logs
LiamHamsters Sep 10, 2025
1f5355e
checking
LiamHamsters Sep 11, 2025
d194d84
fix lint
LiamHamsters Sep 11, 2025
aa02060
fix
LiamHamsters Sep 11, 2025
292fc7d
try
LiamHamsters Sep 12, 2025
c385bbe
Restore tree to d194d84
LiamHamsters Sep 12, 2025
2231913
enable_parameterized_decimal to CI Efcore-tests
LiamHamsters Sep 12, 2025
28277ea
last fix
LiamHamsters Sep 12, 2025
4189555
Update DecimalParameterizedYdbTheoryTest.cs
KirillKurdyukov Sep 12, 2025
7cd18a3
made it easier
LiamHamsters Sep 12, 2025
46cfdc0
change
LiamHamsters Sep 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
ports: [ "2135:2135", "2136:2136", "8765:8765" ]
env:
YDB_LOCAL_SURVIVE_RESTART: true
YDB_FEATURE_FLAGS: enable_parameterized_decimal,enable_table_datetime64
options: '--name ydb-local -h localhost'

steps:
Expand Down
1 change: 1 addition & 0 deletions src/EFCore.Ydb/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Fixed Decimal precision/scale mapping in EF provider.
- Supported Guid (Uuid YDB type).
- PrivateAssets="none" is set to flow the EF Core analyzer to users referencing this package [issue](https://github.com/aspnet/EntityFrameworkCore/pull/11350).

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Data.Common;
using Microsoft.EntityFrameworkCore.Storage;

namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping;
Expand Down Expand Up @@ -29,4 +30,15 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
protected override string ProcessStoreType(
RelationalTypeMappingParameters parameters, string storeType, string storeTypeNameBase
) => $"Decimal({parameters.Precision ?? DefaultPrecision}, {parameters.Scale ?? DefaultScale})";

protected override void ConfigureParameter(DbParameter parameter)
{
base.ConfigureParameter(parameter);

if (Precision is { } p)
parameter.Precision = (byte)p;

if (Scale is { } s)
parameter.Scale = (byte)s;
}
}
15 changes: 11 additions & 4 deletions src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Text.Json;
Expand All @@ -13,6 +14,8 @@ public sealed class YdbTypeMappingSource(
RelationalTypeMappingSourceDependencies relationalDependencies
) : RelationalTypeMappingSource(dependencies, relationalDependencies)
{
private static readonly ConcurrentDictionary<RelationalTypeMappingInfo, RelationalTypeMapping> DecimalCache = new();

#region Mappings

private static readonly YdbBoolTypeMapping Bool = YdbBoolTypeMapping.Default;
Expand Down Expand Up @@ -66,8 +69,6 @@ RelationalTypeMappingSourceDependencies relationalDependencies
{ "Float", [Float] },
{ "Double", [Double] },

{ "Decimal", [Decimal] },

{ "Guid", [Guid] },

{ "Date", [Date] },
Expand Down Expand Up @@ -97,7 +98,6 @@ RelationalTypeMappingSourceDependencies relationalDependencies

{ typeof(float), Float },
{ typeof(double), Double },
{ typeof(decimal), Decimal },

{ typeof(Guid), Guid },

Expand All @@ -111,7 +111,14 @@ RelationalTypeMappingSourceDependencies relationalDependencies
};

protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
=> base.FindMapping(mappingInfo) ?? FindBaseMapping(mappingInfo)?.Clone(mappingInfo);
{
if (mappingInfo.ClrType == typeof(decimal))
{
return DecimalCache.GetOrAdd(mappingInfo, static mi => Decimal.Clone(mi));
}

return base.FindMapping(mappingInfo) ?? FindBaseMapping(mappingInfo)?.Clone(mappingInfo);
}

private static RelationalTypeMapping? FindBaseMapping(in RelationalTypeMappingInfo mappingInfo)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.TestUtilities;

namespace EntityFrameworkCore.Ydb.FunctionalTests.Query;

public class DecimalParameterQueryYdbFixture : SharedStoreFixtureBase<DecimalParameterQueryYdbFixture.TestContext>
{
protected override string StoreName => "DecimalParameterTest";

protected override ITestStoreFactory TestStoreFactory => YdbTestStoreFactory.Instance;

public class TestContext(DbContextOptions options) : DbContext(options);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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<DecimalParameterQueryYdbFixture>
{
private DbContextOptions<ParametricDecimalContext> BuildOptions()
{
using var baseCtx = fixture.CreateContext();
var cs = baseCtx.Database.GetDbConnection().ConnectionString;

return new DbContextOptionsBuilder<ParametricDecimalContext>()
.UseYdb(cs)
.ReplaceService<IModelCacheKeyFactory, ParametricDecimalContext.CacheKeyFactory>()
.Options;
}

public static IEnumerable<object[]> 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<object[]> 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<ISqlGenerationHelper>();
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<IRelationalTypeMappingSource>();
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<DbUpdateException>(() => ctx.SaveChangesAsync());
}
finally
{
await DropItemsTableAsync(ctx, p, s);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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<ParametricDecimalContext> options, int p, int s)
: base(options)
{
_p = p;
_s = s;
}

public DbSet<ParamItem> Items => Set<ParamItem>();

protected override void OnModelCreating(ModelBuilder modelBuilder) =>
modelBuilder.Entity<ParamItem>(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; }
}
Loading