diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs index 18a5895d861..59e3d3ade36 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.Designer.cs @@ -321,7 +321,7 @@ internal static string DataLoaderResolverContextExtensions_UnableToRegister { } /// - /// Looks up a localized string similar to InputPrecision must be less than or equal to 7.. + /// Looks up a localized string similar to InputPrecision must be less than or equal to 9.. /// internal static string DateTimeOptions_InputPrecision_InvalidValue { get { diff --git a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx index 5df0307fe49..4ceae5fbbb0 100644 --- a/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +++ b/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx @@ -1069,7 +1069,7 @@ Type: `{0}` The stability must follow the GraphQL type name rules. - InputPrecision must be less than or equal to 7. + InputPrecision must be less than or equal to 9. OutputPrecision must be less than or equal to 7. diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs index 32bd19d18b6..e8b332798de 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs @@ -8,7 +8,9 @@ namespace HotChocolate.Types; /// public struct DateTimeOptions { - public const byte DefaultInputPrecision = 7; + public const byte DefaultInputPrecision = 9; + + // DateTimeOffset, DateTime, and TimeOnly all have a maximum of 7 fractional second digits. public const byte DefaultOutputPrecision = 7; public DateTimeOptions() @@ -17,17 +19,20 @@ public DateTimeOptions() /// /// Gets the maximum number of fractional second digits to expect when parsing date and time - /// input values. + /// input values. Note that the underlying .NET types (, + /// , and ) have a maximum resolution of 7 + /// fractional digits (100-nanosecond ticks), so digits beyond the 7th are rounded during + /// parsing. /// /// - /// Thrown when the value is greater than 7. + /// Thrown when the value is greater than 9. /// public byte InputPrecision { get; init { - if (value > 7) + if (value > 9) { throw new ArgumentOutOfRangeException( nameof(InputPrecision), diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs index 7451fdf4375..e8980cf3476 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs @@ -196,7 +196,9 @@ private Regex GetDateTimeRegex() 4 => DateTimeRegex4(), 5 => DateTimeRegex5(), 6 => DateTimeRegex6(), - _ => DateTimeRegex7() + 7 => DateTimeRegex7(), + 8 => DateTimeRegex8(), + _ => DateTimeRegex9() }; [GeneratedRegex( @@ -238,4 +240,14 @@ private Regex GetDateTimeRegex() @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,7})?(Z|[+-][0-9]{2}:[0-9]{2})\z", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] private static partial Regex DateTimeRegex7(); + + [GeneratedRegex( + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,8})?(Z|[+-][0-9]{2}:[0-9]{2})\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex DateTimeRegex8(); + + [GeneratedRegex( + @"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?(Z|[+-][0-9]{2}:[0-9]{2})\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex DateTimeRegex9(); } diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs index cdd54992016..729f5973946 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs @@ -176,7 +176,9 @@ private Regex GetLocalDateTimeRegex() 4 => LocalDateTimeRegex4(), 5 => LocalDateTimeRegex5(), 6 => LocalDateTimeRegex6(), - _ => LocalDateTimeRegex7() + 7 => LocalDateTimeRegex7(), + 8 => LocalDateTimeRegex8(), + _ => LocalDateTimeRegex9() }; [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\z", @@ -210,4 +212,12 @@ private Regex GetLocalDateTimeRegex() [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,7})?\z", RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] private static partial Regex LocalDateTimeRegex7(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,8})?\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex LocalDateTimeRegex8(); + + [GeneratedRegex(@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?\z", + RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase)] + private static partial Regex LocalDateTimeRegex9(); } diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs index 42ed4e0b2d6..1bd84e81695 100644 --- a/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs @@ -174,7 +174,9 @@ private Regex GetLocalTimeRegex() 4 => LocalTimeRegex4(), 5 => LocalTimeRegex5(), 6 => LocalTimeRegex6(), - _ => LocalTimeRegex7() + 7 => LocalTimeRegex7(), + 8 => LocalTimeRegex8(), + _ => LocalTimeRegex9() }; [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}\z", RegexOptions.ExplicitCapture)] @@ -200,4 +202,10 @@ private Regex GetLocalTimeRegex() [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,7})?\z", RegexOptions.ExplicitCapture)] private static partial Regex LocalTimeRegex7(); + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,8})?\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex8(); + + [GeneratedRegex(@"^[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,9})?\z", RegexOptions.ExplicitCapture)] + private static partial Regex LocalTimeRegex9(); } diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeOptionsTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeOptionsTests.cs index 3c35caed5ed..082ed96ad0e 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeOptionsTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeOptionsTests.cs @@ -17,7 +17,7 @@ public void DefaultConstructor_ShouldSetDefaultPrecisions() public void DefaultConstants_ShouldBeCorrect() { // assert - Assert.Equal(7, DateTimeOptions.DefaultInputPrecision); + Assert.Equal(9, DateTimeOptions.DefaultInputPrecision); Assert.Equal(7, DateTimeOptions.DefaultOutputPrecision); } @@ -30,6 +30,8 @@ public void DefaultConstants_ShouldBeCorrect() [InlineData(5)] [InlineData(6)] [InlineData(7)] + [InlineData(8)] + [InlineData(9)] public void InputPrecision_ValidValues_ShouldSet(byte precision) { // arrange & act @@ -58,8 +60,6 @@ public void OutputPrecision_ValidValues_ShouldSet(byte precision) } [Theory] - [InlineData(8)] - [InlineData(9)] [InlineData(10)] [InlineData(255)] public void InputPrecision_InvalidValues_ShouldThrow(byte precision) diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs index 2ba79dc582b..c88ee7b15a3 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs @@ -328,6 +328,8 @@ public void DateTime_Relaxed_Format_Check() [InlineData(5, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,5})?(?:[Zz]|[+-]\d{2}:\d{2})$")] [InlineData(6, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:[Zz]|[+-]\d{2}:\d{2})$")] [InlineData(7, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?(?:[Zz]|[+-]\d{2}:\d{2})$")] + [InlineData(8, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,8})?(?:[Zz]|[+-]\d{2}:\d{2})$")] + [InlineData(9, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:[Zz]|[+-]\d{2}:\d{2})$")] public void Pattern_Should_Match_InputPrecision(byte precision, string expectedPattern) { // arrange & act @@ -354,8 +356,8 @@ public static TheoryData ValidInput() }, { DateTimeOptions.DefaultInputPrecision, - "2023-12-24T15:30:00.1234567+01:00", - new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.FromHours(1)).AddTicks(7) + "2023-12-24T15:30:00.123456789+01:00", // Rounded to ".1234568". + new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.FromHours(1)).AddTicks(8) } }; } @@ -376,13 +378,17 @@ public static TheoryData InvalidInput() // ReSharper disable once GrammarMistakeInComment // Invalid date (February 30th). { DateTimeOptions.DefaultInputPrecision, "2023-02-30T15:30:00Z" }, - // More than 7 fractional second digits. - { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.12345678Z" }, + // More than 9 fractional second digits. + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.1234567890Z" }, // Invalid offset (exceeds maximum). { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00+25:00" }, // Invalid offset format. { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00 UTC" }, // Additional cases. + // More than 8 fractional second digits with precision set to 8. + { 8, "2023-12-24T15:30:00.123456789Z" }, + // More than 7 fractional second digits with precision set to 7. + { 7, "2023-12-24T15:30:00.12345678Z" }, // More than 6 fractional second digits with precision set to 6. { 6, "2023-12-24T15:30:00.1234567Z" }, // More than 5 fractional second digits with precision set to 5. diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs index 10b3016814d..4a6b1914dbc 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalDateTimeTypeTests.cs @@ -323,6 +323,8 @@ public void LocalDateTime_Relaxed_Format_Check() [InlineData(5, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,5})?$")] [InlineData(6, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?$")] [InlineData(7, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?$")] + [InlineData(8, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,8})?$")] + [InlineData(9, @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?$")] public void Pattern_Should_Match_InputPrecision(byte precision, string expectedPattern) { // arrange & act @@ -375,8 +377,8 @@ public static TheoryData ValidInput() }, { DateTimeOptions.DefaultInputPrecision, - "2023-12-24t15:30:00.1234567", - new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(7) + "2023-12-24t15:30:00.123456789", // Rounded to ".1234568". + new DateTime(2023, 12, 24, 15, 30, 0, 123, 456).AddTicks(8) } }; } @@ -399,9 +401,13 @@ public static TheoryData InvalidInput() // ReSharper disable once GrammarMistakeInComment // Invalid date (February 30th). { DateTimeOptions.DefaultInputPrecision, "2023-02-30T15:30:00" }, - // More than 7 fractional second digits. - { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.12345678" }, + // More than 9 fractional second digits. + { DateTimeOptions.DefaultInputPrecision, "2023-12-24T15:30:00.1234567890" }, // Additional cases. + // More than 8 fractional second digits with precision set to 8. + { 8, "2023-12-24T15:30:00.123456789" }, + // More than 7 fractional second digits with precision set to 7. + { 7, "2023-12-24T15:30:00.12345678" }, // More than 6 fractional second digits with precision set to 6. { 6, "2023-12-24T15:30:00.1234567" }, // More than 5 fractional second digits with precision set to 5. diff --git a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs index 1b41874f8cb..969d6b29518 100644 --- a/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs +++ b/src/HotChocolate/Core/test/Types.Tests/Types/Scalars/LocalTimeTypeTests.cs @@ -323,6 +323,8 @@ public void LocalTime_Relaxed_Format_Check() [InlineData(5, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,5})?$")] [InlineData(6, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?$")] [InlineData(7, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,7})?$")] + [InlineData(8, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,8})?$")] + [InlineData(9, @"^\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?$")] public void Pattern_Should_Match_InputPrecision(byte precision, string expectedPattern) { // arrange & act @@ -372,8 +374,8 @@ public static TheoryData ValidInput() }, { DateTimeOptions.DefaultInputPrecision, - "07:30:00.1234567", - new TimeOnly(7, 30, 0, 123, 456).Add(TimeSpan.FromTicks(7)) + "07:30:00.123456789", // Rounded to ".1234568". + new TimeOnly(7, 30, 0, 123, 456).Add(TimeSpan.FromTicks(8)) } }; } @@ -395,9 +397,13 @@ public static TheoryData InvalidInput() { DateTimeOptions.DefaultInputPrecision, "24:00:00" }, // Invalid minute (60). { DateTimeOptions.DefaultInputPrecision, "15:60:00" }, - // More than 7 fractional second digits. - { DateTimeOptions.DefaultInputPrecision, "15:30:00.12345678" }, + // More than 9 fractional second digits. + { DateTimeOptions.DefaultInputPrecision, "15:30:00.1234567890" }, // Additional cases. + // More than 8 fractional second digits with precision set to 8. + { 8, "15:30:00.123456789" }, + // More than 7 fractional second digits with precision set to 7. + { 7, "15:30:00.12345678" }, // More than 6 fractional second digits with precision set to 6. { 6, "15:30:00.1234567" }, // More than 5 fractional second digits with precision set to 5.