Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Marten;
using Marten.Linq;
using Marten.Testing.Harness;
using Shouldly;

namespace LinqTests.Bugs;

public class Bug_4169_compiled_query_contains_startswith_endswith_escape : BugIntegrationContext
{
public record WildcardDocument(Guid Id, string Name);

// Each compiled query type must be unique to guarantee fresh code generation

public class ContainsWithPercent : ICompiledListQuery<WildcardDocument>
{
public string Name { get; set; }

public ContainsWithPercent(string name) => Name = name;

public Expression<Func<IMartenQueryable<WildcardDocument>, IEnumerable<WildcardDocument>>> QueryIs()
=> q => q.Where(x => x.Name.Contains(Name));
}

public class StartsWithWithPercent : ICompiledListQuery<WildcardDocument>
{
public string Name { get; set; }

public StartsWithWithPercent(string name) => Name = name;

public Expression<Func<IMartenQueryable<WildcardDocument>, IEnumerable<WildcardDocument>>> QueryIs()
=> q => q.Where(x => x.Name.StartsWith(Name));
}

public class EndsWithWithPercent : ICompiledListQuery<WildcardDocument>
{
public string Name { get; set; }

public EndsWithWithPercent(string name) => Name = name;

public Expression<Func<IMartenQueryable<WildcardDocument>, IEnumerable<WildcardDocument>>> QueryIs()
=> q => q.Where(x => x.Name.EndsWith(Name));
}

[Fact]
public async Task compiled_contains_should_not_treat_percent_as_wildcard()
{
var match = new WildcardDocument(Guid.NewGuid(), "100% Complete");
var noMatch = new WildcardDocument(Guid.NewGuid(), "100 Complete");
theSession.Store(match);
theSession.Store(noMatch);
await theSession.SaveChangesAsync();

var results = (await theSession.QueryAsync(new ContainsWithPercent("100%"))).ToList();

results.Count.ShouldBe(1);
results[0].Id.ShouldBe(match.Id);
}

[Fact]
public async Task compiled_starts_with_should_not_treat_percent_as_wildcard()
{
var match = new WildcardDocument(Guid.NewGuid(), "100% of target");
var noMatch = new WildcardDocument(Guid.NewGuid(), "100 of target");
theSession.Store(match);
theSession.Store(noMatch);
await theSession.SaveChangesAsync();

var results = (await theSession.QueryAsync(new StartsWithWithPercent("100%"))).ToList();

results.Count.ShouldBe(1);
results[0].Id.ShouldBe(match.Id);
}

[Fact]
public async Task compiled_ends_with_should_not_treat_percent_as_wildcard()
{
var match = new WildcardDocument(Guid.NewGuid(), "score: 100%");
var noMatch = new WildcardDocument(Guid.NewGuid(), "score: 100");
theSession.Store(match);
theSession.Store(noMatch);
await theSession.SaveChangesAsync();

var results = (await theSession.QueryAsync(new EndsWithWithPercent("100%"))).ToList();

results.Count.ShouldBe(1);
results[0].Id.ShouldBe(match.Id);
}
}
103 changes: 103 additions & 0 deletions src/LinqTests/Bugs/Bug_4169_compiled_query_ilike_escape.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Marten;
using Marten.Linq;
using Marten.Testing.Harness;
using Shouldly;

namespace LinqTests.Bugs;

