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
16 changes: 8 additions & 8 deletions QueryKit.UnitTests/FilterParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void complex_with_lots_of_types()

var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should()
.Be(""""x => (((((((((x.Title != null) AndAlso x.Title.ToLower().Contains("waffle & chicken".ToLower())) AndAlso (x.Age > 30)) OrElse (x.Id == aa648248-cb69-4217-ac95-d7484795afb2)) OrElse (x.Title == "lamb")) OrElse (x.Title == null)) AndAlso ((x.Age < 18) OrElse ((x.BirthMonth == new Nullable`1(January)) AndAlso x.Title.StartsWith("ally")))) OrElse (x.Rating > 3.5)) OrElse ((x.SpecificDate == new Nullable`1(new DateTimeOffset(637922304030000000, 00:00:00))) AndAlso ((x.Date == new Nullable`1(new DateOnly(2022, 7, 1))) OrElse (x.Time == new Nullable`1(new TimeOnly(0, 0, 3, 0, 0))))))"""");
.Be(""""x => (((((((((x.Title != null) AndAlso x.Title.ToLower().Contains("waffle & chicken".ToLower())) AndAlso (x.Age > 30)) OrElse (x.Id == aa648248-cb69-4217-ac95-d7484795afb2)) OrElse (x.Title == "lamb")) OrElse (x.Title == null)) AndAlso ((x.Age < 18) OrElse ((x.BirthMonth == new Nullable`1(January)) AndAlso ((x.Title != null) AndAlso x.Title.StartsWith("ally"))))) OrElse (x.Rating > 3.5)) OrElse ((x.SpecificDate == new Nullable`1(new DateTimeOffset(637922304030000000, 00:00:00))) AndAlso ((x.Date == new Nullable`1(new DateOnly(2022, 7, 1))) OrElse (x.Time == new Nullable`1(new TimeOnly(0, 0, 3, 0, 0))))))"""");
}

[Fact]
Expand Down Expand Up @@ -173,7 +173,7 @@ public void starts_with_operator()
{
var input = """Title _= "lam" """;
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be("x => x.Title.StartsWith(\"lam\")");
filterExpression.ToDisplayString().Should().Be("x => ((x.Title != null) AndAlso x.Title.StartsWith(\"lam\"))");
}

[Fact]
Expand All @@ -189,7 +189,7 @@ public void ends_with_operator()
{
var input = """Title _-= "b" """;
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be("x => x.Title.EndsWith(\"b\")");
filterExpression.ToDisplayString().Should().Be("x => ((x.Title != null) AndAlso x.Title.EndsWith(\"b\"))");
}

[Fact]
Expand Down Expand Up @@ -372,15 +372,15 @@ public void multiple_properties_and_operators()
{
var input = """Title _= "lamb" && Age >= 25 && Rating < 4.5 && SpecificDate <= 2022-07-01T00:00:03Z && Time == 00:00:03""";
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be("x => ((((x.Title.StartsWith(\"lamb\") AndAlso (x.Age >= 25)) AndAlso (x.Rating < 4.5)) AndAlso (x.SpecificDate <= new Nullable`1(new DateTimeOffset(637922304030000000, 00:00:00)))) AndAlso (x.Time == new Nullable`1(new TimeOnly(0, 0, 3, 0, 0))))");
filterExpression.ToDisplayString().Should().Be("x => ((((((x.Title != null) AndAlso x.Title.StartsWith(\"lamb\")) AndAlso (x.Age >= 25)) AndAlso (x.Rating < 4.5)) AndAlso (x.SpecificDate <= new Nullable`1(new DateTimeOffset(637922304030000000, 00:00:00)))) AndAlso (x.Time == new Nullable`1(new TimeOnly(0, 0, 3, 0, 0))))");
}

[Fact]
public void complex_filter_with_nested_parentheses()
{
var input = """(Title == "lamb" && ((Age >= 25 && Rating < 4.5) || (SpecificDate <= 2022-07-01T00:00:03Z && Time == 00:00:03)) && (Favorite == true || Email.Value _= "example"))""";
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be("""x => (((x.Title == "lamb") AndAlso (((x.Age >= 25) AndAlso (x.Rating < 4.5)) OrElse ((x.SpecificDate <= new Nullable`1(new DateTimeOffset(637922304030000000, 00:00:00))) AndAlso (x.Time == new Nullable`1(new TimeOnly(0, 0, 3, 0, 0)))))) AndAlso ((x.Favorite == True) OrElse x.Email.Value.StartsWith("example")))""");
filterExpression.ToDisplayString().Should().Be("""x => (((x.Title == "lamb") AndAlso (((x.Age >= 25) AndAlso (x.Rating < 4.5)) OrElse ((x.SpecificDate <= new Nullable`1(new DateTimeOffset(637922304030000000, 00:00:00))) AndAlso (x.Time == new Nullable`1(new TimeOnly(0, 0, 3, 0, 0)))))) AndAlso ((x.Favorite == True) OrElse ((x.Email.Value != null) AndAlso x.Email.Value.StartsWith("example"))))""");
}

[Fact]
Expand Down Expand Up @@ -412,7 +412,7 @@ public void ends_with_works()
{
var input = """Title _-= "lamb" """;
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be("x => x.Title.EndsWith(\"lamb\")");
filterExpression.ToDisplayString().Should().Be("x => ((x.Title != null) AndAlso x.Title.EndsWith(\"lamb\"))");
}

[Fact]
Expand All @@ -428,15 +428,15 @@ public void contains_is_case_sensitive()
{
var input = """Title @= "lamb" """;
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be("x => x.Title.Contains(\"lamb\")");
filterExpression.ToDisplayString().Should().Be("x => ((x.Title != null) AndAlso x.Title.Contains(\"lamb\"))");
}

[Fact]
public void not_contains_works()
{
var input = """Title !@= "lamb" """;
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be("x => Not(x.Title.Contains(\"lamb\"))");
filterExpression.ToDisplayString().Should().Be("x => ((x.Title == null) OrElse Not(x.Title.Contains(\"lamb\")))");
}

[Fact]
Expand Down
18 changes: 9 additions & 9 deletions QueryKit.UnitTests/FilterParsingRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,13 @@ public void int_property_compares_to_decimal_property(string input, string expec
}

[Theory]
[InlineData("""Title @= "am" """)]
[InlineData("""Title _= "la" """)]
[InlineData("""Title _-= "mb" """)]
[InlineData("""Title !@= "am" """)]
[InlineData("""Title !_= "la" """)]
[InlineData("""Title !_-= "mb" """)]
public void case_sensitive_string_operator_on_null_property_throws_in_memory(string input)
[InlineData("""Title @= "am" """, new[] { "lamb" })]
[InlineData("""Title _= "la" """, new[] { "lamb" })]
[InlineData("""Title _-= "mb" """, new[] { "lamb" })]
[InlineData("""Title !@= "am" """, new[] { "null", "other" })]
[InlineData("""Title !_= "la" """, new[] { "null", "other" })]
[InlineData("""Title !_-= "mb" """, new[] { "null", "other" })]
public void case_sensitive_string_operator_handles_null_property(string input, string[] expectedFirstNames)
{
var people = new[]
{
Expand All @@ -202,9 +202,9 @@ public void case_sensitive_string_operator_on_null_property_throws_in_memory(str
new TestingPerson { Title = "other", FirstName = "other" },
};

var act = () => people.AsQueryable().ApplyQueryKitFilter(input).ToList();
var result = people.AsQueryable().ApplyQueryKitFilter(input).ToList();

act.Should().Throw<NullReferenceException>();
result.Select(x => x.FirstName).Should().Equal(expectedFirstNames);
}

public static IEnumerable<object[]> ComparisonOperatorFactories() =>
Expand Down
2 changes: 1 addition & 1 deletion QueryKit.UnitTests/OperatorAliasTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public void can_use_contains_not_case_sensitive()
config.CaseInsensitiveAppendix = "$";
});
var filterExpression = FilterParser.ParseFilter<Recipe>(input, config);
filterExpression.ToDisplayString().Should().Be($"""x => x.Title.Contains("titilating")""");
filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title != null) AndAlso x.Title.Contains("titilating"))""");
}

[Fact]
Expand Down
30 changes: 24 additions & 6 deletions QueryKit/Operators/ComparisonOperator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,10 @@ public override Expression GetExpression<T>(Expression left, Expression right, T
return Expression.AndAlso(nullCheck, containsCall);
}

return Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, right);
// null doesn't contain anything, so we need: left != null && left.Contains(right)
return Expression.AndAlso(
Expression.NotEqual(left, Expression.Constant(null, typeof(string))),
Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, right));
}
}

Expand Down Expand Up @@ -415,7 +418,10 @@ public override Expression GetExpression<T>(Expression left, Expression right, T
return Expression.AndAlso(nullCheck, startsWithCall);
}

return Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, right);
// null doesn't start with anything, so we need: left != null && left.StartsWith(right)
return Expression.AndAlso(
Expression.NotEqual(left, Expression.Constant(null, typeof(string))),
Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, right));
}
}

Expand Down Expand Up @@ -447,7 +453,10 @@ public override Expression GetExpression<T>(Expression left, Expression right, T
return Expression.AndAlso(nullCheck, endsWithCall);
}

return Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, right);
// null doesn't end with anything, so we need: left != null && left.EndsWith(right)
return Expression.AndAlso(
Expression.NotEqual(left, Expression.Constant(null, typeof(string))),
Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, right));
}
}

Expand Down Expand Up @@ -479,7 +488,10 @@ public override Expression GetExpression<T>(Expression left, Expression right, T
return Expression.OrElse(nullCheck, notContainsCall);
}

return Expression.Not(Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, right));
// null doesn't contain anything, so it should be included: left == null || !left.Contains(right)
return Expression.OrElse(
Expression.Equal(left, Expression.Constant(null, typeof(string))),
Expression.Not(Expression.Call(left, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, right)));
}
}

Expand Down Expand Up @@ -511,7 +523,10 @@ public override Expression GetExpression<T>(Expression left, Expression right, T
return Expression.OrElse(nullCheck, notStartsWithCall);
}

return Expression.Not(Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, right));
// null doesn't start with anything, so it should be included: left == null || !left.StartsWith(right)
return Expression.OrElse(
Expression.Equal(left, Expression.Constant(null, typeof(string))),
Expression.Not(Expression.Call(left, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, right)));
}
}

Expand Down Expand Up @@ -543,7 +558,10 @@ public override Expression GetExpression<T>(Expression left, Expression right, T
return Expression.OrElse(nullCheck, notEndsWithCall);
}

return Expression.Not(Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, right));
// null doesn't end with anything, so it should be included: left == null || !left.EndsWith(right)
return Expression.OrElse(
Expression.Equal(left, Expression.Constant(null, typeof(string))),
Expression.Not(Expression.Call(left, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, right)));
}
}

Expand Down
Loading