Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 bug: SqlQuery throws exception when using list parameters ([#540](https://github.com/ydb-platform/ydb-dotnet-sdk/issues/540)).
- Added support for the YDB retry policy (ADO.NET) and new configuration methods in `YdbDbContextOptionsBuilder`:
- `EnableRetryIdempotence()`: enables retries for errors classified as idempotent. You must ensure the operation itself is idempotent.
- `UseRetryPolicy(YdbRetryPolicyConfig retryPolicyConfig)`: configures custom backoff parameters and the maximum number of retry attempts.
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.Ydb/src/EntityFrameworkCore.Ydb.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Ydb.Sdk" Version="0.24.0"/>
<PackageReference Include="Ydb.Sdk" Version="0.25.1"/>
</ItemGroup>

<ItemGroup>
Expand Down
11 changes: 11 additions & 0 deletions src/EFCore.Ydb/src/Storage/Internal/Mapping/IYdbTypeMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Ydb.Sdk.Ado.YdbType;

namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping;

internal interface IYdbTypeMapping
{
/// <summary>
/// The database type used by YDB.
/// </summary>
YdbDbType YdbDbType { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ private YdbBoolTypeMapping(RelationalTypeMappingParameters parameters)
{
}

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new YdbBoolTypeMapping(parameters);
protected override YdbBoolTypeMapping Clone(RelationalTypeMappingParameters parameters) => new(parameters);

protected override string GenerateNonNullSqlLiteral(object value)
=> (bool)value ? "TRUE" : "FALSE";
Expand Down
16 changes: 3 additions & 13 deletions src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,16 @@ public class YdbBytesTypeMapping : RelationalTypeMapping
{
public static YdbBytesTypeMapping Default { get; } = new();

private YdbBytesTypeMapping() : base(
new RelationalTypeMappingParameters(
new CoreTypeMappingParameters(
typeof(byte[]),
jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance
),
storeType: "Bytes",
dbType: System.Data.DbType.Binary,
unicode: false
)
)
private YdbBytesTypeMapping() : base("Bytes", typeof(byte[]), System.Data.DbType.Binary,
jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance, unicode: false)
{
}

protected YdbBytesTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters)
{
}

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new YdbBytesTypeMapping(parameters);
protected override YdbBytesTypeMapping Clone(RelationalTypeMappingParameters parameters) => new(parameters);

protected override string GenerateNonNullSqlLiteral(object value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ protected YdbDateOnlyTypeMapping(RelationalTypeMappingParameters parameters) : b
{
}

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new YdbDateOnlyTypeMapping(parameters);
protected override YdbDateOnlyTypeMapping Clone(RelationalTypeMappingParameters parameters) => new(parameters);

protected override string GenerateNonNullSqlLiteral(object value)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage;
using Ydb.Sdk.Ado;
using Ydb.Sdk.Ado.YdbType;

namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping;

public class YdbJsonTypeMapping : JsonTypeMapping
public class YdbJsonTypeMapping : JsonTypeMapping, IYdbTypeMapping
{
public YdbJsonTypeMapping(string storeType, Type clrType, DbType? dbType) : base(storeType, clrType, dbType)
{
Expand All @@ -32,8 +34,7 @@ private static readonly MethodInfo? EncodingGetBytesMethod
private static readonly ConstructorInfo? MemoryStreamConstructor
= typeof(MemoryStream).GetConstructor([typeof(byte[])]);

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new YdbJsonTypeMapping(parameters);
protected override YdbJsonTypeMapping Clone(RelationalTypeMappingParameters parameters) => new(parameters);

public override MethodInfo GetDataReaderMethod() => GetStringMethod;

Expand Down Expand Up @@ -72,4 +73,9 @@ public override Expression CustomizeDataReaderExpression(Expression expression)
EncodingGetBytesMethod ?? throw new Exception(),
expression)
);

public YdbDbType YdbDbType => YdbDbType.Json;

protected override void ConfigureParameter(DbParameter parameter) =>
((YdbParameter)parameter).YdbDbType = YdbDbType;
}
19 changes: 19 additions & 0 deletions src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections;
using System.Data.Common;
using Microsoft.EntityFrameworkCore.Storage;
using Ydb.Sdk.Ado;
using Ydb.Sdk.Ado.YdbType;

namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping;

internal class YdbListTypeMapping(
YdbDbType ydbDbType,
string storeTypeElement
) : RelationalTypeMapping(storeType: $"List<{storeTypeElement}>", typeof(IList))
{
protected override YdbListTypeMapping Clone(RelationalTypeMappingParameters parameters) =>
new(ydbDbType, storeTypeElement);

protected override void ConfigureParameter(DbParameter parameter) =>
((YdbParameter)parameter).YdbDbType = YdbDbType.List | ydbDbType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping;

public class YdbTextTypeMapping : RelationalTypeMapping
public sealed class YdbTextTypeMapping : RelationalTypeMapping
{
public static YdbTextTypeMapping Default { get; } = new("Text");

public YdbTextTypeMapping(string storeType)
private YdbTextTypeMapping(string storeType)
: base(
new RelationalTypeMappingParameters(
new CoreTypeMappingParameters(
Expand All @@ -23,15 +23,15 @@ public YdbTextTypeMapping(string storeType)
{
}

protected YdbTextTypeMapping(RelationalTypeMappingParameters parameters)
private YdbTextTypeMapping(RelationalTypeMappingParameters parameters)
: base(parameters)
{
}

protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
=> new YdbTextTypeMapping(parameters);

protected virtual string EscapeSqlLiteral(string literal) => literal.Replace("'", "\\'");
private static string EscapeSqlLiteral(string literal) => literal.Replace("'", "\\'");

protected override string GenerateNonNullSqlLiteral(object value) => $"'{EscapeSqlLiteral((string)value)}'u";
}
58 changes: 51 additions & 7 deletions src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text.Json;
using EntityFrameworkCore.Ydb.Storage.Internal.Mapping;
using Microsoft.EntityFrameworkCore.Storage;
using Ydb.Sdk.Ado.YdbType;
using Type = System.Type;

namespace EntityFrameworkCore.Ydb.Storage.Internal;
Expand All @@ -15,6 +17,7 @@ RelationalTypeMappingSourceDependencies relationalDependencies
) : RelationalTypeMappingSource(dependencies, relationalDependencies)
{
private static readonly ConcurrentDictionary<RelationalTypeMappingInfo, RelationalTypeMapping> DecimalCache = new();
private static readonly ConcurrentDictionary<YdbDbType, YdbListTypeMapping> ListMappings = new();

#region Mappings

Expand Down Expand Up @@ -125,19 +128,60 @@ RelationalTypeMappingSourceDependencies relationalDependencies
var clrType = mappingInfo.ClrType;
var storeTypeName = mappingInfo.StoreTypeName;

if (storeTypeName is null || !StoreTypeMapping.TryGetValue(storeTypeName, out var mappings))
if (storeTypeName is not null && StoreTypeMapping.TryGetValue(storeTypeName, out var mappings))
{
return clrType is null ? null : ClrTypeMapping.GetValueOrDefault(clrType);
}
// We found the user-specified store type. No CLR type was provided - we're probably
// scaffolding from an existing database, take the first mapping as the default.
if (clrType is null)
{
return mappings[0];
}

foreach (var m in mappings)
{
if (m.ClrType == clrType)
// A CLR type was provided - look for a mapping between the store and CLR types. If not found, fail
// immediately.
foreach (var m in mappings)
{
return m;
if (m.ClrType == clrType)
{
return m;
}
}
}

return clrType is null ? null : ClrTypeMapping.GetValueOrDefault(clrType);
}

public override RelationalTypeMapping? FindMapping(Type type)
{
if (type == typeof(byte[]))
return base.FindMapping(type);

var elementType = type.IsArray
? type.GetElementType()
: type.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IList<>))?
.GetGenericArguments()[0];

if (elementType == null)
return base.FindMapping(type);

elementType = Nullable.GetUnderlyingType(elementType) ?? elementType;

var elementTypeMapping = FindMapping(elementType);

if (elementTypeMapping == null)
return base.FindMapping(type);

var ydbDbType = elementTypeMapping is IYdbTypeMapping ydbTypeMapping
? ydbTypeMapping.YdbDbType
: (elementTypeMapping.DbType ?? DbType.Object).ToYdbDbType();

if (ListMappings.TryGetValue(ydbDbType, out var mapping))
return mapping;

mapping = new YdbListTypeMapping(ydbDbType, elementTypeMapping.StoreType);
ListMappings.TryAdd(ydbDbType, mapping);

return mapping;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
using Microsoft.EntityFrameworkCore.Storage;
using Xunit;

namespace EntityFrameworkCore.Ydb.FunctionalTests.Query;
namespace EntityFrameworkCore.Ydb.FunctionalTests;

public class DecimalParameterizedYdbTest
{
private static DbContextOptions<ParametricDecimalContext> BuildOptions() =>
new DbContextOptionsBuilder<ParametricDecimalContext>()
.UseYdb("Host=localhost;Port=2136")
.EnableServiceProviderCaching(false)
.LogTo(Console.WriteLine)
.Options;

public static TheoryData<int, int, decimal> SuccessCases => new()
Expand Down Expand Up @@ -52,23 +51,17 @@ public async Task Should_RoundtripDecimal_When_ValueFitsPrecisionAndScale(int p,
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<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 ctx.Database.EnsureDeletedAsync();
}

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

[Theory]
Expand All @@ -80,15 +73,9 @@ public async Task Should_ThrowOverflow_When_ValueExceedsPrecisionOrScale(int p,
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<DbUpdateException>(() => ctx.SaveChangesAsync());
}
finally
{
await ctx.Database.EnsureDeletedAsync();
}

ctx.Add(new ParamItem { Price = value });
await Assert.ThrowsAsync<DbUpdateException>(() => ctx.SaveChangesAsync());
}

[Theory]
Expand All @@ -101,25 +88,19 @@ public async Task Should_SumDecimal_When_ValueFitsPrecisionAndScale(int p, int s
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<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 ctx.Database.EnsureDeletedAsync();
}

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

public sealed class ParametricDecimalContext(DbContextOptions<ParametricDecimalContext> options, int p, int s)
Expand Down
Loading