From 37d9f0a6fdacc465a369ef22c4b18312764afc62 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Fri, 24 Oct 2025 15:25:05 +0300 Subject: [PATCH 01/12] fix bug EFCore: SqlQuery with collection parameters ``` [0] (2:15) Error: At function: Filter, At function: Coalesce [0] (2:35) Error: At function: == [0] (2:35) Error: Uncompatible types in compare: Int32 '==' Utf8 ``` --- .../Internal/Mapping/YdbListTypeMapping.cs | 8 ++ .../DecimalParameterizedYdbTest.cs | 75 +++++++------------ .../SqlQueryCollectionParameterTests.cs | 48 ++++++++++++ .../DapperIntegrationTests.cs | 7 +- 4 files changed, 87 insertions(+), 51 deletions(-) create mode 100644 src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs rename src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/{Query => }/DecimalParameterizedYdbTest.cs (66%) create mode 100644 src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs 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..5940deb9 --- /dev/null +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs @@ -0,0 +1,8 @@ +using Microsoft.EntityFrameworkCore.Storage; + +namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; + +public class YdbListTypeMapping() : RelationalTypeMapping(new RelationalTypeMappingParameters()) +{ + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => this; +} 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..75bf3fad 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..611071b6 --- /dev/null +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs @@ -0,0 +1,48 @@ +using EntityFrameworkCore.Ydb.Extensions; +using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace EntityFrameworkCore.Ydb.FunctionalTests; + +public class SqlQueryCollectionParameterTests +{ + [Fact] + public async Task SqlQuery_DoesNotExpandCollectionParameter_InClause() + { + await using var testStore = YdbTestStoreFactory.Instance.Create("SqlQueryCollectionParameterTests"); + await using var testDbContext = new TestDbContext(); + await testStore.CleanAsync(testDbContext); + await testDbContext.Database.EnsureCreatedAsync(); + + var ids = new List { 1, 2, 3 }; + testDbContext.Items.AddRange(new TestEntity { Id = 1, Price = 1 }, new TestEntity { Id = 2, Price = 2 }); + await testDbContext.SaveChangesAsync(); + + var rows = await testDbContext.Database.SqlQuery( + $"SELECT * FROM TestEntity WHERE Id = ({ids})").ToListAsync(); + + Assert.Equal(2, rows.Count); + } + + 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 int Id { get; init; } + public int Price { get; init; } + } +} 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..d9f81c38 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,21 +182,20 @@ 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() } + SELECT COUNT(*) FROM {tableName} WHERE Id IN @Ids;", new { Ids = Enumerable.Range(0, sizeBatch).ToList() } )); await connection.ExecuteAsync($"DROP TABLE {tableName};"); } From 442d70b815ac81141c94436b8aef7ccdfe44ad1d Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Wed, 5 Nov 2025 12:25:10 +0300 Subject: [PATCH 02/12] next iteration --- .../Internal/Mapping/YdbListTypeMapping.cs | 11 ++++++++++- .../Internal/Mapping/YdbTextTypeMapping.cs | 8 ++++---- .../Storage/Internal/YdbTypeMappingSource.cs | 8 ++++++++ .../SqlQueryCollectionParameterTests.cs | 18 +++++++++++------- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs index 5940deb9..b345c3f8 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs @@ -1,8 +1,17 @@ +using System.Collections; using Microsoft.EntityFrameworkCore.Storage; namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; -public class YdbListTypeMapping() : RelationalTypeMapping(new RelationalTypeMappingParameters()) +internal class YdbListTypeMapping() : RelationalTypeMapping( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(IList)), + storeType: "List", + dbType: System.Data.DbType.Object + ) +) { + internal static readonly YdbListTypeMapping Default = new(); + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => this; } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs index ffecd91a..75ae5ed4 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 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..c6fb1be3 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; @@ -48,6 +49,8 @@ RelationalTypeMappingSourceDependencies relationalDependencies // TODO: Await interval in Ydb.Sdk private static readonly TimeSpanTypeMapping Interval = new("Interval", DbType.Object); + + private static readonly YdbListTypeMapping List = YdbListTypeMapping.Default; #endregion @@ -125,6 +128,11 @@ RelationalTypeMappingSourceDependencies relationalDependencies var clrType = mappingInfo.ClrType; var storeTypeName = mappingInfo.StoreTypeName; + if (clrType != null && clrType.IsAssignableTo(typeof(IList))) + { + return List; + } + if (storeTypeName is null || !StoreTypeMapping.TryGetValue(storeTypeName, out var mappings)) { return clrType is null ? null : ClrTypeMapping.GetValueOrDefault(clrType); diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs index 611071b6..c2f88701 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs @@ -16,13 +16,17 @@ public async Task SqlQuery_DoesNotExpandCollectionParameter_InClause() await testDbContext.Database.EnsureCreatedAsync(); var ids = new List { 1, 2, 3 }; - testDbContext.Items.AddRange(new TestEntity { Id = 1, Price = 1 }, new TestEntity { Id = 2, Price = 2 }); + testDbContext.Items.AddRange( + new TestEntity { Id = 1, Price = 1 }, + new TestEntity { Id = 2, Price = 2 }, + new TestEntity { Id = 3, Price = 3 } + ); await testDbContext.SaveChangesAsync(); - + var rows = await testDbContext.Database.SqlQuery( - $"SELECT * FROM TestEntity WHERE Id = ({ids})").ToListAsync(); - - Assert.Equal(2, rows.Count); + $"SELECT * FROM TestEntity WHERE Id IN {ids}").ToListAsync(); + + Assert.Equal(3, rows.Count); } public sealed class TestDbContext : DbContext @@ -36,8 +40,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuild }); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseYdb("Host=localhost;Port=2136") - .EnableServiceProviderCaching(false); + .UseYdb("Host=localhost;Port=2136") + .EnableServiceProviderCaching(false); } public sealed class TestEntity From 641d6e12744bbeb8a2df0b85652a57011e723b08 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Thu, 6 Nov 2025 15:27:31 +0300 Subject: [PATCH 03/12] commit --- .../Program.cs | 170 +++++++++++++++++- .../src/EntityFrameworkCore.Ydb.csproj | 2 +- .../Internal/YdbHistoryRepository.cs | 2 +- .../Internal/Mapping/IYdbTypeMapping.cs | 11 ++ .../Internal/Mapping/YdbBoolTypeMapping.cs | 3 +- .../Internal/Mapping/YdbBytesTypeMapping.cs | 17 +- .../Mapping/YdbDateOnlyTypeMapping.cs | 4 +- .../Internal/Mapping/YdbJsonTypeMapping.cs | 12 +- .../Internal/Mapping/YdbListTypeMapping.cs | 20 ++- .../Internal/Mapping/YdbTextTypeMapping.cs | 2 +- .../Storage/Internal/YdbTypeMappingSource.cs | 60 +++++-- .../Test.cs | 50 ++++++ 12 files changed, 302 insertions(+), 51 deletions(-) create mode 100644 src/EFCore.Ydb/src/Storage/Internal/Mapping/IYdbTypeMapping.cs create mode 100644 src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Test.cs diff --git a/examples/EntityFrameworkCore.Ydb.QuickStart/Program.cs b/examples/EntityFrameworkCore.Ydb.QuickStart/Program.cs index 32018e4c..4c522797 100644 --- a/examples/EntityFrameworkCore.Ydb.QuickStart/Program.cs +++ b/examples/EntityFrameworkCore.Ydb.QuickStart/Program.cs @@ -23,9 +23,9 @@ await db.SaveChangesAsync(); // Delete -Console.WriteLine("Delete the blog"); -db.Remove(blog); -await db.SaveChangesAsync(); +// Console.WriteLine("Delete the blog"); +// db.Remove(blog); +// await db.SaveChangesAsync(); internal class BloggingContextFactory : IDesignTimeDbContextFactory { @@ -61,6 +61,8 @@ internal BloggingContext(DbContextOptions options) : base(optio public DbSet Blogs { get; set; } public DbSet Posts { get; set; } + + // public DbSet Users { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseYdb("Host=localhost;Port=2136;Database=/local"); @@ -84,5 +86,165 @@ internal class Post public string Content { get; init; } = string.Empty; + public bool EmailConfirmed { get; init; } + public Blog Blog { get; init; } = null!; -} \ No newline at end of file +} +// +// public class IdentityUser +// { +// public virtual Guid? Id { get; protected set; } +// +// public virtual Guid? TenantId { get; protected set; } +// +// /// +// /// Gets or sets the user name for this user. +// /// +// public virtual string UserName { get; protected internal set; } +// +// +// public virtual string NormalizedUserName { get; protected internal set; } +// +// public virtual string Name { get; set; } +// +// public virtual string Surname { get; set; } +// +// public virtual string Email { get; protected internal set; } +// +// public virtual string NormalizedEmail { get; protected internal set; } +// +// /// +// /// Gets or sets a flag indicating if a user has confirmed their email address. +// /// +// /// True if the email address has been confirmed, otherwise false. +// public virtual bool EmailConfirmed { get; protected internal set; } +// +// public virtual string PasswordHash { get; protected internal set; } +// +// public virtual string SecurityStamp { get; protected internal set; } +// +// public virtual bool IsExternal { get; set; } +// +// public virtual string PhoneNumber { get; protected internal set; } +// +// /// +// /// Gets or sets a flag indicating if a user has confirmed their telephone address. +// /// +// /// True if the telephone number has been confirmed, otherwise false. +// public virtual bool PhoneNumberConfirmed { get; protected internal set; } +// +// /// +// /// Gets or sets a flag indicating if the user is active. +// /// +// public virtual bool IsActive { get; protected internal set; } +// +// /// +// /// Gets or sets a flag indicating if two factor authentication is enabled for this user. +// /// +// /// True if 2fa is enabled, otherwise false. +// public virtual bool TwoFactorEnabled { get; protected internal set; } +// +// /// +// /// Gets or sets the date and time, in UTC, when any user lockout ends. +// /// +// /// +// /// A value in the past means the user is not locked out. +// /// +// public virtual DateTimeOffset? LockoutEnd { get; protected internal set; } +// +// /// +// /// Gets or sets a flag indicating if the user could be locked out. +// /// +// /// True if the user could be locked out, otherwise false. +// public virtual bool LockoutEnabled { get; protected internal set; } +// +// /// +// /// Gets or sets the number of failed login attempts for the current user. +// /// +// public virtual int AccessFailedCount { get; protected internal set; } +// +// /// +// /// Should change password on next login. +// /// +// public virtual bool ShouldChangePasswordOnNextLogin { get; protected internal set; } +// +// /// +// /// A version value that is increased whenever the entity is changed. +// /// +// public virtual int EntityVersion { get; protected set; } +// +// /// +// /// Gets or sets the last password change time for the user. +// /// +// public virtual DateTimeOffset? LastPasswordChangeTime { get; protected set; } +// +// protected IdentityUser() +// { +// } +// +// public IdentityUser( +// Guid id, +// string userName, +// string email, +// Guid? tenantId = null) +// +// { +// Id = id; +// TenantId = tenantId; +// UserName = userName; +// NormalizedUserName = userName.ToUpperInvariant(); +// Email = email; +// NormalizedEmail = email.ToUpperInvariant(); +// SecurityStamp = Guid.NewGuid().ToString(); +// IsActive = true; +// } +// +// +// /// +// /// Use for regular email confirmation. +// /// Using this skips the confirmation process and directly sets the . +// /// +// public virtual void SetEmailConfirmed(bool confirmed) +// { +// EmailConfirmed = confirmed; +// } +// +// public virtual void SetPhoneNumberConfirmed(bool confirmed) +// { +// PhoneNumberConfirmed = confirmed; +// } +// +// /// +// /// Normally use to change the phone number +// /// in the application code. +// /// This method is to directly set it with a confirmation information. +// /// +// /// +// /// +// /// +// public void SetPhoneNumber(string phoneNumber, bool confirmed) +// { +// PhoneNumber = phoneNumber; +// PhoneNumberConfirmed = confirmed; +// } +// +// public virtual void SetIsActive(bool isActive) +// { +// IsActive = isActive; +// } +// +// public virtual void SetShouldChangePasswordOnNextLogin(bool shouldChangePasswordOnNextLogin) +// { +// ShouldChangePasswordOnNextLogin = shouldChangePasswordOnNextLogin; +// } +// +// public virtual void SetLastPasswordChangeTime(DateTimeOffset? lastPasswordChangeTime) +// { +// LastPasswordChangeTime = lastPasswordChangeTime; +// } +// +// public override string ToString() +// { +// return $"{base.ToString()}, UserName = {UserName}"; +// } +// } \ No newline at end of file diff --git a/src/EFCore.Ydb/src/EntityFrameworkCore.Ydb.csproj b/src/EFCore.Ydb/src/EntityFrameworkCore.Ydb.csproj index 2065a872..1d399f29 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/Migrations/Internal/YdbHistoryRepository.cs b/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs index a4de8c3e..1c5070d6 100644 --- a/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs +++ b/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs @@ -53,7 +53,7 @@ await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( return new YdbMigrationDatabaseLock(this); } - catch (YdbException) + catch (YdbException ex) { await Task.Delay(100 + Random.Shared.Next(1000), cancellationToken); } 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..d1de1210 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs @@ -1,6 +1,7 @@ using System.Text; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Json; +using Ydb.Sdk.Ado.YdbType; namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; @@ -8,17 +9,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) { } @@ -26,8 +18,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..99e0a0db 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs @@ -3,7 +3,6 @@ namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; -// TODO: Await DateOnly support in Ydb.Sdk public class YdbDateOnlyTypeMapping : RelationalTypeMapping { private const string DateOnlyFormatConst = "{0:yyyy-MM-dd}"; @@ -24,8 +23,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 index b345c3f8..40d395d4 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs @@ -1,17 +1,19 @@ using System.Collections; +using System.Data.Common; using Microsoft.EntityFrameworkCore.Storage; +using Ydb.Sdk.Ado.YdbType; namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; -internal class YdbListTypeMapping() : RelationalTypeMapping( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(IList)), - storeType: "List", - dbType: System.Data.DbType.Object - ) -) +internal class YdbListTypeMapping( + YdbDbType ydbDbType, + string storeTypeElement +) : RelationalTypeMapping(storeType: $"List<{storeTypeElement}>", typeof(IList)) { - internal static readonly YdbListTypeMapping Default = new(); + protected override YdbListTypeMapping Clone(RelationalTypeMappingParameters parameters) => + new(ydbDbType, storeTypeElement); - protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => this; + protected override void ConfigureParameter(DbParameter parameter) + { + } } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs index 75ae5ed4..ae8a8f84 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbTextTypeMapping.cs @@ -31,7 +31,7 @@ private YdbTextTypeMapping(RelationalTypeMappingParameters parameters) protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) => new YdbTextTypeMapping(parameters); - private 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 c6fb1be3..a8d3092f 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -3,9 +3,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; @@ -49,8 +51,6 @@ RelationalTypeMappingSourceDependencies relationalDependencies // TODO: Await interval in Ydb.Sdk private static readonly TimeSpanTypeMapping Interval = new("Interval", DbType.Object); - - private static readonly YdbListTypeMapping List = YdbListTypeMapping.Default; #endregion @@ -128,24 +128,56 @@ RelationalTypeMappingSourceDependencies relationalDependencies var clrType = mappingInfo.ClrType; var storeTypeName = mappingInfo.StoreTypeName; - if (clrType != null && clrType.IsAssignableTo(typeof(IList))) + if (storeTypeName is not null && StoreTypeMapping.TryGetValue(storeTypeName, out var mappings)) { - return List; - } - - if (storeTypeName is 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); } + + protected override RelationalTypeMapping? FindCollectionMapping( + RelationalTypeMappingInfo info, + Type modelType, + Type? providerType, + CoreTypeMapping? elementMapping + ) + { + var elementType = modelType.IsArray + ? modelType.GetElementType() + : modelType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IList<>))? + .GetGenericArguments()[0]; + + if (elementType == null) + return null; + + elementType = Nullable.GetUnderlyingType(elementType) ?? elementType; + + var typeMapping = ClrTypeMapping.GetValueOrDefault(elementType); + + if (typeMapping == null) + return null; + + var ydbDbType = typeMapping is IYdbTypeMapping ydbTypeMapping + ? ydbTypeMapping.YdbDbType + : (typeMapping.DbType ?? DbType.Object).ToYdbDbType(); + + return new YdbListTypeMapping(ydbDbType, ) + } } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Test.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Test.cs new file mode 100644 index 00000000..3432195a --- /dev/null +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Test.cs @@ -0,0 +1,50 @@ +using EntityFrameworkCore.Ydb.Extensions; +using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace EntityFrameworkCore.Ydb.FunctionalTests; + +public class Test +{ + [Fact] + public async Task SqlQuery_DoesNotExpandCollectionParameter_InClause() + { + await using var testStore = YdbTestStoreFactory.Instance.Create("SqlQueryCollectionParameterTests"); + await using var testDbContext = new TestDbContext(); + await testStore.CleanAsync(testDbContext); + await testDbContext.Database.EnsureCreatedAsync(); + + var ids = new List { 1, 2, 3 }; + testDbContext.Items.AddRange( + new TestEntity { Id = 1 }, + new TestEntity { Id = 2 }, + new TestEntity { Id = 3 } + ); + await testDbContext.SaveChangesAsync(); + + var rows = await testDbContext.Database.SqlQuery($"SELECT * FROM TestEntity").ToListAsync(); + + Assert.Equal(3, rows.Count); + } + + public sealed class TestDbContext : DbContext + { + public DbSet Items => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder + .UseYdb("Host=localhost;Port=2136") + .EnableServiceProviderCaching(false); + } + + public sealed class TestEntity + { + public int Id { get; init; } + + /// + /// Gets or sets a flag indicating if a user has confirmed their email address. + /// + /// True if the email address has been confirmed, otherwise false. + public bool EmailConfirmed { get; protected internal set; } + } +} From b498e9140c00330092593e64edb7abf997b68af4 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Thu, 6 Nov 2025 18:20:34 +0300 Subject: [PATCH 04/12] commit --- .../src/EntityFrameworkCore.Ydb.csproj | 2 +- .../Internal/Mapping/YdbListTypeMapping.cs | 6 +- .../Storage/Internal/YdbTypeMappingSource.cs | 12 +- .../SqlQueryCollectionParameterTests.cs | 122 +++++++++++++++--- .../Test.cs | 50 ------- 5 files changed, 111 insertions(+), 81 deletions(-) delete mode 100644 src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Test.cs diff --git a/src/EFCore.Ydb/src/EntityFrameworkCore.Ydb.csproj b/src/EFCore.Ydb/src/EntityFrameworkCore.Ydb.csproj index 1d399f29..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/YdbListTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs index 40d395d4..08dbb778 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbListTypeMapping.cs @@ -1,6 +1,7 @@ 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; @@ -13,7 +14,6 @@ string storeTypeElement protected override YdbListTypeMapping Clone(RelationalTypeMappingParameters parameters) => new(ydbDbType, storeTypeElement); - protected override void ConfigureParameter(DbParameter parameter) - { - } + protected override void ConfigureParameter(DbParameter parameter) => + ((YdbParameter)parameter).YdbDbType = YdbDbType.List | ydbDbType; } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs index a8d3092f..21575f43 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -169,15 +169,15 @@ RelationalTypeMappingSourceDependencies relationalDependencies elementType = Nullable.GetUnderlyingType(elementType) ?? elementType; - var typeMapping = ClrTypeMapping.GetValueOrDefault(elementType); + var elementTypeMapping = FindMapping(elementType); - if (typeMapping == null) + if (elementTypeMapping == null) return null; - var ydbDbType = typeMapping is IYdbTypeMapping ydbTypeMapping + var ydbDbType = elementTypeMapping is IYdbTypeMapping ydbTypeMapping ? ydbTypeMapping.YdbDbType - : (typeMapping.DbType ?? DbType.Object).ToYdbDbType(); - - return new YdbListTypeMapping(ydbDbType, ) + : (elementTypeMapping.DbType ?? DbType.Object).ToYdbDbType(); + + return new YdbListTypeMapping(ydbDbType, elementTypeMapping.StoreType); } } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs index c2f88701..a7d0bb47 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs @@ -7,46 +7,126 @@ namespace EntityFrameworkCore.Ydb.FunctionalTests; public class SqlQueryCollectionParameterTests { - [Fact] - public async Task SqlQuery_DoesNotExpandCollectionParameter_InClause() + 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 { new(2001, 2, 26), new(2002, 2, 24), new(2010, 3, 14) }]; + // yield return [new DateOnly[] { new(2001, 2, 26), new(2002, 2, 24), new(2010, 3, 14) }]; + // 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 + // { new DateOnly(2001, 2, 26), new DateOnly(2002, 2, 24), new DateOnly(2010, 3, 14), null } + // ]; + // yield return [new DateOnly?[] { new(2001, 2, 26), new(2002, 2, 24), new(2010, 3, 14), 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_DoesNotExpandCollectionParameter_InClause(IEnumerable listIds) { await using var testStore = YdbTestStoreFactory.Instance.Create("SqlQueryCollectionParameterTests"); - await using var testDbContext = new TestDbContext(); + await using var testDbContext = new TestDbContext(); await testStore.CleanAsync(testDbContext); await testDbContext.Database.EnsureCreatedAsync(); - var ids = new List { 1, 2, 3 }; - testDbContext.Items.AddRange( - new TestEntity { Id = 1, Price = 1 }, - new TestEntity { Id = 2, Price = 2 }, - new TestEntity { Id = 3, Price = 3 } - ); + testDbContext.Items.AddRange(listIds.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 Id IN {ids}").ToListAsync(); + var rows = await testDbContext.Database.SqlQuery>( + $"SELECT * FROM TestEntity WHERE Value IN {listIds}").ToListAsync(); Assert.Equal(3, rows.Count); } - public sealed class TestDbContext : DbContext + public sealed class TestDbContext : DbContext { - public DbSet Items => Set(); + public DbSet> Items => Set>(); - protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity(b => - { - b.ToTable("TestEntity"); - b.HasKey(x => x.Id); - }); + 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 sealed class TestEntity { - public int Id { get; init; } - public int Price { get; init; } + public Guid Id { get; init; } + public TValue Value { get; init; } } } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Test.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Test.cs deleted file mode 100644 index 3432195a..00000000 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/Test.cs +++ /dev/null @@ -1,50 +0,0 @@ -using EntityFrameworkCore.Ydb.Extensions; -using EntityFrameworkCore.Ydb.FunctionalTests.TestUtilities; -using Microsoft.EntityFrameworkCore; -using Xunit; - -namespace EntityFrameworkCore.Ydb.FunctionalTests; - -public class Test -{ - [Fact] - public async Task SqlQuery_DoesNotExpandCollectionParameter_InClause() - { - await using var testStore = YdbTestStoreFactory.Instance.Create("SqlQueryCollectionParameterTests"); - await using var testDbContext = new TestDbContext(); - await testStore.CleanAsync(testDbContext); - await testDbContext.Database.EnsureCreatedAsync(); - - var ids = new List { 1, 2, 3 }; - testDbContext.Items.AddRange( - new TestEntity { Id = 1 }, - new TestEntity { Id = 2 }, - new TestEntity { Id = 3 } - ); - await testDbContext.SaveChangesAsync(); - - var rows = await testDbContext.Database.SqlQuery($"SELECT * FROM TestEntity").ToListAsync(); - - Assert.Equal(3, rows.Count); - } - - public sealed class TestDbContext : DbContext - { - public DbSet Items => Set(); - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseYdb("Host=localhost;Port=2136") - .EnableServiceProviderCaching(false); - } - - public sealed class TestEntity - { - public int Id { get; init; } - - /// - /// Gets or sets a flag indicating if a user has confirmed their email address. - /// - /// True if the email address has been confirmed, otherwise false. - public bool EmailConfirmed { get; protected internal set; } - } -} From ccc41bd22ab548041deffb13c201da70ca801fad Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Thu, 6 Nov 2025 18:36:32 +0300 Subject: [PATCH 05/12] commit --- .../SqlQueryCollectionParameterTests.cs | 138 +++++++++--------- 1 file changed, 65 insertions(+), 73 deletions(-) diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs index a7d0bb47..0f37aff7 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs @@ -11,79 +11,71 @@ public class SqlQueryCollectionParameterTests 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 { new(2001, 2, 26), new(2002, 2, 24), new(2010, 3, 14) }]; - // yield return [new DateOnly[] { new(2001, 2, 26), new(2002, 2, 24), new(2010, 3, 14) }]; - // 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 - // { new DateOnly(2001, 2, 26), new DateOnly(2002, 2, 24), new DateOnly(2010, 3, 14), null } - // ]; - // yield return [new DateOnly?[] { new(2001, 2, 26), new(2002, 2, 24), new(2010, 3, 14), 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 { 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]]; From 66b11e1f4e903476ee10948ae39b7de992063d49 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Thu, 6 Nov 2025 18:41:51 +0300 Subject: [PATCH 06/12] revert --- .../Program.cs | 170 +----------------- .../Internal/YdbHistoryRepository.cs | 2 +- .../Mapping/YdbDateOnlyTypeMapping.cs | 1 + 3 files changed, 6 insertions(+), 167 deletions(-) diff --git a/examples/EntityFrameworkCore.Ydb.QuickStart/Program.cs b/examples/EntityFrameworkCore.Ydb.QuickStart/Program.cs index 4c522797..32018e4c 100644 --- a/examples/EntityFrameworkCore.Ydb.QuickStart/Program.cs +++ b/examples/EntityFrameworkCore.Ydb.QuickStart/Program.cs @@ -23,9 +23,9 @@ await db.SaveChangesAsync(); // Delete -// Console.WriteLine("Delete the blog"); -// db.Remove(blog); -// await db.SaveChangesAsync(); +Console.WriteLine("Delete the blog"); +db.Remove(blog); +await db.SaveChangesAsync(); internal class BloggingContextFactory : IDesignTimeDbContextFactory { @@ -61,8 +61,6 @@ internal BloggingContext(DbContextOptions options) : base(optio public DbSet Blogs { get; set; } public DbSet Posts { get; set; } - - // public DbSet Users { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseYdb("Host=localhost;Port=2136;Database=/local"); @@ -86,165 +84,5 @@ internal class Post public string Content { get; init; } = string.Empty; - public bool EmailConfirmed { get; init; } - public Blog Blog { get; init; } = null!; -} -// -// public class IdentityUser -// { -// public virtual Guid? Id { get; protected set; } -// -// public virtual Guid? TenantId { get; protected set; } -// -// /// -// /// Gets or sets the user name for this user. -// /// -// public virtual string UserName { get; protected internal set; } -// -// -// public virtual string NormalizedUserName { get; protected internal set; } -// -// public virtual string Name { get; set; } -// -// public virtual string Surname { get; set; } -// -// public virtual string Email { get; protected internal set; } -// -// public virtual string NormalizedEmail { get; protected internal set; } -// -// /// -// /// Gets or sets a flag indicating if a user has confirmed their email address. -// /// -// /// True if the email address has been confirmed, otherwise false. -// public virtual bool EmailConfirmed { get; protected internal set; } -// -// public virtual string PasswordHash { get; protected internal set; } -// -// public virtual string SecurityStamp { get; protected internal set; } -// -// public virtual bool IsExternal { get; set; } -// -// public virtual string PhoneNumber { get; protected internal set; } -// -// /// -// /// Gets or sets a flag indicating if a user has confirmed their telephone address. -// /// -// /// True if the telephone number has been confirmed, otherwise false. -// public virtual bool PhoneNumberConfirmed { get; protected internal set; } -// -// /// -// /// Gets or sets a flag indicating if the user is active. -// /// -// public virtual bool IsActive { get; protected internal set; } -// -// /// -// /// Gets or sets a flag indicating if two factor authentication is enabled for this user. -// /// -// /// True if 2fa is enabled, otherwise false. -// public virtual bool TwoFactorEnabled { get; protected internal set; } -// -// /// -// /// Gets or sets the date and time, in UTC, when any user lockout ends. -// /// -// /// -// /// A value in the past means the user is not locked out. -// /// -// public virtual DateTimeOffset? LockoutEnd { get; protected internal set; } -// -// /// -// /// Gets or sets a flag indicating if the user could be locked out. -// /// -// /// True if the user could be locked out, otherwise false. -// public virtual bool LockoutEnabled { get; protected internal set; } -// -// /// -// /// Gets or sets the number of failed login attempts for the current user. -// /// -// public virtual int AccessFailedCount { get; protected internal set; } -// -// /// -// /// Should change password on next login. -// /// -// public virtual bool ShouldChangePasswordOnNextLogin { get; protected internal set; } -// -// /// -// /// A version value that is increased whenever the entity is changed. -// /// -// public virtual int EntityVersion { get; protected set; } -// -// /// -// /// Gets or sets the last password change time for the user. -// /// -// public virtual DateTimeOffset? LastPasswordChangeTime { get; protected set; } -// -// protected IdentityUser() -// { -// } -// -// public IdentityUser( -// Guid id, -// string userName, -// string email, -// Guid? tenantId = null) -// -// { -// Id = id; -// TenantId = tenantId; -// UserName = userName; -// NormalizedUserName = userName.ToUpperInvariant(); -// Email = email; -// NormalizedEmail = email.ToUpperInvariant(); -// SecurityStamp = Guid.NewGuid().ToString(); -// IsActive = true; -// } -// -// -// /// -// /// Use for regular email confirmation. -// /// Using this skips the confirmation process and directly sets the . -// /// -// public virtual void SetEmailConfirmed(bool confirmed) -// { -// EmailConfirmed = confirmed; -// } -// -// public virtual void SetPhoneNumberConfirmed(bool confirmed) -// { -// PhoneNumberConfirmed = confirmed; -// } -// -// /// -// /// Normally use to change the phone number -// /// in the application code. -// /// This method is to directly set it with a confirmation information. -// /// -// /// -// /// -// /// -// public void SetPhoneNumber(string phoneNumber, bool confirmed) -// { -// PhoneNumber = phoneNumber; -// PhoneNumberConfirmed = confirmed; -// } -// -// public virtual void SetIsActive(bool isActive) -// { -// IsActive = isActive; -// } -// -// public virtual void SetShouldChangePasswordOnNextLogin(bool shouldChangePasswordOnNextLogin) -// { -// ShouldChangePasswordOnNextLogin = shouldChangePasswordOnNextLogin; -// } -// -// public virtual void SetLastPasswordChangeTime(DateTimeOffset? lastPasswordChangeTime) -// { -// LastPasswordChangeTime = lastPasswordChangeTime; -// } -// -// public override string ToString() -// { -// return $"{base.ToString()}, UserName = {UserName}"; -// } -// } \ No newline at end of file +} \ No newline at end of file diff --git a/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs b/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs index 1c5070d6..a4de8c3e 100644 --- a/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs +++ b/src/EFCore.Ydb/src/Migrations/Internal/YdbHistoryRepository.cs @@ -53,7 +53,7 @@ await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync( return new YdbMigrationDatabaseLock(this); } - catch (YdbException ex) + catch (YdbException) { await Task.Delay(100 + Random.Shared.Next(1000), cancellationToken); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs index 99e0a0db..5cbb8500 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbDateOnlyTypeMapping.cs @@ -3,6 +3,7 @@ namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; +// TODO: Await DateOnly support in Ydb.Sdk public class YdbDateOnlyTypeMapping : RelationalTypeMapping { private const string DateOnlyFormatConst = "{0:yyyy-MM-dd}"; From d33dd9d9dfb923d7ec1a092b8b907e40a10c75b7 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Thu, 6 Nov 2025 18:48:57 +0300 Subject: [PATCH 07/12] fixex --- .../src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs | 1 - src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs | 1 - .../SqlQueryCollectionParameterTests.cs | 2 +- .../test/Ydb.Sdk.Ado.Dapper.Tests/DapperIntegrationTests.cs | 4 ++-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs index d1de1210..f5ce410a 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs @@ -1,7 +1,6 @@ using System.Text; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Json; -using Ydb.Sdk.Ado.YdbType; namespace EntityFrameworkCore.Ydb.Storage.Internal.Mapping; diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs index 21575f43..4dd97966 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs index 0f37aff7..33255588 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs @@ -119,6 +119,6 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => public sealed class TestEntity { public Guid Id { get; init; } - public TValue Value { 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 d9f81c38..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 @@ -194,9 +194,9 @@ await connection.ExecuteAsync($"INSERT INTO {tableName}(Id, Name, Now) VALUES(@I new { Id = i, Name = $"Name {i}", DateTime.Now }); } - Assert.Equal(sizeBatch, await connection.ExecuteScalarAsync($@" + 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};"); } From 59e71232baf008615001c21c60455edd5b07ac3f Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Thu, 6 Nov 2025 18:52:35 +0300 Subject: [PATCH 08/12] fix linter --- .../DecimalParameterizedYdbTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/DecimalParameterizedYdbTest.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/DecimalParameterizedYdbTest.cs index 75bf3fad..79399ebf 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/DecimalParameterizedYdbTest.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/DecimalParameterizedYdbTest.cs @@ -51,7 +51,7 @@ public async Task Should_RoundtripDecimal_When_ValueFitsPrecisionAndScale(int p, await using var ctx = NewCtx(p, s); await testStore.CleanAsync(ctx); await ctx.Database.EnsureCreatedAsync(); - + var e = new ParamItem { Price = value }; ctx.Add(e); await ctx.SaveChangesAsync(); From edbd575a5dd988ce46e03c16d682d9e95bb29336 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Fri, 7 Nov 2025 00:02:51 +0300 Subject: [PATCH 09/12] fix! --- .../Internal/Mapping/YdbBytesTypeMapping.cs | 2 +- .../Storage/Internal/YdbTypeMappingSource.cs | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs index f5ce410a..6daf133a 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/Mapping/YdbBytesTypeMapping.cs @@ -9,7 +9,7 @@ public class YdbBytesTypeMapping : RelationalTypeMapping public static YdbBytesTypeMapping Default { get; } = new(); private YdbBytesTypeMapping() : base("Bytes", typeof(byte[]), System.Data.DbType.Binary, - jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance) + jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance, unicode: false) { } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs index 4dd97966..5e6f0226 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -150,28 +150,23 @@ RelationalTypeMappingSourceDependencies relationalDependencies return clrType is null ? null : ClrTypeMapping.GetValueOrDefault(clrType); } - protected override RelationalTypeMapping? FindCollectionMapping( - RelationalTypeMappingInfo info, - Type modelType, - Type? providerType, - CoreTypeMapping? elementMapping - ) + public override RelationalTypeMapping? FindMapping(Type type) { - var elementType = modelType.IsArray - ? modelType.GetElementType() - : modelType.GetInterfaces() + var elementType = type.IsArray + ? type.GetElementType() + : type.GetInterfaces() .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IList<>))? .GetGenericArguments()[0]; if (elementType == null) - return null; + return base.FindMapping(type); elementType = Nullable.GetUnderlyingType(elementType) ?? elementType; var elementTypeMapping = FindMapping(elementType); if (elementTypeMapping == null) - return null; + return base.FindMapping(type); var ydbDbType = elementTypeMapping is IYdbTypeMapping ydbTypeMapping ? ydbTypeMapping.YdbDbType From 89da6f6fa348cdc480ed0c277c3b4a136f1a9797 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Fri, 7 Nov 2025 13:08:26 +0300 Subject: [PATCH 10/12] added cache --- src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs | 3 ++- .../SqlQueryCollectionParameterTests.cs | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs index 5e6f0226..e613c147 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -17,6 +17,7 @@ RelationalTypeMappingSourceDependencies relationalDependencies ) : RelationalTypeMappingSource(dependencies, relationalDependencies) { private static readonly ConcurrentDictionary DecimalCache = new(); + private static readonly ConcurrentDictionary ListMappings = new(); #region Mappings @@ -172,6 +173,6 @@ RelationalTypeMappingSourceDependencies relationalDependencies ? ydbTypeMapping.YdbDbType : (elementTypeMapping.DbType ?? DbType.Object).ToYdbDbType(); - return new YdbListTypeMapping(ydbDbType, elementTypeMapping.StoreType); + return ListMappings.GetOrAdd(ydbDbType, _ => new YdbListTypeMapping(ydbDbType, elementTypeMapping.StoreType)); } } diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs index 33255588..0b017c94 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs @@ -83,19 +83,19 @@ public static IEnumerable GetCollectionTestCases() [Theory] [MemberData(nameof(GetCollectionTestCases))] - public async Task SqlQuery_DoesNotExpandCollectionParameter_InClause(IEnumerable listIds) + public async Task SqlQuery_DoesNotExpandCollectionParameter_InClause(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(listIds.Where(value => value != null) + 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 {listIds}").ToListAsync(); + $"SELECT * FROM TestEntity WHERE Value IN {listValues}").ToListAsync(); Assert.Equal(3, rows.Count); } From a5aa932c88a381ee5b626103b3d00843fc51dc91 Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Fri, 7 Nov 2025 13:41:33 +0300 Subject: [PATCH 11/12] update CHANGELOG.md --- src/EFCore.Ydb/CHANGELOG.md | 1 + .../Storage/Internal/YdbTypeMappingSource.cs | 3 +++ .../SqlQueryCollectionParameterTests.cs | 20 ++++++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) 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/Storage/Internal/YdbTypeMappingSource.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs index e613c147..6f03cab5 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -153,6 +153,9 @@ RelationalTypeMappingSourceDependencies relationalDependencies public override RelationalTypeMapping? FindMapping(Type type) { + if (type == typeof(byte[])) + return base.FindMapping(type); + var elementType = type.IsArray ? type.GetElementType() : type.GetInterfaces() diff --git a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs index 0b017c94..0fe025e9 100644 --- a/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs +++ b/src/EFCore.Ydb/test/EntityFrameworkCore.Ydb.FunctionalTests/SqlQueryCollectionParameterTests.cs @@ -83,7 +83,7 @@ public static IEnumerable GetCollectionTestCases() [Theory] [MemberData(nameof(GetCollectionTestCases))] - public async Task SqlQuery_DoesNotExpandCollectionParameter_InClause(IEnumerable listValues) + public async Task SqlQuery_UsesListParameterForInClause(IEnumerable listValues) { await using var testStore = YdbTestStoreFactory.Instance.Create("SqlQueryCollectionParameterTests"); await using var testDbContext = new TestDbContext(); @@ -100,6 +100,24 @@ public async Task SqlQuery_DoesNotExpandCollectionParameter_InClause(IEnumera 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>(); From 122515e50ebf33f62d522422da7c1acbfe4da1eb Mon Sep 17 00:00:00 2001 From: KirillKurdyukov Date: Fri, 7 Nov 2025 13:52:54 +0300 Subject: [PATCH 12/12] delete allocations --- .../src/Storage/Internal/YdbTypeMappingSource.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs index 6f03cab5..b01b160e 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbTypeMappingSource.cs @@ -176,6 +176,12 @@ RelationalTypeMappingSourceDependencies relationalDependencies ? ydbTypeMapping.YdbDbType : (elementTypeMapping.DbType ?? DbType.Object).ToYdbDbType(); - return ListMappings.GetOrAdd(ydbDbType, _ => new YdbListTypeMapping(ydbDbType, elementTypeMapping.StoreType)); + if (ListMappings.TryGetValue(ydbDbType, out var mapping)) + return mapping; + + mapping = new YdbListTypeMapping(ydbDbType, elementTypeMapping.StoreType); + ListMappings.TryAdd(ydbDbType, mapping); + + return mapping; } }