Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b92edaa
Fixing datetime arithmetic overflow/underflow.
v-isyamauchi-gh Sep 1, 2026
01dc4de
Addressing reviewer's comments.
v-isyamauchi-gh Sep 1, 2026
c2315f6
Addressing more comments.
v-isyamauchi-gh Sep 1, 2026
fe905dd
Addressing more comments 2.
v-isyamauchi-gh Sep 1, 2026
a776e52
Merge branch 'main' into personal/v-isyamauchi/181592
v-isyamauchi-gh Sep 2, 2026
ec1ab52
Addressing more comments 3
v-isyamauchi-gh Sep 2, 2026
fe822c5
Refactor DateTime arithmetic to try-catch for robustness
v-isyamauchi-gh Sep 2, 2026
fe0a471
Revert "Refactor DateTime arithmetic to try-catch for robustness"
v-isyamauchi-gh Sep 2, 2026
a954c15
Changing to a try-catch approach.
v-isyamauchi-gh Sep 2, 2026
ad09ea4
Merge branch 'main' into personal/v-isyamauchi/181592
v-isyamauchi-gh Sep 3, 2026
d6cb34f
Adding 2 additional parameters to the safe methods.
v-isyamauchi-gh Sep 3, 2026
d465110
Addressing reviewers comments 4
v-isyamauchi-gh Sep 3, 2026
6b28e15
Reverting last change
v-isyamauchi-gh Sep 3, 2026
d4ae9f2
Merge branch 'main' into personal/v-isyamauchi/181592
v-isyamauchi-gh Sep 4, 2026
104b146
Addressing comments 5.
v-isyamauchi-gh Sep 4, 2026
0a2bcee
Merge branch 'main' into personal/v-isyamauchi/181592
v-isyamauchi-gh Sep 8, 2026
c4b8f69
Fixing IdHelper throwing the arg exception.
v-isyamauchi-gh Sep 8, 2026
ed8ed35
Merge branch 'main' into personal/v-isyamauchi/181592
v-isyamauchi-gh Sep 9, 2026
49856fe
Changing the handling of overflow exceptions.
v-isyamauchi-gh Sep 10, 2026
f7302b0
Addressing comments 6
v-isyamauchi-gh Sep 10, 2026
732f8a8
Addressing comments 7
v-isyamauchi-gh Sep 10, 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,152 @@
// -------------------------------------------------------------------------------------------------
// 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.Extensions;
using Microsoft.Health.Fhir.Tests.Common;
using Microsoft.Health.Test.Utilities;
using Xunit;

