Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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 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,13 @@ 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);
var p = (byte)(Precision ?? DefaultPrecision);
var s = (byte)(Scale ?? DefaultScale);
parameter.Precision = p;
parameter.Scale = s;
}
}
11 changes: 10 additions & 1 deletion src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,16 @@ RelationalTypeMappingSourceDependencies relationalDependencies
};

protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
=> base.FindMapping(mappingInfo) ?? FindBaseMapping(mappingInfo)?.Clone(mappingInfo);
{
if (mappingInfo.ClrType == typeof(decimal)
|| string.Equals(mappingInfo.StoreTypeNameBase, "Decimal", StringComparison.OrdinalIgnoreCase)
|| (mappingInfo.StoreTypeName?.StartsWith("Decimal", StringComparison.OrdinalIgnoreCase) ?? false))
{
return Decimal.Clone(mappingInfo);
}

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,42 @@
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)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ItemDefault>(b =>
{
b.HasKey(x => x.Id);
b.Property(x => x.Price);
});

modelBuilder.Entity<ItemExplicit>(b =>
{
b.HasKey(x => x.Id);
b.Property(x => x.Price).HasPrecision(22, 9);
});
}
}
}

public class ItemDefault
{
public int Id { get; set; }
public decimal Price { get; set; }
}

public class ItemExplicit
{
public int Id { get; set; }
public decimal Price { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using EntityFrameworkCore.Ydb.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Xunit;

namespace EntityFrameworkCore.Ydb.FunctionalTests.Query;

public class DecimalParameterYdbTest(DecimalParameterQueryYdbFixture fixture)
: IClassFixture<DecimalParameterQueryYdbFixture>
{
private DecimalParameterQueryYdbFixture Fixture { get; } = fixture;

[ConditionalFact]
public async Task Parameter_decimal_uses_default_22_9_and_roundtrips()
{
await using var ctx = Fixture.CreateContext();
await ctx.Database.EnsureCreatedAsync();

var v = 1.23456789m;
ctx.Add(new ItemDefault { Price = v });
await ctx.SaveChangesAsync();

var got = await ctx.Set<ItemDefault>().SingleAsync(x => x.Price == v);
Assert.Equal(v, got.Price);
}

[ConditionalFact]
public async Task Parameter_decimal_respects_explicit_22_9_and_roundtrips()
{
await using var ctx = Fixture.CreateContext();
await ctx.Database.EnsureCreatedAsync();

var v = 123.456789012m;
ctx.Add(new ItemExplicit { Price = v });
await ctx.SaveChangesAsync();

var got = await ctx.Set<ItemExplicit>().SingleAsync(x => x.Price == v);
Assert.Equal(v, got.Price);
}

[ConditionalFact]
public async Task Decimal_out_of_range_bubbles_up()
{
await using var ctx = Fixture.CreateContext();
await ctx.Database.EnsureCreatedAsync();

var tooBig = new ItemExplicit { Price = 10_000_000_000_000m };
ctx.Add(tooBig);

var ex = await Assert.ThrowsAsync<DbUpdateException>(() => ctx.SaveChangesAsync());
Assert.Contains("Decimal", ex.InnerException?.Message ?? "");
}

[Fact]
public void Type_mapping_default_decimal_is_22_9()
{
using var ctx = Fixture.CreateContext();
var tms = ctx.GetService<IRelationalTypeMappingSource>();
var mapping = tms.FindMapping(typeof(decimal))!;
Assert.Equal("Decimal(22, 9)", mapping.StoreType);
}

[Fact]
public void Type_mapping_custom_decimal_is_30_10()
{
var opts = new DbContextOptionsBuilder<MappingOnlyContext>()
.UseYdb("Host=localhost;Database=/local")
.Options;

using var ctx = new MappingOnlyContext(opts);
var tms = ctx.GetService<IRelationalTypeMappingSource>();

var et = ctx.Model.FindEntityType(typeof(MappingEntity))!;
var prop = et.FindProperty(nameof(MappingEntity.Price))!;
var mapping = tms.FindMapping(prop)!;

Assert.Equal("Decimal(30, 10)", mapping.StoreType);
}

[ConditionalFact]
public async Task Parameter_decimal_respects_custom_30_10_and_roundtrips_if_supported()
{
var cs = Environment.GetEnvironmentVariable("YDB_EF_CONN");
if (string.IsNullOrWhiteSpace(cs)) return;

var opts = new DbContextOptionsBuilder<MappingOnlyContext>()
.UseYdb(cs)
.Options;

await using var ctx = new MappingOnlyContext(opts);

try
{
await ctx.Database.EnsureCreatedAsync();
}
catch (Exception ex) when (ex.ToString()
.Contains("EnableParameterizedDecimal", StringComparison.OrdinalIgnoreCase))
{
return;
}

var v = 123.4567890123m;
ctx.Add(new MappingEntity { Price = v });
await ctx.SaveChangesAsync();

var got = await ctx.Set<MappingEntity>().SingleAsync(x => x.Price == v);
Assert.Equal(v, got.Price);
}

private sealed class MappingOnlyContext(DbContextOptions<MappingOnlyContext> options) : DbContext(options)
{
protected override void OnModelCreating(ModelBuilder b)
=> b.Entity<MappingEntity>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Price).HasPrecision(30, 10);
});
}

