Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
55820c4
docs: add ADR-2604 for date-only search param SQL optimization
mikaelweave Apr 24, 2026
aa4d1ca
feat: add IsDateOnly derived flag to SearchParameterInfo
mikaelweave Apr 24, 2026
37fa16f
fix(search): map FHIRPath .as(date) string cast to Date type
mikaelweave Apr 24, 2026
0fd30a8
feat(search): compute and propagate IsDateOnly through support resolver
mikaelweave Apr 24, 2026
437fbce
feat(sql): add DateOnlyEqualityRewriter
mikaelweave Apr 24, 2026
07246d2
feat(sql): wire DateOnlyEqualityRewriter into search pipeline
mikaelweave Apr 24, 2026
d28ddce
test: add E2E regression for date-only birthdate search (equality + r…
mikaelweave Apr 24, 2026
24d2f87
test: wrap date-only birthdate test in try/catch for CI diagnostics
mikaelweave Apr 24, 2026
8c94060
chore: promote ADR-2604 to accepted status
mikaelweave Apr 24, 2026
d0fe705
fix(sql): reject approximate (ap) date queries in DateOnlyEqualityRew…
mikaelweave Apr 24, 2026
f769992
fix(search): propagate IsDateOnly on cluster cache-refresh path
mikaelweave Apr 24, 2026
969288a
docs: add single-point SQL rewrite design
mikaelweave Apr 24, 2026
7a8f91a
chore: ignore local worktree directories
mikaelweave Apr 24, 2026
d5c5c10
docs: add patient birthdate partial-date query design
mikaelweave May 7, 2026
9001a93
docs: revise birthdate partial-date design scope
mikaelweave May 7, 2026
1c104a4
docs: clarify scalar temporal query design
mikaelweave May 7, 2026
b94d14c
docs: add scalar temporal birthdate implementation plan
mikaelweave May 7, 2026
f687638
feat(search): add scalar temporal search parameter metadata
mikaelweave May 7, 2026
66333d4
fix(search): isolate scalar temporal metadata contract
mikaelweave May 7, 2026
4a1dca6
fix(search): adapt scalar temporal resolver contract callers
mikaelweave May 7, 2026
6c1bfbc
fix(search): correct scalar temporal tuple discards
mikaelweave May 7, 2026
b4098bb
fix(search): preserve encoding in resolver contract updates
mikaelweave May 7, 2026
6ccc546
feat(search): compute scalar temporal search metadata
mikaelweave May 7, 2026
4a5bd18
fix(search): make scalar temporal mixed test distinct
mikaelweave May 7, 2026
c04f75f
fix(search): correct scalar temporal mixed test cleanup
mikaelweave May 7, 2026
25eae90
feat(search): propagate scalar temporal metadata
mikaelweave May 7, 2026
6aea5a0
fix(search): restore status manager resolver assertion
mikaelweave May 7, 2026
3daa463
fix(search): simplify temporal metadata resolution guard
mikaelweave May 7, 2026
0b3060f
fix(search): count scalar temporal parameters in one pass
mikaelweave May 7, 2026
4f7dc7c
test(sql): cover scalar temporal equality rewrite
mikaelweave May 7, 2026
5a7657a
test(sql): refine scalar temporal pass-through assertions
mikaelweave May 7, 2026
2706f17
test(sql): distinguish scalar temporal from date-only rewrite gate
mikaelweave May 7, 2026
bc80a03
test(sql): improve scalar temporal assertion diagnostics
mikaelweave May 7, 2026
5bbb605
test(sql): clarify composite scalar temporal pass-through
mikaelweave May 7, 2026
8f2e66d
feat(sql): replace date-only rewrite with scalar temporal rewrite
mikaelweave May 7, 2026
dca75d9
fix(sql): remove month collapse, add reversed-order test for scalar t…
mikaelweave May 7, 2026
fede0bf
test(sql): clarify scalar temporal composite gate
mikaelweave May 7, 2026
728e0f7
feat(sql): add scalar temporal search diagnostics
mikaelweave May 7, 2026
5e5cf6f
test(sql): refine scalar temporal diagnostics metadata
mikaelweave May 7, 2026
d645716
test(e2e): cover partial birthdate equality semantics
mikaelweave May 7, 2026
93d4668
fix(search): clarify scalar temporal startup diagnostics
mikaelweave May 7, 2026
35abba8
Simplify birthdate date rewrite
mikaelweave May 8, 2026
7e1e83e
Remove unnecessary date resolver mapping
mikaelweave May 8, 2026
df42882
Restore search parameter infrastructure
mikaelweave May 8, 2026
edecd38
Revert search parameter infrastructure restore
mikaelweave May 8, 2026
f2fb7f4
test(e2e): align birthdate baseline coverage
mikaelweave May 12, 2026
025b3f1
merge from main
mikaelweave May 12, 2026
9cb78de
refactor(ScalarTemporalEqualityRewriter): make rewrite flow linear
mikaelweave May 13, 2026
d0a3a02
chore: revert unused changes in search parameter files
mikaelweave May 13, 2026
747f3c3
Merge remote-tracking branch 'origin/main' into personal/mikaelw/scal…
mikaelweave May 13, 2026
ab1192c
test: take DateSearchTests from main
mikaelweave May 13, 2026
9483c73
test: take Shared.Tests.Integration from main
mikaelweave May 13, 2026
f544e8b
test(e2e): align day-precision birthdate expectations with rewriter
mikaelweave May 13, 2026
5553f3a
use main tests
mikaelweave May 13, 2026
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,228 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using Microsoft.Health.Fhir.Core.Features.Search.Expressions;
using Microsoft.Health.Fhir.Core.Models;
using Microsoft.Health.Fhir.SqlServer.Features.Search.Expressions;
using Microsoft.Health.Fhir.SqlServer.Features.Search.Expressions.Visitors;
using Microsoft.Health.Fhir.Tests.Common;
using Microsoft.Health.Fhir.ValueSets;
using Microsoft.Health.Test.Utilities;
using Xunit;

namespace Microsoft.Health.Fhir.SqlServer.UnitTests.Features.Search.Expressions
{
[Trait(Traits.OwningTeam, OwningTeam.Fhir)]
[Trait(Traits.Category, Categories.Search)]
public class ScalarTemporalEqualityRewriterTests
{
private static readonly DateTimeOffset StartOfDay = new DateTimeOffset(2016, 7, 6, 0, 0, 0, TimeSpan.Zero);
private static readonly DateTimeOffset EndOfDay = new DateTimeOffset(2016, 7, 6, 23, 59, 59, TimeSpan.Zero).AddTicks(9999999);
private static readonly DateTimeOffset StartOfLastDayOfMonth = new DateTimeOffset(2016, 7, 31, 0, 0, 0, TimeSpan.Zero);
private static readonly DateTimeOffset EndOfLastDayOfMonth = new DateTimeOffset(2016, 7, 31, 23, 59, 59, TimeSpan.Zero).AddTicks(9999999);
private static readonly DateTimeOffset StartOfLastDayOfYear = new DateTimeOffset(2016, 12, 31, 0, 0, 0, TimeSpan.Zero);
private static readonly DateTimeOffset EndOfLastDayOfYear = new DateTimeOffset(2016, 12, 31, 23, 59, 59, TimeSpan.Zero).AddTicks(9999999);
private static readonly DateTimeOffset StartOfMonth = new DateTimeOffset(2016, 7, 1, 0, 0, 0, TimeSpan.Zero);
private static readonly DateTimeOffset EndOfMonth = new DateTimeOffset(2016, 7, 31, 23, 59, 59, TimeSpan.Zero).AddTicks(9999999);
private static readonly DateTimeOffset StartOfYear = new DateTimeOffset(2016, 1, 1, 0, 0, 0, TimeSpan.Zero);
private static readonly DateTimeOffset EndOfYear = new DateTimeOffset(2016, 12, 31, 23, 59, 59, TimeSpan.Zero).AddTicks(9999999);

private static SearchParameterInfo BuildBirthdateParam(Uri url = null, SearchParamType searchParamType = SearchParamType.Date)
{
return new SearchParameterInfo(
"birthdate",
"birthdate",
searchParamType,
url ?? new Uri("http://hl7.org/fhir/SearchParameter/individual-birthdate"),
expression: "Patient.birthDate",
baseResourceTypes: new[] { "Patient" });
}

private static MultiaryExpression EqualityPattern(DateTimeOffset start, DateTimeOffset end) =>
(MultiaryExpression)Expression.And(
Expression.GreaterThanOrEqual(FieldName.DateTimeStart, null, start),
Expression.LessThanOrEqual(FieldName.DateTimeEnd, null, end));

Check warning

Code scanning / CodeQL

Cast to same type Warning

This cast is redundant because the expression already has type MultiaryExpression.
Comment on lines +45 to +47

[Fact]
public void GivenAllowListedBirthdateExactDay_WhenRewritten_ThenUsesEndDateTimeAndNotLongerThanDay()
{
var expr = new SearchParameterExpression(BuildBirthdateParam(), EqualityPattern(StartOfDay, EndOfDay));

var result = expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null);

AssertEndDateEqualsAndNotLongerThanDay(result, EndOfDay);
}

[Fact]
public void GivenAllowListedBirthdateLastDayOfMonth_WhenRewritten_ThenUsesEndDateTimeAndNotLongerThanDay()
{
var expr = new SearchParameterExpression(BuildBirthdateParam(), EqualityPattern(StartOfLastDayOfMonth, EndOfLastDayOfMonth));

var result = expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null);

AssertEndDateEqualsAndNotLongerThanDay(result, EndOfLastDayOfMonth);
}

[Fact]
public void GivenAllowListedBirthdateLastDayOfYear_WhenRewritten_ThenUsesEndDateTimeAndNotLongerThanDay()
{
var expr = new SearchParameterExpression(BuildBirthdateParam(), EqualityPattern(StartOfLastDayOfYear, EndOfLastDayOfYear));

var result = expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null);

AssertEndDateEqualsAndNotLongerThanDay(result, EndOfLastDayOfYear);
}

