Skip to content

Commit a35fd27

Browse files
committed
Fix LIKE escape type mapping with value converters
Keep LIKE and ILIKE escape expressions independent from match and pattern type mapping so property converters cannot rewrite provider-generated escape values. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e5b861c2-ea5b-411c-8efe-3d925d29c52e
1 parent 6cb59bb commit a35fd27

2 files changed

Lines changed: 71 additions & 7 deletions

File tree

‎src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs‎

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ public virtual PgFunctionExpression AggregateFunction(
394394
sqlExpression = sqlExpression switch
395395
{
396396
SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping),
397+
LikeExpression e => ApplyTypeMappingOnLike(e),
397398

398399
// PostgreSQL-specific expression types
399400
PgAnyExpression e => ApplyTypeMappingOnAny(e),
@@ -710,20 +711,30 @@ private SqlExpression ApplyTypeMappingOnArraySlice(
710711
array.TypeMapping);
711712
}
712713

714+
// The base implementation is private, so duplicate it here to keep the escape character's type mapping independent.
715+
private SqlExpression ApplyTypeMappingOnLike(LikeExpression likeExpression)
716+
{
717+
var inferredTypeMapping = ExpressionExtensions.InferTypeMapping(likeExpression.Match, likeExpression.Pattern)
718+
?? _typeMappingSource.FindMapping(likeExpression.Match.Type, Dependencies.Model);
719+
720+
return new LikeExpression(
721+
ApplyTypeMapping(likeExpression.Match, inferredTypeMapping),
722+
ApplyTypeMapping(likeExpression.Pattern, inferredTypeMapping),
723+
// The escape character must not inherit the match or pattern's value converter (#3888).
724+
ApplyDefaultTypeMapping(likeExpression.EscapeChar),
725+
_boolTypeMapping);
726+
}
727+
713728
private SqlExpression ApplyTypeMappingOnILike(PgILikeExpression ilikeExpression)
714729
{
715-
var inferredTypeMapping = (ilikeExpression.EscapeChar is null
716-
? ExpressionExtensions.InferTypeMapping(
717-
ilikeExpression.Match, ilikeExpression.Pattern)
718-
: ExpressionExtensions.InferTypeMapping(
719-
ilikeExpression.Match, ilikeExpression.Pattern,
720-
ilikeExpression.EscapeChar))
730+
var inferredTypeMapping = ExpressionExtensions.InferTypeMapping(ilikeExpression.Match, ilikeExpression.Pattern)
721731
?? _typeMappingSource.FindMapping(ilikeExpression.Match.Type, Dependencies.Model);
722732

723733
return new PgILikeExpression(
724734
ApplyTypeMapping(ilikeExpression.Match, inferredTypeMapping),
725735
ApplyTypeMapping(ilikeExpression.Pattern, inferredTypeMapping),
726-
ApplyTypeMapping(ilikeExpression.EscapeChar, inferredTypeMapping),
736+
// The escape character must not inherit the match or pattern's value converter (#3888).
737+
ApplyDefaultTypeMapping(ilikeExpression.EscapeChar),
727738
_boolTypeMapping);
728739
}
729740

‎test/EFCore.PG.FunctionalTests/Query/AdHocMiscellaneousQueryNpgsqlTest.cs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,57 @@ public static DateTime Modify(DateTime date)
8080
=> throw new NotSupportedException();
8181
}
8282
}
83+
84+
[ConditionalFact]
85+
public virtual async Task Like_with_implicit_escape_does_not_apply_value_converter()
86+
{
87+
var contextFactory = await InitializeNonSharedTest<Context3888>(
88+
seed: async context =>
89+
{
90+
context.Entities.Add(new Context3888.Entity { Value = "ABC" });
91+
await context.SaveChangesAsync();
92+
});
93+
94+
await using var context = contextFactory.CreateDbContext();
95+
96+
var pattern = "%ABC%";
97+
98+
Assert.Equal(1, await context.Entities.CountAsync(e => EF.Functions.Like(e.Value!, pattern)));
99+
Assert.Equal(1, await context.Entities.CountAsync(e => EF.Functions.ILike(e.Value!, pattern)));
100+
101+
AssertSql(
102+
"""
103+
@pattern='%ABC%'
104+
105+
SELECT count(*)::int
106+
FROM "Entities" AS e
107+
WHERE e."Value" LIKE @pattern ESCAPE ''
108+
""",
109+
//
110+
"""
111+
@pattern='%ABC%'
112+
113+
SELECT count(*)::int
114+
FROM "Entities" AS e
115+
WHERE e."Value" ILIKE @pattern ESCAPE ''
116+
""");
117+
}
118+
119+
protected class Context3888(DbContextOptions options) : DbContext(options)
120+
{
121+
public DbSet<Entity> Entities { get; set; } = null!;
122+
123+
protected override void OnModelCreating(ModelBuilder modelBuilder)
124+
=> modelBuilder.Entity<Entity>()
125+
.Property(e => e.Value)
126+
.HasConversion(
127+
value => string.IsNullOrWhiteSpace(value) ? null : value,
128+
value => value ?? string.Empty);
129+
130+
public class Entity
131+
{
132+
public int Id { get; set; }
133+
public string? Value { get; set; }
134+
}
135+
}
83136
}

0 commit comments

Comments
 (0)