diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs index 3ac07d17f..7fa8de2f0 100644 --- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs +++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs @@ -394,6 +394,7 @@ public virtual PgFunctionExpression AggregateFunction( sqlExpression = sqlExpression switch { SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping), + LikeExpression e => ApplyTypeMappingOnLike(e), // PostgreSQL-specific expression types PgAnyExpression e => ApplyTypeMappingOnAny(e), @@ -710,20 +711,30 @@ private SqlExpression ApplyTypeMappingOnArraySlice( array.TypeMapping); } + // The base implementation is private, so duplicate it here to keep the escape character's type mapping independent. + private SqlExpression ApplyTypeMappingOnLike(LikeExpression likeExpression) + { + var inferredTypeMapping = ExpressionExtensions.InferTypeMapping(likeExpression.Match, likeExpression.Pattern) + ?? _typeMappingSource.FindMapping(likeExpression.Match.Type, Dependencies.Model); + + return new LikeExpression( + ApplyTypeMapping(likeExpression.Match, inferredTypeMapping), + ApplyTypeMapping(likeExpression.Pattern, inferredTypeMapping), + // The escape character must not inherit the match or pattern's value converter (#3888). + ApplyDefaultTypeMapping(likeExpression.EscapeChar), + _boolTypeMapping); + } + private SqlExpression ApplyTypeMappingOnILike(PgILikeExpression ilikeExpression) { - var inferredTypeMapping = (ilikeExpression.EscapeChar is null - ? ExpressionExtensions.InferTypeMapping( - ilikeExpression.Match, ilikeExpression.Pattern) - : ExpressionExtensions.InferTypeMapping( - ilikeExpression.Match, ilikeExpression.Pattern, - ilikeExpression.EscapeChar)) + var inferredTypeMapping = ExpressionExtensions.InferTypeMapping(ilikeExpression.Match, ilikeExpression.Pattern) ?? _typeMappingSource.FindMapping(ilikeExpression.Match.Type, Dependencies.Model); return new PgILikeExpression( ApplyTypeMapping(ilikeExpression.Match, inferredTypeMapping), ApplyTypeMapping(ilikeExpression.Pattern, inferredTypeMapping), - ApplyTypeMapping(ilikeExpression.EscapeChar, inferredTypeMapping), + // The escape character must not inherit the match or pattern's value converter (#3888). + ApplyDefaultTypeMapping(ilikeExpression.EscapeChar), _boolTypeMapping); } diff --git a/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs index 1c4546f47..a5bfc2544 100644 --- a/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs @@ -80,4 +80,57 @@ public static DateTime Modify(DateTime date) => throw new NotSupportedException(); } } + + [ConditionalFact] + public virtual async Task Like_with_implicit_escape_does_not_apply_value_converter() + { + var contextFactory = await InitializeNonSharedTest( + seed: async context => + { + context.Entities.Add(new Context3888.Entity { Value = "ABC" }); + await context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var pattern = "%ABC%"; + + Assert.Equal(1, await context.Entities.CountAsync(e => EF.Functions.Like(e.Value!, pattern))); + Assert.Equal(1, await context.Entities.CountAsync(e => EF.Functions.ILike(e.Value!, pattern))); + + AssertSql( + """ +@pattern='%ABC%' + +SELECT count(*)::int +FROM "Entities" AS e +WHERE e."Value" LIKE @pattern ESCAPE '' +""", + // + """ +@pattern='%ABC%' + +SELECT count(*)::int +FROM "Entities" AS e +WHERE e."Value" ILIKE @pattern ESCAPE '' +"""); + } + + protected class Context3888(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity() + .Property(e => e.Value) + .HasConversion( + value => string.IsNullOrWhiteSpace(value) ? null : value, + value => value ?? string.Empty); + + public class Entity + { + public int Id { get; set; } + public string? Value { get; set; } + } + } }