[Fact]
public void GivenAllowListedBirthdateMonth_WhenRewritten_ThenPassThrough()
{
var expr = new SearchParameterExpression(BuildBirthdateParam(), EqualityPattern(StartOfMonth, EndOfMonth));

var result = Assert.IsType<SearchParameterExpression>(expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null));

Assert.Same(expr, result);
}

[Fact]
public void GivenAllowListedBirthdateYear_WhenRewritten_ThenUsesEndDateTimeRange()
{
var expr = new SearchParameterExpression(BuildBirthdateParam(), EqualityPattern(StartOfYear, EndOfYear));

var result = expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null);

var rewritten = Assert.IsType<SearchParameterExpression>(result);
var multiary = Assert.IsType<MultiaryExpression>(rewritten.Expression);
Assert.Equal(MultiaryOperator.And, multiary.MultiaryOperation);
Assert.Collection(
multiary.Expressions,
first =>
{
var binary = Assert.IsType<BinaryExpression>(first);
Assert.Equal(FieldName.DateTimeEnd, binary.FieldName);
Assert.Equal(BinaryOperator.GreaterThanOrEqual, binary.BinaryOperator);
Assert.Equal(StartOfYear, binary.Value);
},
second =>
{
var binary = Assert.IsType<BinaryExpression>(second);
Assert.Equal(FieldName.DateTimeEnd, binary.FieldName);
Assert.Equal(BinaryOperator.LessThanOrEqual, binary.BinaryOperator);
Assert.Equal(EndOfYear, binary.Value);
});
}

