Skip to content
Open
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
36 changes: 36 additions & 0 deletions QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,40 @@ public async Task has_returns_matching_rows(string input, string[] expectedTitle
// Assert
recipes.Select(x => x.Title[prefix.Length..]).Should().BeEquivalentTo(expectedTitles);
}

[Theory]
[InlineData("SpecificDateTime == 2024-01-15T08:00:00")]
[InlineData("SpecificDateTime == 2024-01-15T10:00:00+02:00")]
[InlineData("SpecificDateTime ^^ [2024-01-15T08:00:00]")]
[InlineData("SpecificDateTime ^^ [2024-01-15T10:00:00+02:00]")]
[InlineData("SpecificDate == 2024-01-15T08:00:00")]
[InlineData("SpecificDate == 2024-01-15T10:00:00+02:00")]
[InlineData("SpecificDate ^^ [2024-01-15T08:00:00]")]
[InlineData("SpecificDate ^^ [2024-01-15T10:00:00+02:00]")]
public async Task date_time_without_offset_is_utc(string valueFilter)
{
// Arrange
var testingServiceScope = new TestingServiceScope();
var title = $"utc {Guid.NewGuid()}";
var fakePersonOne = new FakeTestingPersonBuilder()
.WithTitle(title)
.WithSpecificDateTime(new DateTime(2024, 1, 15, 8, 0, 0, DateTimeKind.Utc))
.WithSpecificDate(new DateTimeOffset(2024, 1, 15, 8, 0, 0, TimeSpan.Zero))
.Build();
var fakePersonTwo = new FakeTestingPersonBuilder()
.WithTitle(title)
.WithSpecificDateTime(new DateTime(2024, 1, 15, 9, 0, 0, DateTimeKind.Utc))
.WithSpecificDate(new DateTimeOffset(2024, 1, 15, 9, 0, 0, TimeSpan.Zero))
.Build();
await testingServiceScope.InsertAsync(fakePersonOne, fakePersonTwo);

var input = $"""{nameof(TestingPerson.Title)} == "{title}" && {valueFilter}""";

// Act
var queryablePeople = testingServiceScope.DbContext().People;
var people = await queryablePeople.ApplyQueryKitFilter(input).ToListAsync();

// Assert
people.Select(x => x.Id).Should().Equal(fakePersonOne.Id);
}
}
6 changes: 3 additions & 3 deletions QueryKit.UnitTests/FilterParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ public void can_handle_datetime()
{
var input = """SpecificDateTime == 2022-07-01T00:00:03""";
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be(""""x => (x.SpecificDateTime == new DateTime(637922304030000000, Local))"""");
filterExpression.ToDisplayString().Should().Be(""""x => (x.SpecificDateTime == new DateTime(637922304030000000, Utc))"""");
}

[Fact]
Expand All @@ -292,7 +292,7 @@ public void can_handle_datetime_another()
{
var input = """SpecificDateTime == "2022-07-01T00:00:03" """;
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be(""""x => (x.SpecificDateTime == new DateTime(637922304030000000, Local))"""");
filterExpression.ToDisplayString().Should().Be(""""x => (x.SpecificDateTime == new DateTime(637922304030000000, Utc))"""");
}

[Fact]
Expand Down Expand Up @@ -340,7 +340,7 @@ public void can_handle_datetime_with_milliseconds()
{
var input = """SpecificDateTime == 2022-07-01T00:00:03.123""";
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.ToDisplayString().Should().Be("x => (x.SpecificDateTime == new DateTime(637922304031230000, Local))");
filterExpression.ToDisplayString().Should().Be("x => (x.SpecificDateTime == new DateTime(637922304031230000, Utc))");
}

[Fact]
Expand Down
44 changes: 44 additions & 0 deletions QueryKit.UnitTests/FilterParsingRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,50 @@ public void has_returns_matching_rows(string input, string[] expectedTitles)
result.Select(x => x.Title).Should().BeEquivalentTo(expectedTitles);
}

[Theory]
[InlineData("SpecificDateTime == 2024-01-15T08:00:00", "x => (x.SpecificDateTime == new DateTime(638409024000000000, Utc))")]
[InlineData("SpecificDateTime == \"2024-01-15T08:00:00\"", "x => (x.SpecificDateTime == new DateTime(638409024000000000, Utc))")]
[InlineData("SpecificDateTime == 2024-01-15T10:00:00+02:00", "x => (x.SpecificDateTime == new DateTime(638409024000000000, Utc))")]
[InlineData("SpecificDateTime == 2024-01-15T08:00:00Z", "x => (x.SpecificDateTime == new DateTime(638409024000000000, Utc))")]
[InlineData("SpecificDate == 2024-01-15T08:00:00", "x => (x.SpecificDate == new Nullable`1(new DateTimeOffset(638409024000000000, 00:00:00)))")]
[InlineData("SpecificDate == 2024-01-15T10:00:00+02:00", "x => (x.SpecificDate == new Nullable`1(new DateTimeOffset(638409024000000000, 00:00:00)))")]
public void date_time_without_offset_is_utc(string input, string expected)
{
var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);

filterExpression.ToDisplayString().Should().Be(expected);
}

[Theory]
[InlineData("SpecificDateTime ^^ [2024-01-15T08:00:00]")]
[InlineData("SpecificDateTime ^^ [2024-01-15T10:00:00+02:00]")]
[InlineData("SpecificDate ^^ [2024-01-15T08:00:00]")]
[InlineData("SpecificDate ^^ [2024-01-15T10:00:00+02:00]")]
public void date_time_list_value_matches_scalar_value(string input)
{
var people = new[]
{
new TestingPerson
{
Title = "match",
SpecificDateTime = new DateTime(2024, 1, 15, 8, 0, 0, DateTimeKind.Utc),
SpecificDate = new DateTimeOffset(2024, 1, 15, 8, 0, 0, TimeSpan.Zero),
},
new TestingPerson
{
Title = "other",
SpecificDateTime = new DateTime(2024, 1, 15, 9, 0, 0, DateTimeKind.Utc),
SpecificDate = new DateTimeOffset(2024, 1, 15, 9, 0, 0, TimeSpan.Zero),
},
};

var result = people.AsQueryable().ApplyQueryKitFilter(input).ToList();
var scalarResult = people.AsQueryable().ApplyQueryKitFilter(input.Replace("^^ [", "== ").TrimEnd(']')).ToList();

result.Select(x => x.Title).Should().Equal("match");
scalarResult.Select(x => x.Title).Should().Equal("match");
}

private static TResult WithCulture<TResult>(string cultureName, Func<TResult> action)
{
var originalCulture = CultureInfo.CurrentCulture;
Expand Down
28 changes: 15 additions & 13 deletions QueryKit/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,14 @@ from trailingSpaces in Parse.WhiteSpace.Many()
select LogicalOperator.GetByOperatorString(match.Operator)));
}