private sealed class MappingEntity
{
public int Id { get; set; }
public decimal Price { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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)
.Options;
}

public static IEnumerable<object[]> AdoLikeCases =>
[
[22, 9, 1.23456789m],
[30, 10, 123.4567890123m],
[18, 2, 1.239m]
];

public static IEnumerable<object[]> OverflowCases =>
[
[15, 2, 123456789012345.67m],
[10, 0, 12345678901m],
[22, 9, 1.0000000001m]
];

private ParametricDecimalContext NewCtx(int p, int s)
=> new(BuildOptions(), p, s);

[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);

try
{
var e = new ParamItem { Price = value };
ctx.Add(e);
await ctx.SaveChangesAsync();

var got = await ctx.Items.AsNoTracking().SingleAsync(x => x.Id == e.Id);

var expected = Math.Round(value, s, MidpointRounding.ToEven);
Assert.Equal(expected, 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);
}
catch (DbUpdateException ex) when ((ex.InnerException?.Message ?? "").Contains("Cannot find table",
StringComparison.OrdinalIgnoreCase))
{
}
catch (Exception ex) when (ex.ToString()
.Contains("EnableParameterizedDecimal", StringComparison.OrdinalIgnoreCase))
{
}

Check failure on line 69 in src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTheoryTest.cs

View workflow job for this annotation

GitHub Actions / efcore-tests (latest, 9.0.x)

EntityFrameworkCore.Ydb.FunctionalTests.Query.DecimalParameterizedYdbTheoryTest.Decimal_roundtrips_or_rounds_like_ado(p: 22

Microsoft.EntityFrameworkCore.DbUpdateException : An error occurred while saving the entity changes. See the inner exception for details. ---- System.OverflowException : Value 1.23456789 does not fit Decimal(18, 2)

Check failure on line 69 in src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTheoryTest.cs

View workflow job for this annotation

GitHub Actions / efcore-tests (latest, 9.0.x)

EntityFrameworkCore.Ydb.FunctionalTests.Query.DecimalParameterizedYdbTheoryTest.Decimal_roundtrips_or_rounds_like_ado(p: 18

Microsoft.EntityFrameworkCore.DbUpdateException : An error occurred while saving the entity changes. See the inner exception for details. ---- System.OverflowException : Value 1.239 does not fit Decimal(18, 2)

Check failure on line 69 in src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTheoryTest.cs

View workflow job for this annotation

GitHub Actions / efcore-tests (latest, 8.0.x)

EntityFrameworkCore.Ydb.FunctionalTests.Query.DecimalParameterizedYdbTheoryTest.Decimal_roundtrips_or_rounds_like_ado(p: 22

Microsoft.EntityFrameworkCore.DbUpdateException : An error occurred while saving the entity changes. See the inner exception for details. ---- System.OverflowException : Value 1.23456789 does not fit Decimal(18, 2)

Check failure on line 69 in src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTheoryTest.cs

View workflow job for this annotation

GitHub Actions / efcore-tests (latest, 8.0.x)

EntityFrameworkCore.Ydb.FunctionalTests.Query.DecimalParameterizedYdbTheoryTest.Decimal_roundtrips_or_rounds_like_ado(p: 18

Microsoft.EntityFrameworkCore.DbUpdateException : An error occurred while saving the entity changes. See the inner exception for details. ---- System.OverflowException : Value 1.239 does not fit Decimal(18, 2)
}

[Theory]
[MemberData(nameof(OverflowCases))]
public async Task Decimal_overflow_bubbles_up(int p, int s, decimal value)
{
await using var ctx = NewCtx(p, s);

try
{
ctx.Add(new ParamItem { Price = value });
await Assert.ThrowsAsync<DbUpdateException>(() => ctx.SaveChangesAsync());
}
catch (DbUpdateException ex) when ((ex.InnerException?.Message ?? "").Contains("Cannot find table",
StringComparison.OrdinalIgnoreCase))
{
}
catch (Exception ex) when (ex.ToString()
.Contains("EnableParameterizedDecimal", StringComparison.OrdinalIgnoreCase))
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore;

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.HasKey(x => x.Id);
b.Property(x => x.Price).HasPrecision(_p, _s);
});
}

public sealed class ParamItem
{
public int Id { get; set; }
public decimal Price { get; set; }
}
Loading