[Fact]
public void GivenDateParameterNotAllowListed_WhenEqualityPatternMatched_ThenPassThrough()
{
var expr = new SearchParameterExpression(
BuildBirthdateParam(new Uri("http://example.org/SearchParameter/test-date")),
EqualityPattern(StartOfYear, EndOfYear));

var result = Assert.IsType<SearchParameterExpression>(expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null));

Assert.Same(expr, result);
}

[Fact]
public void GivenAllowListedNonDateParameter_WhenEqualityPatternMatched_ThenPassThrough()
{
var expr = new SearchParameterExpression(
BuildBirthdateParam(searchParamType: SearchParamType.String),
EqualityPattern(StartOfYear, EndOfYear));

var result = Assert.IsType<SearchParameterExpression>(expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null));

Assert.Same(expr, result);
}

[Fact]
public void GivenAllowListedBirthdateExactDayReversedOrder_WhenRewritten_ThenUsesEndDateTimeAndNotLongerThanDay()
{
var reversedPattern = (MultiaryExpression)Expression.And(
Expression.LessThanOrEqual(FieldName.DateTimeEnd, null, EndOfDay),
Expression.GreaterThanOrEqual(FieldName.DateTimeStart, null, StartOfDay));

Check warning

Code scanning / CodeQL

Cast to same type Warning

This cast is redundant because the expression already has type MultiaryExpression.
Comment on lines +144 to +146
var expr = new SearchParameterExpression(BuildBirthdateParam(), reversedPattern);

var result = expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null);