// A date or time value without an offset is read as UTC so the result does not depend on the server time zone.
// A value with an offset is converted to the same instant in UTC. Npgsql only accepts a DateTimeOffset parameter with offset 0.
private static DateTime ParseDateTime(string value)
=> DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);

private static DateTimeOffset ParseDateTimeOffset(string value)
=> DateTimeOffset.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToUniversalTime();

private static readonly Dictionary<Type, Func<string, object>> TypeConversionFunctions = new()
{
{ typeof(string), value => value },
Expand All @@ -372,8 +380,8 @@ from trailingSpaces in Parse.WhiteSpace.Many()
{ typeof(long), value => long.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(short), value => short.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(byte), value => byte.Parse(value, CultureInfo.InvariantCulture) },
{ typeof(DateTime), value => DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal) },
{ typeof(DateTimeOffset), value => DateTimeOffset.Parse(value).ToUniversalTime() },
{ typeof(DateTime), value => ParseDateTime(value) },
{ typeof(DateTimeOffset), value => ParseDateTimeOffset(value) },
{ typeof(DateOnly), value => DateOnly.Parse(value) },
{ typeof(TimeOnly), value => TimeOnly.Parse(value) },
{ typeof(TimeSpan), value => TimeSpan.Parse(value) },
Expand Down Expand Up @@ -529,22 +537,16 @@ private static Expression CreateRightExprFromType(Type leftExprType, string righ

if (targetType == typeof(DateTime))
{
var dtStyle = right.EndsWith("Z") ? DateTimeStyles.AdjustToUniversal : DateTimeStyles.AssumeLocal;
var dt = DateTime.Parse(right, CultureInfo.InvariantCulture, dtStyle);
if (right.EndsWith("Z"))
{
dt = DateTime.SpecifyKind(dt, DateTimeKind.Utc);
}
var dt = ParseDateTime(right);

return FilterValue.Parameter(dt, rawType);
}

if (targetType == typeof(DateTimeOffset))
{
var dtStyle = right.EndsWith("Z") ? DateTimeStyles.AdjustToUniversal : DateTimeStyles.AssumeLocal;
var dto = DateTimeOffset.Parse(right, CultureInfo.InvariantCulture, dtStyle);
// Npgsql only accepts a DateTimeOffset parameter with offset 0. The UTC value is the same instant.
return FilterValue.Parameter(dto.ToUniversalTime(), rawType);
var dto = ParseDateTimeOffset(right);

return FilterValue.Parameter(dto, rawType);
}

if (targetType == typeof(DateOnly))
Expand Down Expand Up @@ -1528,7 +1530,7 @@ private static Expression CreateCustomOperationExpression<T>(ParameterExpression
return doubleValue;

// Try DateTime
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var dateTimeValue))
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTimeValue))
return dateTimeValue;

// Try Guid
Expand Down
Loading