public class Bug_4169_compiled_query_ilike_escape : BugIntegrationContext
{
public record IlikeEscapeDocument(Guid Id, string DisplayName);

// Separate compiled query types per test to guarantee each triggers fresh code generation
// with the specific special character value

public class FindByDisplayNameWithPercent : ICompiledListQuery<IlikeEscapeDocument>
{
public string DisplayName { get; set; }

public FindByDisplayNameWithPercent(string displayName)
{
DisplayName = displayName;
}

public Expression<Func<IMartenQueryable<IlikeEscapeDocument>, IEnumerable<IlikeEscapeDocument>>> QueryIs()
{
return q => q.Where(x => x.DisplayName.Equals(DisplayName, StringComparison.InvariantCultureIgnoreCase));
}
}

public class FindByDisplayNameWithUnderscore : ICompiledListQuery<IlikeEscapeDocument>
{
public string DisplayName { get; set; }

public FindByDisplayNameWithUnderscore(string displayName)
{
DisplayName = displayName;
}

public Expression<Func<IMartenQueryable<IlikeEscapeDocument>, IEnumerable<IlikeEscapeDocument>>> QueryIs()
{
return q => q.Where(x => x.DisplayName.Equals(DisplayName, StringComparison.InvariantCultureIgnoreCase));
}
}

public class FindByDisplayNameWithBackslash : ICompiledListQuery<IlikeEscapeDocument>
{
public string DisplayName { get; set; }

public FindByDisplayNameWithBackslash(string displayName)
{
DisplayName = displayName;
}

public Expression<Func<IMartenQueryable<IlikeEscapeDocument>, IEnumerable<IlikeEscapeDocument>>> QueryIs()
{
return q => q.Where(x => x.DisplayName.Equals(DisplayName, StringComparison.InvariantCultureIgnoreCase));
}
}

[Fact]
public async Task compiled_query_with_percentage_in_equals_ignore_case()
{
var doc = new IlikeEscapeDocument(Guid.NewGuid(), "100% Complete");
theSession.Store(doc);
await theSession.SaveChangesAsync();

var results = (await theSession.QueryAsync(new FindByDisplayNameWithPercent("100% Complete"))).ToList();

results.Count.ShouldBe(1);
results[0].Id.ShouldBe(doc.Id);
}

[Fact]
public async Task compiled_query_with_underscore_in_equals_ignore_case()
{
var doc = new IlikeEscapeDocument(Guid.NewGuid(), "hello_world");
theSession.Store(doc);
await theSession.SaveChangesAsync();

var results = (await theSession.QueryAsync(new FindByDisplayNameWithUnderscore("hello_world"))).ToList();

results.Count.ShouldBe(1);
results[0].Id.ShouldBe(doc.Id);
}

[Fact]
public async Task compiled_query_with_backslash_in_equals_ignore_case()
{
var doc = new IlikeEscapeDocument(Guid.NewGuid(), @"path\to\file");
theSession.Store(doc);
await theSession.SaveChangesAsync();

var results = (await theSession.QueryAsync(new FindByDisplayNameWithBackslash(@"path\to\file"))).ToList();

results.Count.ShouldBe(1);
results[0].Id.ShouldBe(doc.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Marten;
using Marten.Linq;
using Marten.Testing.Harness;
using Shouldly;

namespace LinqTests.Bugs;

public class Bug_4169_compiled_query_startswith_endswith_swap : BugIntegrationContext
{
public record SwapDocument(Guid Id, string Name);

// Select a scalar value to force StatelessCompiledQuery (no document selector)

public class StartsWithProjection : ICompiledListQuery<SwapDocument, string>
{
public string Prefix { get; set; }

public StartsWithProjection(string prefix) => Prefix = prefix;

public Expression<Func<IMartenQueryable<SwapDocument>, IEnumerable<string>>> QueryIs()
=> q => q.Where(x => x.Name.StartsWith(Prefix)).Select(x => x.Name);
}

public class EndsWithProjection : ICompiledListQuery<SwapDocument, string>
{
public string Suffix { get; set; }

public EndsWithProjection(string suffix) => Suffix = suffix;

public Expression<Func<IMartenQueryable<SwapDocument>, IEnumerable<string>>> QueryIs()
=> q => q.Where(x => x.Name.EndsWith(Suffix)).Select(x => x.Name);
}

[Fact]
public async Task compiled_starts_with_projection_should_match_prefix_not_suffix()
{
var match = new SwapDocument(Guid.NewGuid(), "hello world");
var noMatch = new SwapDocument(Guid.NewGuid(), "world hello");
theSession.Store(match);
theSession.Store(noMatch);
await theSession.SaveChangesAsync();

var results = (await theSession.QueryAsync(new StartsWithProjection("hello"))).ToList();

results.Count.ShouldBe(1);
results[0].ShouldBe("hello world");
}

[Fact]
public async Task compiled_ends_with_projection_should_match_suffix_not_prefix()
{
var match = new SwapDocument(Guid.NewGuid(), "world hello");
var noMatch = new SwapDocument(Guid.NewGuid(), "hello world");
theSession.Store(match);
theSession.Store(noMatch);
await theSession.SaveChangesAsync();

var results = (await theSession.QueryAsync(new EndsWithProjection("hello"))).ToList();

results.Count.ShouldBe(1);
results[0].ShouldBe("world hello");
}
}
16 changes: 13 additions & 3 deletions src/Marten/Internal/CompiledQueries/ClonedCompiledQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,26 @@ public Task<TOut> HandleAsync(DbDataReader reader, IMartenSession session, Cance

protected string StartsWith(string value)
{
return $"{value}%";
return $"{EscapeLikeValue(value)}%";
}

protected string ContainsString(string value)
{
return $"%{value}%";
return $"%{EscapeLikeValue(value)}%";
}

protected string EndsWith(string value)
{
return $"%{value}";
return $"%{EscapeLikeValue(value)}";
}

protected string EqualsIgnoreCaseValue(string value)
{
return EscapeLikeValue(value);
}

private static string EscapeLikeValue(string value)
{
return value.Replace("\\", "\\\\").Replace("%", "\\%").Replace("_", "\\_");
}
}
16 changes: 13 additions & 3 deletions src/Marten/Internal/CompiledQueries/ComplexCompiledQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,26 @@ public Task<TOut> HandleAsync(DbDataReader reader, IMartenSession session, Cance

protected string StartsWith(string value)
{
return $"%{value}";
return $"{EscapeLikeValue(value)}%";
}

protected string ContainsString(string value)
{
return $"%{value}%";
return $"%{EscapeLikeValue(value)}%";
}

protected string EndsWith(string value)
{
return $"{value}%";
return $"%{EscapeLikeValue(value)}";
}

protected string EqualsIgnoreCaseValue(string value)
{
return EscapeLikeValue(value);
}

private static string EscapeLikeValue(string value)
{
return value.Replace("\\", "\\\\").Replace("%", "\\%").Replace("_", "\\_");
}
}
16 changes: 13 additions & 3 deletions src/Marten/Internal/CompiledQueries/StatelessCompiledQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,26 @@ public Task<TOut> HandleAsync(DbDataReader reader, IMartenSession session, Cance

protected string StartsWith(string value)
{
return $"%{value}";
return $"{EscapeLikeValue(value)}%";
}

protected string ContainsString(string value)
{
return $"%{value}%";
return $"%{EscapeLikeValue(value)}%";
}

protected string EndsWith(string value)
{
return $"{value}%";
return $"%{EscapeLikeValue(value)}";
}

protected string EqualsIgnoreCaseValue(string value)
{
return EscapeLikeValue(value);
}

private static string EscapeLikeValue(string value)
{
return value.Replace("\\", "\\\\").Replace("%", "\\%").Replace("_", "\\_");
}
}
Loading
Loading