AssertEndDateEqualsAndNotLongerThanDay(result, EndOfDay);
}

[Fact]
public void GivenSingleSidedPredicate_WhenRewritten_ThenPassThrough()
{
var single = Expression.GreaterThanOrEqual(FieldName.DateTimeStart, null, StartOfDay);
var expr = new SearchParameterExpression(BuildBirthdateParam(), single);

var result = Assert.IsType<SearchParameterExpression>(expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null));

Assert.Same(expr, result);
}

[Fact]
public void GivenRangeOperatorPattern_WhenRewritten_ThenPassThrough()
{
var range = Expression.GreaterThan(FieldName.DateTimeStart, null, EndOfDay);
var expr = new SearchParameterExpression(BuildBirthdateParam(), range);

var result = Assert.IsType<SearchParameterExpression>(expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null));

Assert.Same(expr, result);
}

[Fact]
public void GivenApproximateExpression_WhenRewritten_ThenPassThrough()
{
var approxStart = StartOfDay.AddDays(-30);
var approxEnd = EndOfDay.AddDays(30);
var expr = new SearchParameterExpression(BuildBirthdateParam(), EqualityPattern(approxStart, approxEnd));

var result = Assert.IsType<SearchParameterExpression>(expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null));

Assert.Same(expr, result);
}

[Fact]
public void GivenCompositeParameter_WhenEqualityPatternMatched_ThenPassThrough()
{
var composite = new SearchParameterInfo(
"Patient-code-birthdate",
"code-birthdate",
SearchParamType.Composite,
new Uri("http://example.org/SearchParameter/Patient-code-birthdate"),
expression: "Patient",
baseResourceTypes: new[] { "Patient" });
var expr = new SearchParameterExpression(composite, EqualityPattern(StartOfDay, EndOfDay));

var result = Assert.IsType<SearchParameterExpression>(expr.AcceptVisitor(ScalarTemporalEqualityRewriter.Instance, null));

Assert.Same(expr, result);
}

private static void AssertEndDateEqualsAndNotLongerThanDay(Expression result, DateTimeOffset expectedEnd)
{
var rewritten = Assert.IsType<SearchParameterExpression>(result);
var multiary = Assert.IsType<MultiaryExpression>(rewritten.Expression);
Assert.Equal(MultiaryOperator.And, multiary.MultiaryOperation);
Assert.Collection(
multiary.Expressions,
first =>
{
var binary = Assert.IsType<BinaryExpression>(first);
Assert.Equal(FieldName.DateTimeEnd, binary.FieldName);
Assert.Equal(BinaryOperator.Equal, binary.BinaryOperator);
Assert.Equal(expectedEnd, binary.Value);
},
second =>
{
var binary = Assert.IsType<BinaryExpression>(second);
Assert.Equal(SqlFieldName.DateTimeIsLongerThanADay, binary.FieldName);
Assert.Equal(BinaryOperator.Equal, binary.BinaryOperator);
Assert.False((bool)binary.Value);
});
}
}
}
Loading
Loading