diff --git a/src/EFCore.Ydb/CHANGELOG.md b/src/EFCore.Ydb/CHANGELOG.md index 3b4ec968..4966623c 100644 --- a/src/EFCore.Ydb/CHANGELOG.md +++ b/src/EFCore.Ydb/CHANGELOG.md @@ -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. diff --git a/src/EFCore.Ydb/src/EntityFrameworkCore.Ydb.csproj b/src/EFCore.Ydb/src/EntityFrameworkCore.Ydb.csproj index 2065a872..5f056122 100644 --- a/src/EFCore.Ydb/src/EntityFrameworkCore.Ydb.csproj +++ b/src/EFCore.Ydb/src/EntityFrameworkCore.Ydb.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/IYdbTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/IYdbTypeMapping.cs new file mode 100644 index 00000000..1aefc55a --- /dev/null +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/IYdbTypeMapping.cs @@ -0,0 +1,11 @@ +using Ydb.Sdk.Ado.YdbType; + +namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; + +internal interface IYdbTypeMapping +{ + /// + /// The database type used by YDB. + /// + YdbDbType YdbDbType { get; } +} diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBoolTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBoolTypeMapping.cs index dae16c8c..b0a900bd 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBoolTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBoolTypeMapping.cs @@ -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"; diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs index 1d7449ba..6daf133a 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs @@ -8,17 +8,8 @@ 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) { } @@ -26,8 +17,7 @@ protected YdbBytesTypeMapping(RelationalTypeMappingParameters parameters) : base { } - protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) - => new YdbBytesTypeMapping(parameters); + protected override YdbBytesTypeMapping Clone(RelationalTypeMappingParameters parameters) => new(parameters); protected override string GenerateNonNullSqlLiteral(object value) { diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs index 637520d1..5cbb8500 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs @@ -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) { diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbJsonTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbJsonTypeMapping.cs index 4d9df313..4cdc35e6 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbJsonTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbJsonTypeMapping.cs @@ -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) { @@ -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; @@ -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; } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs new file mode 100644 index 00000000..08dbb778 --- /dev/null +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs @@ -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; +} diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs index ffecd91a..ae8a8f84 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs @@ -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( @@ -23,7 +23,7 @@ public YdbTextTypeMapping(string storeType) { } - protected YdbTextTypeMapping(RelationalTypeMappingParameters parameters) + private YdbTextTypeMapping(RelationalTypeMappingParameters parameters) : base(parameters) { } @@ -31,7 +31,7 @@ protected YdbTextTypeMapping(RelationalTypeMappingParameters 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"; } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs index fc7d7e49..b01b160e 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -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; @@ -15,6 +17,7 @@ RelationalTypeMappingSourceDependencies relationalDependencies ) : RelationalTypeMappingSource(dependencies, relationalDependencies) { private static readonly ConcurrentDictionary DecimalCache = new(); + private static readonly ConcurrentDictionary ListMappings = new(); #region Mappings @@ -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; + } } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/DecimalParameterizedYdbTest.cs similarity index 66% rename from src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs rename to src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/DecimalParameterizedYdbTest.cs index a5522380..79399ebf 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Query/DecimalParameterizedYdbTest.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/DecimalParameterizedYdbTest.cs @@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Storage; using Xunit; -namespace EntityFrameworkCore.Ydb.FunctionalTests.Query; +namespace EntityFrameworkCore.Ydb.FunctionalTests; public class DecimalParameterizedYdbTest { @@ -13,7 +13,6 @@ private static DbContextOptions BuildOptions() => new DbContextOptionsBuilder() .UseYdb("Host=localhost;Port=2136") .EnableServiceProviderCaching(false) - .LogTo(Console.WriteLine) .Options; public static TheoryData SuccessCases => new() @@ -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(); - 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(); + 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] @@ -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(() => ctx.SaveChangesAsync()); - } - finally - { - await ctx.Database.EnsureDeletedAsync(); - } + + ctx.Add(new ParamItem { Price = value }); + await Assert.ThrowsAsync(() => ctx.SaveChangesAsync()); } [Theory] @@ -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(); - 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(); + 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 options, int p, int s) diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs new file mode 100644 index 00000000..0fe025e9 --- /dev/null +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs @@ -0,0 +1,142 @@ +using EntityFrameworkCore.Ydb.Extensions; +using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace EntityFrameworkCore.Ydb.FunctionalTests; + +public class SqlQueryCollectionParameterTests +{ + private static readonly DateTime SomeTimestamp = DateTime.Parse("2025-11-02T18:47:14.112353"); + + public static IEnumerable GetCollectionTestCases() + { + yield return [new List { false, true, false }]; + yield return [(bool[])[false, true, false]]; + yield return [new List { 1, 2, 3 }]; + yield return [new sbyte[] { 1, 2, 3 }]; + yield return [new List { 1, 2, 3 }]; + yield return [new short[] { 1, 2, 3 }]; + yield return [new List { 1, 2, 3 }]; + yield return [(int[])[1, 2, 3]]; + yield return [new List { 1, 2, 3 }]; + yield return [new long[] { 1, 2, 3 }]; + yield return [new List { 1, 2, 3 }]; + yield return [new List { 1, 2, 3 }]; + yield return [new ushort[] { 1, 2, 3 }]; + yield return [new List { 1, 2, 3 }]; + yield return [new uint[] { 1, 2, 3 }]; + yield return [new List { 1, 2, 3 }]; + yield return [new ulong[] { 1, 2, 3 }]; + yield return [new List { 1, 2, 3 }]; + yield return [new float[] { 1, 2, 3 }]; + yield return [new List { 1, 2, 3 }]; + yield return [new double[] { 1, 2, 3 }]; + yield return [new List { 1, 2, 3 }]; + yield return [new decimal[] { 1, 2, 3 }]; + yield return [new List { "1", "2", "3" }]; + yield return [(string[])["1", "2", "3"]]; + yield return [new List { new byte[] { 1, 1 }, new byte[] { 2, 2 }, new byte[] { 3, 3 } }]; + yield return [(byte[][])[[1, 1], [2, 2], [3, 3]]]; + yield return + [new List { SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3) }]; + yield return [(DateTime[])[SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3)]]; + yield return [new List { TimeSpan.FromDays(1), TimeSpan.FromDays(2), TimeSpan.FromDays(3) }]; + yield return [(TimeSpan[])[TimeSpan.FromDays(1), TimeSpan.FromDays(2), TimeSpan.FromDays(3)]]; + yield return [new List { false, true, false, null }]; + yield return [(bool?[])[false, true, false, null]]; + yield return [new List { 1, 2, 3, null }]; + yield return [new sbyte?[] { 1, 2, 3, null }]; + yield return [new List { 1, 2, 3, null }]; + yield return [new short?[] { 1, 2, 3, null }]; + yield return [new List { 1, 2, 3, null }]; + yield return [(int?[])[1, 2, 3, null]]; + yield return [new List { 1, 2, 3, null }]; + yield return [new long?[] { 1, 2, 3, null }]; + yield return [new List { 1, 2, 3, null }]; + yield return [new List { 1, 2, 3, null }]; + yield return [new ushort?[] { 1, 2, 3, null }]; + yield return [new List { 1, 2, 3, null }]; + yield return [new uint?[] { 1, 2, 3, null }]; + yield return [new List { 1, 2, 3, null }]; + yield return [new ulong?[] { 1, 2, 3, null }]; + yield return [new List { 1, 2, 3, null }]; + yield return [new float?[] { 1, 2, 3, null }]; + yield return [new List { 1, 2, 3, null }]; + yield return [new double?[] { 1, 2, 3, null }]; + yield return [new List { 1, 2, 3, null }]; + yield return [new decimal?[] { 1, 2, 3, null }]; + yield return [new List { "1", "2", "3", null }]; + yield return [(string?[])["1", "2", "3", null]]; + yield return [new List { new byte[] { 1, 1 }, new byte[] { 2, 2 }, new byte[] { 3, 3 }, null }]; + yield return [(byte[]?[])[[1, 1], [2, 2], [3, 3], null]]; + yield return + [ + new List { SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3), null } + ]; + yield return + [(DateTime?[])[SomeTimestamp.AddDays(1), SomeTimestamp.AddDays(2), SomeTimestamp.AddDays(3), null]]; + yield return + [new List { TimeSpan.FromDays(1), TimeSpan.FromDays(2), TimeSpan.FromDays(3), null }]; + yield return [(TimeSpan?[])[TimeSpan.FromDays(1), TimeSpan.FromDays(2), TimeSpan.FromDays(3), null]]; + } + + [Theory] + [MemberData(nameof(GetCollectionTestCases))] + public async Task SqlQuery_UsesListParameterForInClause(IEnumerable listValues) + { + await using var testStore = YdbTestStoreFactory.Instance.Create("SqlQueryCollectionParameterTests"); + await using var testDbContext = new TestDbContext(); + await testStore.CleanAsync(testDbContext); + await testDbContext.Database.EnsureCreatedAsync(); + + testDbContext.Items.AddRange(listValues.Where(value => value != null) + .Select(value => new TestEntity { Id = Guid.NewGuid(), Value = value })); + await testDbContext.SaveChangesAsync(); + + var rows = await testDbContext.Database.SqlQuery>( + $"SELECT * FROM TestEntity WHERE Value IN {listValues}").ToListAsync(); + + Assert.Equal(3, rows.Count); + } + + [Fact] + public async Task SqlQuery_UsesBytesForWhereClause() + { + await using var testStore = YdbTestStoreFactory.Instance.Create("SqlQueryCollectionParameterTests"); + await using var testDbContext = new TestDbContext(); + await testStore.CleanAsync(testDbContext); + await testDbContext.Database.EnsureCreatedAsync(); + + var bytes = new byte[] { 1, 2, 3 }; + testDbContext.Items.Add(new TestEntity { Id = Guid.NewGuid(), Value = [1, 2, 3] }); + await testDbContext.SaveChangesAsync(); + + var rows = await testDbContext.Database.SqlQuery>( + $"SELECT * FROM TestEntity WHERE Value = {bytes}").ToListAsync(); + + Assert.Single(rows); + } + + public sealed class TestDbContext : DbContext + { + public DbSet> Items => Set>(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) => + modelBuilder.Entity>(b => + { + b.ToTable("TestEntity"); + b.HasKey(x => x.Id); + }); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder + .UseYdb("Host=localhost;Port=2136") + .EnableServiceProviderCaching(false); + } + + public sealed class TestEntity + { + public Guid Id { get; init; } + public TValue Value { get; set; } = default!; + } +} diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Dapper.Tests/DapperIntegrationTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Dapper.Tests/DapperIntegrationTests.cs index 4f5e0556..fc0ad7da 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Dapper.Tests/DapperIntegrationTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Dapper.Tests/DapperIntegrationTests.cs @@ -182,22 +182,21 @@ await connection.ExecuteAsync( [Fact] public async Task WhereIds_WhenInListSizeParametersIs15_000_ExecutedQuery() { - const ulong sizeBatch = 15_000; + const int sizeBatch = 15_000; const string tableName = "table_dapper_when_size_parameters"; await using var connection = await CreateOpenConnectionAsync(); await connection.ExecuteAsync( $"CREATE TABLE {tableName} (Id Int32, Name Text, Now Timestamp, PRIMARY KEY(Id));"); - for (var i = 0; i < 15_000; i++) + for (var i = 0; i < sizeBatch; i++) { await connection.ExecuteAsync($"INSERT INTO {tableName}(Id, Name, Now) VALUES(@Id, @Name, @Now)", new { Id = i, Name = $"Name {i}", DateTime.Now }); } - Assert.Equal(sizeBatch, await connection.ExecuteScalarAsync($@" - SELECT COUNT(*) FROM {tableName} - WHERE Id IN @Ids;", new { Ids = Enumerable.Range(0, 15_000).ToList() } - )); + Assert.Equal(sizeBatch, Convert.ToInt32(await connection.ExecuteScalarAsync($@" + SELECT COUNT(*) FROM {tableName} WHERE Id IN @Ids;", new { Ids = Enumerable.Range(0, sizeBatch).ToList() } + ))); await connection.ExecuteAsync($"DROP TABLE {tableName};"); }