namespace Microsoft.Health.Fhir.Core.UnitTests.Extensions
{
[Trait(Traits.OwningTeam, OwningTeam.Fhir)]
[Trait(Traits.Category, Categories.Search)]
public class DateTimeSafeExtensionsTests
{
// SafeAddTicks - DateTime

[Fact]
public void GivenNormalDateTime_WhenSafeAddTicksCalled_ThenReturnsExpectedResult()
{
var dt = new DateTime(2024, 6, 15, 12, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddTicks(TimeSpan.TicksPerMillisecond);
Assert.Equal(dt.AddTicks(TimeSpan.TicksPerMillisecond), result);
}

[Fact]
public void GivenDateTimeNearMaxValue_WhenSafeAddTicksPositive_ThenClampsToMaxValue()
{
var dt = DateTime.MaxValue.AddTicks(-100);
DateTime result = dt.SafeAddTicks(TimeSpan.TicksPerMillisecond);
Assert.Equal(DateTime.MaxValue, result);
}

[Fact]
public void GivenDateTimeMaxValue_WhenSafeAddTicksPositive_ThenClampsToMaxValue()
{
DateTime result = DateTime.MaxValue.SafeAddTicks(1);
Assert.Equal(DateTime.MaxValue, result);
}

[Fact]
public void GivenDateTimeNearMinValue_WhenSafeAddTicksNegative_ThenClampsToMinValue()
{
var dt = DateTime.MinValue.AddTicks(100);
DateTime result = dt.SafeAddTicks(-TimeSpan.TicksPerMillisecond);
Assert.Equal(DateTime.MinValue, result);
}

[Fact]
public void GivenDateTimeMinValue_WhenSafeAddTicksNegative_ThenClampsToMinValue()
{
DateTime result = DateTime.MinValue.SafeAddTicks(-1);
Assert.Equal(DateTime.MinValue, result);
}

[Fact]
public void GivenDateTime_WhenSafeAddTicksZero_ThenReturnsOriginal()
{
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddTicks(0);
Assert.Equal(dt, result);
}

[Fact]
public void GivenAnyDateTime_WhenSafeAddTicksLongMinValue_ThenClampsToMinValue()
{
var dt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddTicks(long.MinValue);
Assert.Equal(DateTime.MinValue, result);
}

// SafeAddTicks - DateTimeOffset

[Fact]
public void GivenNormalDateTimeOffset_WhenSafeAddTicksCalled_ThenReturnsExpectedResult()
{
var dto = new DateTimeOffset(2024, 6, 15, 12, 0, 0, TimeSpan.Zero);
DateTimeOffset result = dto.SafeAddTicks(TimeSpan.TicksPerMillisecond);
Assert.Equal(dto.AddTicks(TimeSpan.TicksPerMillisecond), result);
}

[Fact]
public void GivenDateTimeOffsetNearMaxValue_WhenSafeAddTicksPositive_ThenClampsToMaxValue()
{
var dto = DateTimeOffset.MaxValue.AddTicks(-100);
DateTimeOffset result = dto.SafeAddTicks(TimeSpan.TicksPerMillisecond);
Assert.Equal(DateTimeOffset.MaxValue, result);
}

[Fact]
public void GivenDateTimeOffsetNearMinValue_WhenSafeAddTicksNegative_ThenClampsToMinValue()
{
var dto = DateTimeOffset.MinValue.AddTicks(100);
DateTimeOffset result = dto.SafeAddTicks(-TimeSpan.TicksPerMillisecond);
Assert.Equal(DateTimeOffset.MinValue, result);
}

// SafeAddDays - DateTime

[Fact]
public void GivenNormalDateTime_WhenSafeAddDaysCalled_ThenReturnsExpectedResult()
{
var dt = new DateTime(2024, 6, 15, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.SafeAddDays(1);
Assert.Equal(dt.AddDays(1), result);
}

[Fact]
public void GivenDateTimeNearMaxValue_WhenSafeAddDaysPositive_ThenClampsToMaxValue()
{
var dt = DateTime.MaxValue.AddDays(-0.5);
DateTime result = dt.SafeAddDays(1);
Assert.Equal(DateTime.MaxValue, result);
}

[Fact]
public void GivenDateTimeNearMinValue_WhenSafeAddDaysNegative_ThenClampsToMinValue()
{
var dt = DateTime.MinValue.AddDays(0.5);
DateTime result = dt.SafeAddDays(-1);
Assert.Equal(DateTime.MinValue, result);
}

// SafeAddDays - DateTimeOffset

[Fact]
public void GivenNormalDateTimeOffset_WhenSafeAddDaysCalled_ThenReturnsExpectedResult()
{
var dto = new DateTimeOffset(2024, 6, 15, 0, 0, 0, TimeSpan.Zero);
DateTimeOffset result = dto.SafeAddDays(1);
Assert.Equal(dto.AddDays(1), result);
}

[Fact]
public void GivenDateTimeOffsetNearMaxValue_WhenSafeAddDaysPositive_ThenClampsToMaxValue()
{
var dto = DateTimeOffset.MaxValue.AddDays(-0.5);
DateTimeOffset result = dto.SafeAddDays(1);
Assert.Equal(DateTimeOffset.MaxValue, result);
}

[Fact]
public void GivenDateTimeOffsetNearMinValue_WhenSafeAddDaysNegative_ThenClampsToMinValue()
{
var dto = DateTimeOffset.MinValue.AddDays(0.5);
DateTimeOffset result = dto.SafeAddDays(-1);
Assert.Equal(DateTimeOffset.MinValue, result);
}
}
}
Comment thread
v-isyamauchi-gh marked this conversation as resolved.
Comment thread
v-isyamauchi-gh marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;

namespace Microsoft.Health.Fhir.Core.Extensions
{
/// <summary>
/// Extension methods for <see cref="DateTime"/> and <see cref="DateTimeOffset"/> that clamp
/// results to <see cref="DateTime.MinValue"/>/<see cref="DateTime.MaxValue"/> instead of
/// throwing on overflow.
/// </summary>
public static class DateTimeSafeExtensions
{
/// <summary>
/// Adds the specified number of ticks to a <see cref="DateTime"/>, clamping the result
/// to <see cref="DateTime.MinValue"/> or <see cref="DateTime.MaxValue"/> on overflow.
/// </summary>
public static DateTime SafeAddTicks(this DateTime value, long ticks)
{
if (ticks == long.MinValue)
{
// Can't negate long.MinValue; adding it always underflows within DateTime's range.
return DateTime.MinValue;
}

if (ticks > 0 && value.Ticks > DateTime.MaxValue.Ticks - ticks)
{
return DateTime.MaxValue;
}

if (ticks < 0 && value.Ticks < DateTime.MinValue.Ticks - ticks)
{
return DateTime.MinValue;
}

return value.AddTicks(ticks);
}
Comment thread
v-isyamauchi-gh marked this conversation as resolved.
Outdated

/// <summary>
/// Adds the specified number of ticks to a <see cref="DateTimeOffset"/>, clamping the result
/// to <see cref="DateTimeOffset.MinValue"/> or <see cref="DateTimeOffset.MaxValue"/> on overflow.
/// </summary>
Comment thread
Copilot marked this conversation as resolved.
public static DateTimeOffset SafeAddTicks(this DateTimeOffset value, long ticks)
{
if (ticks == long.MinValue)
{
// Can't negate long.MinValue; adding it always underflows within DateTimeOffset's range.
return DateTimeOffset.MinValue;
}

if (ticks > 0 && value.Ticks > DateTimeOffset.MaxValue.Ticks - ticks)
{
return DateTimeOffset.MaxValue;
}

if (ticks < 0 && value.Ticks < DateTimeOffset.MinValue.Ticks - ticks)
{
return DateTimeOffset.MinValue;
}

return value.AddTicks(ticks);
}
Comment thread
v-isyamauchi-gh marked this conversation as resolved.
Outdated

/// <summary>
/// Adds the specified number of days to a <see cref="DateTime"/>, clamping the result
/// to <see cref="DateTime.MinValue"/> or <see cref="DateTime.MaxValue"/> on overflow.
/// </summary>
public static DateTime SafeAddDays(this DateTime value, int days)
{
long ticks = days * TimeSpan.TicksPerDay;
return value.SafeAddTicks(ticks);
}

/// <summary>
/// Adds the specified number of days to a <see cref="DateTimeOffset"/>, clamping the result
/// to <see cref="DateTimeOffset.MinValue"/> or <see cref="DateTimeOffset.MaxValue"/> on overflow.
/// </summary>
public static DateTimeOffset SafeAddDays(this DateTimeOffset value, int days)
{
long ticks = days * TimeSpan.TicksPerDay;
return value.SafeAddTicks(ticks);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ void ISearchValueVisitor.Visit(DateTimeSearchValue dateTime)

var differenceTicks = (long)((Clock.UtcNow.Ticks - Math.Max(startTicks, endTicks)) * ApproximateMultiplier);

var approximateStart = dateTime.Start.AddTicks(-differenceTicks);
var approximateEnd = dateTime.End.AddTicks(differenceTicks);
var approximateStart = dateTime.Start.SafeAddTicks(-differenceTicks);
var approximateEnd = dateTime.End.SafeAddTicks(differenceTicks);
Comment thread
v-isyamauchi-gh marked this conversation as resolved.

// Spec (ap): the search range overlaps the target range. Emit overlap directly
// (Start <= approxEnd AND End >= approxStart) rather than the eq-shaped containment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// -------------------------------------------------------------------------------------------------

using System;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Search.Expressions;
using Microsoft.Health.Fhir.ValueSets;

Expand Down Expand Up @@ -41,7 +42,7 @@ expression.Expressions[0] is BinaryExpression isLong &&
return Expression.And(
Expression.Equals(SqlFieldName.DateTimeIsLongerThanADay, left.ComponentIndex, false),
new BinaryExpression(left.BinaryOperator, FieldName.DateTimeEnd, left.ComponentIndex, left.Value),
new BinaryExpression(left.BinaryOperator, FieldName.DateTimeStart, left.ComponentIndex, ((DateTimeOffset)left.Value).AddTicks(-TimeSpan.TicksPerDay)),
new BinaryExpression(left.BinaryOperator, FieldName.DateTimeStart, left.ComponentIndex, ((DateTimeOffset)left.Value).SafeAddTicks(-TimeSpan.TicksPerDay)),
new BinaryExpression(right.BinaryOperator, FieldName.DateTimeStart, right.ComponentIndex, right.Value));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using Microsoft.Health.Core.Extensions;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Search;
using Microsoft.Health.Fhir.Core.Features.Search.Expressions;
using Microsoft.Health.Fhir.SqlServer.Features.Storage;
Expand Down Expand Up @@ -56,7 +57,7 @@ public override Expression VisitBinary(BinaryExpression expression, object conte
return Expression.GreaterThanOrEqual(
SqlFieldName.ResourceSurrogateId,
null,
new DateTimeOffset(truncated.AddTicks(TimeSpan.TicksPerMillisecond)).ToSurrogateId());
new DateTimeOffset(truncated.SafeAddTicks(TimeSpan.TicksPerMillisecond)).ToSurrogateId());
Comment thread
v-isyamauchi-gh marked this conversation as resolved.
Outdated
Comment thread
v-isyamauchi-gh marked this conversation as resolved.
Outdated
case BinaryOperator.GreaterThanOrEqual:
if (original == truncated)
{
Expand All @@ -81,7 +82,7 @@ public override Expression VisitBinary(BinaryExpression expression, object conte
return Expression.LessThan(
SqlFieldName.ResourceSurrogateId,
null,
new DateTimeOffset(truncated.AddTicks(TimeSpan.TicksPerMillisecond)).ToSurrogateId());
new DateTimeOffset(truncated.SafeAddTicks(TimeSpan.TicksPerMillisecond)).ToSurrogateId());
case BinaryOperator.NotEqual:
case BinaryOperator.Equal: // expecting eq to have been rewritten as a range
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using System.Collections.Generic;
using Microsoft.Health.Fhir.Core.Extensions;
using Microsoft.Health.Fhir.Core.Features.Search.Expressions;
using Microsoft.Health.Fhir.Core.Models;
using Microsoft.Health.Fhir.SqlServer.Features.Search.Expressions;
Expand Down Expand Up @@ -133,7 +134,7 @@ private static Precision ClassifyPrecision(DateTimeOffset start, DateTimeOffset
return Precision.NotRewritable;
}

if (end == start.AddDays(1).AddTicks(-1))
if (end == start.SafeAddTicks(TimeSpan.TicksPerDay - 1))
Comment thread
Copilot marked this conversation as resolved.
Outdated
{
return Precision.ExactDay;
}
Expand Down
Loading