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
117 changes: 117 additions & 0 deletions AzureSearchEmulator.UnitTests/DateRangeFilterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Text.Json.Nodes;
using AzureSearchEmulator.Indexing;
using AzureSearchEmulator.Models;
using AzureSearchEmulator.Searching;
using Lucene.Net.Documents;
using Lucene.Net.Search;
using Microsoft.OData.UriParser;
using Xunit;

namespace AzureSearchEmulator.UnitTests;

/// <summary>
/// Covers <c>gt</c>, <c>ge</c>, <c>lt</c> and <c>le</c> on an <c>Edm.DateTimeOffset</c> field,
/// which previously failed with "comparison not supported for type DateTimeOffset".
/// </summary>
/// <remarks>
/// Document 2 is dated exactly on the boundary used by every test, so each test also shows
/// whether its operator includes or excludes the boundary.
/// </remarks>
public class DateRangeFilterTests : IDisposable
{
private const string Boundary = "2024-06-01T00:00:00Z";

private readonly LuceneTestHelper _helper;
private readonly IndexSearcher _searcher;

public DateRangeFilterTests()
{
_helper = new LuceneTestHelper(CreateIndex(), CreateDocuments());
_searcher = _helper.CreateSearcher();
}

public void Dispose() => _helper.Dispose();

private static SearchIndex CreateIndex() => new()
{
Name = "dated",
Fields =
[
new SearchField { Name = "Id", Type = "Edm.String", Key = true, Searchable = true },
new SearchField { Name = "Updated", Type = "Edm.DateTimeOffset", Filterable = true },
]
};

private static List<Document> CreateDocuments() =>
[
Doc("1", "2024-01-01T00:00:00Z"),
Doc("2", Boundary),
Doc("3", "2025-01-01T00:00:00Z"),
Doc("4", null),
];

private static Document Doc(string id, string? updated)
{
var json = new JsonObject { ["Id"] = id };

if (updated != null)
{
json["Updated"] = DateTimeOffset.Parse(updated);
}

var doc = new Document();

foreach (var field in CreateIndex().Fields)
{
if (json[field.Name] is not { } value)
{
continue;
}

foreach (var luceneField in field.CreateFields(value))
{
doc.Add(luceneField);
}
}

return doc;
}

private List<string> Search(string filter)
{
var query = new UriQueryExpressionParser(100)
.ParseFilter(filter)
.Accept(new ODataQueryVisitor(_helper.Index));

return _searcher.Search(query, 100).ScoreDocs
.Select(sd => _searcher.Doc(sd.Doc).Get("Id"))
.OrderBy(id => id, StringComparer.Ordinal)
.ToList();
}

[Theory]
[InlineData($"Updated gt {Boundary}", new[] { "3" })]
[InlineData($"Updated ge {Boundary}", new[] { "2", "3" })]
[InlineData($"Updated lt {Boundary}", new[] { "1" })]
[InlineData($"Updated le {Boundary}", new[] { "1", "2" })]
public void RangeOnDateField_MatchesDocumentsOnTheRightSideOfTheBoundary(string filter, string[] expected)
{
Assert.Equal(expected, Search(filter));
}

/// <summary>
/// The same instant written with a different offset must give the same result, since the
/// comparison is between instants, not between the written forms.
/// </summary>
[Fact]
public void RangeOnDateField_ComparesInstantsWhateverTheOffset()
{
Assert.Equal(["2", "3"], Search("Updated ge 2024-06-01T02:00:00+02:00"));
}

[Fact]
public void ClosedDateRange_MatchesDocumentsBetweenTheBounds()
{
Assert.Equal(["2"], Search("Updated gt 2024-01-01T00:00:00Z and Updated lt 2025-01-01T00:00:00Z"));
}
}
4 changes: 4 additions & 0 deletions AzureSearchEmulator/Searching/ODataQueryVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ private static Query HandleLessThanComparison(string path, LiteralToken literalT
float floatValue => NumericRangeQuery.NewDoubleRange(path, double.NegativeInfinity, (double)floatValue, true, false),
double doubleValue => NumericRangeQuery.NewDoubleRange(path, double.NegativeInfinity, doubleValue, true, false),
decimal decimalValue => NumericRangeQuery.NewDoubleRange(path, double.NegativeInfinity, (double)decimalValue, true, false),
DateTimeOffset dateValue => NumericRangeQuery.NewInt64Range(path, long.MinValue, dateValue.ToUnixTimeMilliseconds(), true, false),
_ => throw new NotImplementedException($"Less than comparison not supported for type {literalToken.Value?.GetType().Name}")
};
}
Expand All @@ -626,6 +627,7 @@ private static Query HandleLessThanOrEqualComparison(string path, LiteralToken l
float floatValue => NumericRangeQuery.NewDoubleRange(path, double.NegativeInfinity, (double)floatValue, true, true),
double doubleValue => NumericRangeQuery.NewDoubleRange(path, double.NegativeInfinity, doubleValue, true, true),
decimal decimalValue => NumericRangeQuery.NewDoubleRange(path, double.NegativeInfinity, (double)decimalValue, true, true),
DateTimeOffset dateValue => NumericRangeQuery.NewInt64Range(path, long.MinValue, dateValue.ToUnixTimeMilliseconds(), true, true),
_ => throw new NotImplementedException($"Less than or equal comparison not supported for type {literalToken.Value?.GetType().Name}")
};
}
Expand All @@ -640,6 +642,7 @@ private static Query HandleGreaterThanComparison(string path, LiteralToken liter
float floatValue => NumericRangeQuery.NewDoubleRange(path, (double)floatValue, double.PositiveInfinity, false, true),
double doubleValue => NumericRangeQuery.NewDoubleRange(path, doubleValue, double.PositiveInfinity, false, true),
decimal decimalValue => NumericRangeQuery.NewDoubleRange(path, (double)decimalValue, double.PositiveInfinity, false, true),
DateTimeOffset dateValue => NumericRangeQuery.NewInt64Range(path, dateValue.ToUnixTimeMilliseconds(), long.MaxValue, false, true),
_ => throw new NotImplementedException($"Greater than comparison not supported for type {literalToken.Value?.GetType().Name}")
};
}
Expand All @@ -654,6 +657,7 @@ private static Query HandleGreaterThanOrEqualComparison(string path, LiteralToke
float floatValue => NumericRangeQuery.NewDoubleRange(path, (double)floatValue, double.PositiveInfinity, true, true),
double doubleValue => NumericRangeQuery.NewDoubleRange(path, doubleValue, double.PositiveInfinity, true, true),
decimal decimalValue => NumericRangeQuery.NewDoubleRange(path, (double)decimalValue, double.PositiveInfinity, true, true),
DateTimeOffset dateValue => NumericRangeQuery.NewInt64Range(path, dateValue.ToUnixTimeMilliseconds(), long.MaxValue, true, true),
_ => throw new NotImplementedException($"Greater than or equal comparison not supported for type {literalToken.Value?.GetType().Name}")
};
}
Expand Down