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
Expand Up @@ -16,7 +16,7 @@ namespace QueryKit.IntegrationTests.Tests;
public class FilterParsingRegressionTests : TestBase
{
[Fact]
public async Task operator_alias_text_inside_quoted_value_is_replaced()
public async Task operator_alias_text_inside_quoted_value_is_kept()
{
// Arrange
var testingServiceScope = new TestingServiceScope();
Expand All @@ -42,7 +42,8 @@ public async Task operator_alias_text_inside_quoted_value_is_replaced()
var people = await appliedQueryable.ToListAsync();

// Assert
people.Should().BeEmpty();
people.Count.Should().Be(1);
people[0].Id.Should().Be(fakePersonOne.Id);
}

[Fact]
Expand Down
25 changes: 19 additions & 6 deletions QueryKit.UnitTests/OperatorAliasTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,27 +205,27 @@ public void can_use_ints()
}

[Fact]
public void alias_text_inside_quoted_value_is_replaced()
public void alias_text_inside_quoted_value_is_not_replaced()
{
var input = """Title eq "salt and pepper or eq gt" and Rating gt 3""";

var config = new CustomQueryKitConfiguration();
var filterExpression = FilterParser.ParseFilter<Recipe>(input, config);
filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title == "salt && pepper || == gt") AndAlso (x.Rating > 3))""");
filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title == "salt and pepper or eq gt") AndAlso (x.Rating > 3))""");
}

[Fact]
public void case_insensitive_alias_text_inside_quoted_value_is_replaced()
public void case_insensitive_alias_text_inside_quoted_value_is_not_replaced()
{
var input = """Title eqi "Whisk AND fry" """;

var config = new CustomQueryKitConfiguration();
var filterExpression = FilterParser.ParseFilter<Recipe>(input, config);
filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title != null) AndAlso (x.Title.ToLower() == "Whisk && fry".ToLower()))""");
filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title != null) AndAlso (x.Title.ToLower() == "Whisk AND fry".ToLower()))""");
}

[Fact]
public void can_use_alias_operator_with_query_name_and_alias_text_in_the_value_is_replaced()
public void can_use_alias_operator_with_query_name()
{
var input = """name eq "a eq b" """;

Expand All @@ -234,7 +234,20 @@ public void can_use_alias_operator_with_query_name_and_alias_text_in_the_value_i
settings.Property<Recipe>(x => x.Title).HasQueryName("name");
});
var filterExpression = FilterParser.ParseFilter<Recipe>(input, config);
filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "a == b")""");
filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "a eq b")""");
}

[Fact]
public void query_name_and_alias_text_inside_quoted_value_is_not_replaced()
{
var input = """name eq "name eq x" """;

var config = new CustomQueryKitConfiguration(settings =>
{
settings.Property<Recipe>(x => x.Title).HasQueryName("name");
});
var filterExpression = FilterParser.ParseFilter<Recipe>(input, config);
filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "name eq x")""");
}

public class CustomQueryKitConfiguration : QueryKitConfiguration
Expand Down
28 changes: 0 additions & 28 deletions QueryKit/Configuration/QueryKitConfigurationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,9 @@
namespace QueryKit.Configuration;

using System.Text.RegularExpressions;
using QueryKit.Exceptions;
using QueryKit.Operators;

internal static class QueryKitConfigurationExtensions
{
internal static string ReplaceComparisonAliases(this IQueryKitConfiguration configuration, string input)
{
var aliasedOperators = ComparisonOperator.GetAliasMatches(configuration);
foreach (var comparisonAliasMatch in aliasedOperators)
{
var escapedAlias = Regex.Escape(comparisonAliasMatch.Alias);
var regex = AliasRegexCache.Get($@"(?<=\s|^){escapedAlias}(?=\s|$)");
input = regex.Replace(input, comparisonAliasMatch.Operator);
}

return input;
}

internal static string ReplaceLogicalAliases(this IQueryKitConfiguration configuration, string input)
{
var aliasedOperators = LogicalOperator.GetAliasMatches(configuration);
foreach (var logicalAliasMatch in aliasedOperators)
{
var escapedAlias = Regex.Escape(logicalAliasMatch.Alias);
var regex = AliasRegexCache.Get($@"(?<=\s|^){escapedAlias}(?=\s|$)");
input = regex.Replace(input, logicalAliasMatch.Operator);
}

return input;
}

internal static string? GetPropertyPathByQueryName(this IQueryKitConfiguration configuration, string? queryName)
{
return configuration.PropertyMappings.GetPropertyPathByQueryName(queryName);
Expand Down
8 changes: 2 additions & 6 deletions QueryKit/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ public static class FilterParser
public static Expression<Func<T, bool>> ParseFilter<T>(string input, IQueryKitConfiguration? config = null)
{
EnsureWithinInputLength(input, config);

input = config?.ReplaceLogicalAliases(input) ?? input;
input = config?.ReplaceComparisonAliases(input) ?? input;

var parameter = Expression.Parameter(typeof(T), "x");
Expression expr;
Expand Down Expand Up @@ -238,13 +235,12 @@ private static Parser<IEnumerable<string>> PropertyListParser(Parser<string> pro

private static Parser<ComparisonOperator> ComparisonOperatorParser(IQueryKitConfiguration? config)
{
var operatorParser = CanonicalComparisonOperatorParser.Or(ComparisonOperatorAliasParser(config));
var operatorParser = ComparisonOperatorAliasParser(config).Or(CanonicalComparisonOperatorParser);
return Parse.Char(ComparisonOperator.AllPrefix).Optional().Select(opt => opt.IsDefined)
.Then(hasHash => operatorParser.Select(x => ComparisonOperator.GetByOperatorString(x.Operator, x.CaseInsensitive, hasHash)));
}

// The rewrite before the parse replaces each alias that stands between whitespace, like v1.14.2.
// The grammar reads an alias that the rewrite did not replace, for example `(Age)eq 3`.
// Aliases are matched in the grammar (not by rewriting the input) so text inside quoted values is never changed.
// Longer aliases are tried first so an alias that starts with another alias (e.g. `@@$$` and `@@$`) still matches.
private static Parser<(string Operator, bool CaseInsensitive)> ComparisonOperatorAliasParser(IQueryKitConfiguration? config)
{
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,8 @@ var filterExpression = FilterParser.ParseFilter<Recipe>(input, config);
> `Title @@$ "titilating"` ✅
> `Title@@$"titilating"` ❌

QueryKit reads a custom operator only where an operator can stand. Operator text inside a quoted value does not change. For example, `Title eq "salt and pepper"` compares `Title` with `salt and pepper`.

#### Allow Unknown Properties

By default, QueryKit will throw an error if it doesn't recognize a property name, If you want to loosen the reigns here a bit, you can set `AllowUnknownProperties` to `true` in your config. When active, unknown properties will be ignored in the expression resolution.
Expand Down
Loading