From 1df3e6a631c784d68c52be2f12118f7244608006 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Fri, 28 Nov 2025 15:40:26 -0700 Subject: [PATCH 01/15] (wip) start implementing reads for +-infinity date/timestamp --- .../NativeMethods/NativeMethods.DateTime.cs | 15 +++ .../Reader/DateTimeVectorDataReader.cs | 103 ++++++++++++++++-- .../Extensions/DateTimeExtensions.cs | 16 +++ 3 files changed, 125 insertions(+), 9 deletions(-) diff --git a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs index f631a9f2..f3e9e152 100644 --- a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs +++ b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs @@ -30,5 +30,20 @@ public static class DateTimeHelpers [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_to_timestamp")] public static extern DuckDBTimestampStruct DuckDBToTimestamp(DuckDBTimestamp dateStruct); + + [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_date")] + public static extern bool DuckDBIsFiniteDate(DuckDBDate date); + + [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp")] + public static extern bool DuckDBIsFiniteTimestamp(DuckDBTimestampStruct ts); + + [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp_s")] + public static extern bool DuckDBIsFiniteTimestampS(DuckDBTimestampStruct ts); + + [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp_ms")] + public static extern bool DuckDBIsFiniteTimestampMs(DuckDBTimestampStruct ts); + + [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp_ns")] + public static extern bool DuckDBIsFiniteTimestampNs(DuckDBTimestampStruct ts); } } \ No newline at end of file diff --git a/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs b/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs index 37f678f0..c0c45f76 100644 --- a/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs +++ b/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs @@ -28,7 +28,25 @@ protected override T GetValidValue(ulong offset, Type targetType) { if (DuckDBType == DuckDBType.Date) { - var dateOnly = GetDateOnly(offset); + var (dateOnly, isFinite, isPositiveInfinity) = GetDateOnly(offset); + + if (!isFinite) + { + if (targetType == DateTimeType || targetType == DateTimeNullableType) + { + var dateTime = isPositiveInfinity ? DateTime.MaxValue : DateTime.MinValue; + return (T)(object)dateTime; + } + +#if NET6_0_OR_GREATER + if (targetType == DateOnlyType || targetType == DateOnlyNullableType) + { + var dateOnlyValue = isPositiveInfinity ? DateOnly.MaxValue : DateOnly.MinValue; + return (T)(object)dateOnlyValue; + } +#endif + return (T)(object)dateOnly; + } if (targetType == DateTimeType || targetType == DateTimeNullableType) { @@ -81,8 +99,8 @@ protected override T GetValidValue(ulong offset, Type targetType) return DuckDBType switch { - DuckDBType.Timestamp or DuckDBType.TimestampS or - DuckDBType.TimestampTz or DuckDBType.TimestampMs or + DuckDBType.Timestamp or DuckDBType.TimestampS or + DuckDBType.TimestampTz or DuckDBType.TimestampMs or DuckDBType.TimestampNs => ReadTimestamp(offset, targetType), _ => base.GetValidValue(offset, targetType) }; @@ -90,7 +108,31 @@ DuckDBType.TimestampTz or DuckDBType.TimestampMs or private T ReadTimestamp(ulong offset, Type targetType) { - var (timestamp, additionalTicks) = GetFieldData(offset).ToDuckDBTimestamp(DuckDBType); + var timestampStruct = GetFieldData(offset); + + if (!timestampStruct.IsFinite(DuckDBType)) + { + if (targetType == DateTimeType || targetType == DateTimeNullableType) + { + var dateTime = timestampStruct.IsPositiveInfinity() ? DateTime.MaxValue : DateTime.MinValue; + return (T)(object)dateTime; + } + + if (targetType == DateTimeOffsetType || targetType == DateTimeOffsetNullableType) + { + var dateTime = timestampStruct.IsPositiveInfinity() ? DateTime.MaxValue : DateTime.MinValue; + var dateTimeOffset = new DateTimeOffset(dateTime, TimeSpan.Zero); + return (T)(object)dateTimeOffset; + } + + // As of 1.4.2, duckdb_from_timestamp throws a ConversionException + // for infinity, so infinity values are only successfully returned + // for DateTime and DateOnly types. + var infinityTimestamp = NativeMethods.DateTimeHelpers.DuckDBFromTimestamp(timestampStruct); + return (T)(object)infinityTimestamp; + } + + var (timestamp, additionalTicks) = timestampStruct.ToDuckDBTimestamp(DuckDBType); if (targetType == DateTimeType || targetType == DateTimeNullableType) { @@ -115,7 +157,7 @@ internal override object GetValue(ulong offset, Type targetType) DuckDBType.Date => GetDate(offset, targetType), DuckDBType.Time => GetTime(offset, targetType), DuckDBType.TimeTz => GetDateTimeOffset(offset, targetType), - DuckDBType.Timestamp or DuckDBType.TimestampS or + DuckDBType.Timestamp or DuckDBType.TimestampS or DuckDBType.TimestampTz or DuckDBType.TimestampMs or DuckDBType.TimestampNs => GetDateTime(offset, targetType), _ => base.GetValue(offset, targetType) @@ -134,14 +176,36 @@ private DuckDBTimeOnly GetTimeOnly(ulong offset) return NativeMethods.DateTimeHelpers.DuckDBFromTime(GetFieldData(offset)); } - private DuckDBDateOnly GetDateOnly(ulong offset) + private (DuckDBDateOnly dateOnly, bool IsFinite, bool IsPositiveInfinity) GetDateOnly(ulong offset) { - return NativeMethods.DateTimeHelpers.DuckDBFromDate(GetFieldData(offset)); + var date = GetFieldData(offset); + var isFinite = NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(date); + var isPositiveInfinity = date.Days == int.MaxValue; + return (NativeMethods.DateTimeHelpers.DuckDBFromDate(date), isFinite, isPositiveInfinity); } private object GetDate(ulong offset, Type targetType) { - var dateOnly = GetDateOnly(offset); + var (dateOnly, isFinite, isPositiveInfinity) = GetDateOnly(offset); + + if (!isFinite) + { + if (targetType == DateTimeType) + { + return isPositiveInfinity ? DateTime.MaxValue : DateTime.MinValue; + } + +#if NET6_0_OR_GREATER + if (targetType == DateOnlyType) + { + return isPositiveInfinity ? DateOnly.MaxValue : DateOnly.MinValue; + } +#endif + + return dateOnly; + } + + if (targetType == DateTimeType) { return (DateTime)dateOnly; @@ -177,7 +241,28 @@ private object GetTime(ulong offset, Type targetType) private object GetDateTime(ulong offset, Type targetType) { - var (timestamp, additionalTicks) = GetFieldData(offset).ToDuckDBTimestamp(DuckDBType); + var timestampStruct = GetFieldData(offset); + + if (!timestampStruct.IsFinite(DuckDBType)) + { + if (targetType == typeof(DateTime)) + { + return timestampStruct.IsPositiveInfinity() ? DateTime.MaxValue : DateTime.MinValue; + } + + if (targetType == DateTimeOffsetType) + { + var dateTime = timestampStruct.IsPositiveInfinity() ? DateTime.MaxValue : DateTime.MinValue; + return new DateTimeOffset(dateTime, TimeSpan.Zero); + } + + // As of 1.4.2, duckdb_from_timestamp throws a ConversionException + // for infinity, so infinity values are only successfully returned + // for DateTime and DateOnly types. + return timestampStruct.ToDuckDBTimestamp(DuckDBType); + } + + var (timestamp, additionalTicks) = timestampStruct.ToDuckDBTimestamp(DuckDBType); if (targetType == typeof(DateTime)) { diff --git a/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs b/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs index 3c2c20a0..71193e9d 100644 --- a/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs +++ b/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs @@ -81,4 +81,20 @@ public static (DuckDBTimestamp result, int additionalTicks) ToDuckDBTimestamp(th return (result, additionalTicks); } + + public static bool IsFinite(this DuckDBTimestampStruct timestamp, DuckDBType duckDBType) + { + return duckDBType switch + { + DuckDBType.TimestampNs => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(timestamp), + DuckDBType.TimestampMs => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(timestamp), + DuckDBType.TimestampS => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(timestamp), + _ => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(timestamp) + }; + } + + public static bool IsPositiveInfinity(this DuckDBTimestampStruct timestamp) + { + return timestamp.Micros == long.MaxValue; + } } \ No newline at end of file From 23bbfc28e1ac232f76825aae02cef2fdd3093d9b Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Fri, 28 Nov 2025 15:54:32 -0700 Subject: [PATCH 02/15] Start testing infinity reading --- .../DuckDBDataReaderInfinityTests.cs | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs diff --git a/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs b/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs new file mode 100644 index 00000000..28086e6c --- /dev/null +++ b/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs @@ -0,0 +1,252 @@ +using DuckDB.NET.Data; +using DuckDB.NET.Native; +using FluentAssertions; +using System; +using Xunit; + +namespace DuckDB.NET.Test; + +public class DuckDBDataReaderInfinityTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) +{ + [Fact] + public void ReadInfinityDate() + { + Command.CommandText = "SELECT 'infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetDateTime(0).Should().Be(DateTime.MaxValue); + reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + } + + [Fact] + public void ReadNegativeInfinityDate() + { + Command.CommandText = "SELECT '-infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetDateTime(0).Should().Be(DateTime.MinValue); + reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + } + +#if NET6_0_OR_GREATER + [Fact] + public void ReadInfinityDateAsDateOnly() + { + Command.CommandText = "SELECT 'infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(DateOnly.MaxValue); + } + + [Fact] + public void ReadNegativeInfinityDateAsDateOnly() + { + Command.CommandText = "SELECT '-infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(DateOnly.MinValue); + } +#endif + + [Fact] + public void ReadInfinityTimestamp() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetDateTime(0).Should().Be(DateTime.MaxValue); + reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + } + + [Fact] + public void ReadNegativeInfinityTimestamp() + { + Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetDateTime(0).Should().Be(DateTime.MinValue); + reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + } + + [Fact] + public void ReadInfinityTimestampAsDateTimeOffset() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MaxValue, TimeSpan.Zero)); + } + + [Fact] + public void ReadNegativeInfinityTimestampAsDateTimeOffset() + { + Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero)); + } + + [Fact] + public void ReadInfinityTimestampTz() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetDateTime(0).Should().Be(DateTime.MaxValue); + reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + } + + [Fact] + public void ReadNegativeInfinityTimestampTz() + { + Command.CommandText = "SELECT '-infinity'::TIMESTAMPTZ"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetDateTime(0).Should().Be(DateTime.MinValue); + reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + } + + [Fact] + public void ReadInfinityTimestampTzAsDateTimeOffset() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MaxValue, TimeSpan.Zero)); + } + + [Fact] + public void ReadNegativeInfinityTimestampTzAsDateTimeOffset() + { + Command.CommandText = "SELECT '-infinity'::TIMESTAMPTZ"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero)); + } + + [Fact] + public void ReadMixedInfinityDates() + { + Command.CommandText = "SELECT * FROM (VALUES ('infinity'::DATE), ('-infinity'::DATE), ('2024-01-15'::DATE)) AS t(d)"; + using var reader = Command.ExecuteReader(); + + reader.Read(); + reader.GetDateTime(0).Should().Be(DateTime.MaxValue); + + reader.Read(); + reader.GetDateTime(0).Should().Be(DateTime.MinValue); + + reader.Read(); + reader.GetDateTime(0).Should().Be(new DateTime(2024, 1, 15)); + } + + [Fact] + public void ReadMixedInfinityTimestamps() + { + Command.CommandText = "SELECT * FROM (VALUES ('infinity'::TIMESTAMP), ('-infinity'::TIMESTAMP), ('2024-01-15 12:30:45'::TIMESTAMP)) AS t(ts)"; + using var reader = Command.ExecuteReader(); + + reader.Read(); + reader.GetDateTime(0).Should().Be(DateTime.MaxValue); + + reader.Read(); + reader.GetDateTime(0).Should().Be(DateTime.MinValue); + + reader.Read(); + reader.GetDateTime(0).Should().Be(new DateTime(2024, 1, 15, 12, 30, 45)); + } + + [Fact] + public void ReadNullableInfinityDate() + { + Command.CommandText = "SELECT 'infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + } + + [Fact] + public void ReadNullableNegativeInfinityDate() + { + Command.CommandText = "SELECT '-infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + } + + [Fact] + public void ReadNullableInfinityTimestamp() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + } + + [Fact] + public void ReadNullableNegativeInfinityTimestamp() + { + Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + } + +#if NET6_0_OR_GREATER + [Fact] + public void ReadNullableInfinityDateAsDateOnly() + { + Command.CommandText = "SELECT 'infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(DateOnly.MaxValue); + } + + [Fact] + public void ReadNullableNegativeInfinityDateAsDateOnly() + { + Command.CommandText = "SELECT '-infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(DateOnly.MinValue); + } +#endif + + [Fact] + public void ReadNullableInfinityTimestampAsDateTimeOffset() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MaxValue, TimeSpan.Zero)); + } + + [Fact] + public void ReadNullableNegativeInfinityTimestampAsDateTimeOffset() + { + Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero)); + } +} From df8ed0c544648949cdc82fef77565523f534c09b Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 1 Dec 2025 12:08:49 -0700 Subject: [PATCH 03/15] Fix duckdb_*_is_finite marshalling --- .../NativeMethods/NativeMethods.DateTime.cs | 9 ++- .../DuckDBDataReaderInfinityTests.cs | 81 ++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs index f3e9e152..da594d05 100644 --- a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs +++ b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs @@ -31,19 +31,26 @@ public static class DateTimeHelpers [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_to_timestamp")] public static extern DuckDBTimestampStruct DuckDBToTimestamp(DuckDBTimestamp dateStruct); + // NOTE: for boolean return values, MarshalAs(UnmanagedType.I1) is used because the default is to use 4-byte Win32 BOOLs + // https://learn.microsoft.com/en-us/dotnet/standard/native-interop/customize-struct-marshalling#customizing-boolean-field-marshalling [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_date")] + [return: MarshalAs(UnmanagedType.I1)] public static extern bool DuckDBIsFiniteDate(DuckDBDate date); [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp")] + [return: MarshalAs(UnmanagedType.I1)] public static extern bool DuckDBIsFiniteTimestamp(DuckDBTimestampStruct ts); [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp_s")] + [return: MarshalAs(UnmanagedType.I1)] public static extern bool DuckDBIsFiniteTimestampS(DuckDBTimestampStruct ts); [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp_ms")] + [return: MarshalAs(UnmanagedType.I1)] public static extern bool DuckDBIsFiniteTimestampMs(DuckDBTimestampStruct ts); [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp_ns")] + [return: MarshalAs(UnmanagedType.I1)] public static extern bool DuckDBIsFiniteTimestampNs(DuckDBTimestampStruct ts); } -} \ No newline at end of file +} diff --git a/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs b/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs index 28086e6c..7ed5122f 100644 --- a/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs +++ b/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs @@ -8,6 +8,71 @@ namespace DuckDB.NET.Test; public class DuckDBDataReaderInfinityTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) { + [Fact] + public void duckdb_date_is_finite() + { + var positiveInfinity = new DuckDBDate { Days = Int32.MaxValue }; + var negativeInfinity = new DuckDBDate { Days = -Int32.MaxValue }; // NOTE: -MaxValue, not MinValue + var finitePositive = new DuckDBDate { Days = Int32.MaxValue - 1 }; + var finiteNegative = new DuckDBDate { Days = -Int32.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(negativeInfinity).Should().BeFalse(); + } + + [Fact] + public void duckdb_timestamp_is_finite() + { + var positiveInfinity = new DuckDBTimestampStruct { Micros = Int64.MaxValue }; + var negativeInfinity = new DuckDBTimestampStruct { Micros = -Int64.MaxValue }; + var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; + var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(negativeInfinity).Should().BeFalse(); + } + + [Fact] + public void duckdb_timestamp_s_is_finite() + { + var positiveInfinity = new DuckDBTimestampStruct { Micros = Int64.MaxValue }; + var negativeInfinity = new DuckDBTimestampStruct { Micros = -Int64.MaxValue }; + var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; + var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(negativeInfinity).Should().BeFalse(); + } + + [Fact] + public void duckdb_timestamp_ms_is_finite() + { + var positiveInfinity = new DuckDBTimestampStruct { Micros = Int64.MaxValue }; + var negativeInfinity = new DuckDBTimestampStruct { Micros = -Int64.MaxValue }; + var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; + var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(negativeInfinity).Should().BeFalse(); + } + + [Fact] + public void duckdb_timestamp_ns_is_finite() + { + var positiveInfinity = new DuckDBTimestampStruct { Micros = Int64.MaxValue }; + var negativeInfinity = new DuckDBTimestampStruct { Micros = -Int64.MaxValue }; + var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; + var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(negativeInfinity).Should().BeFalse(); + } + [Fact] public void ReadInfinityDate() { @@ -55,23 +120,35 @@ public void ReadNegativeInfinityDateAsDateOnly() [Fact] public void ReadInfinityTimestamp() { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMP, 'infinity'::TIMESTAMP_NS, 'infinity'::TIMESTAMP_MS, 'infinity'::TIMESTAMP_S"; using var reader = Command.ExecuteReader(); reader.Read(); reader.GetDateTime(0).Should().Be(DateTime.MaxValue); reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + reader.GetDateTime(1).Should().Be(DateTime.MaxValue); + reader.GetFieldValue(1).Should().Be(DateTime.MaxValue); + reader.GetDateTime(2).Should().Be(DateTime.MaxValue); + reader.GetFieldValue(2).Should().Be(DateTime.MaxValue); + reader.GetDateTime(3).Should().Be(DateTime.MaxValue); + reader.GetFieldValue(3).Should().Be(DateTime.MaxValue); } [Fact] public void ReadNegativeInfinityTimestamp() { - Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; + Command.CommandText = "SELECT '-infinity'::TIMESTAMP, '-infinity'::TIMESTAMP_NS, '-infinity'::TIMESTAMP_MS, '-infinity'::TIMESTAMP_S"; using var reader = Command.ExecuteReader(); reader.Read(); reader.GetDateTime(0).Should().Be(DateTime.MinValue); reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + reader.GetDateTime(1).Should().Be(DateTime.MinValue); + reader.GetFieldValue(1).Should().Be(DateTime.MinValue); + reader.GetDateTime(2).Should().Be(DateTime.MinValue); + reader.GetFieldValue(2).Should().Be(DateTime.MinValue); + reader.GetDateTime(3).Should().Be(DateTime.MinValue); + reader.GetFieldValue(3).Should().Be(DateTime.MinValue); } [Fact] From 35bb3c7f377f2f2753ddd3554b68f8f5a1ca18f6 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 1 Dec 2025 19:21:09 -0700 Subject: [PATCH 04/15] Use infinity constants and special case them in conversions --- DuckDB.NET.Bindings/DuckDBDateOnly.cs | 71 +++++++++++++++++-- DuckDB.NET.Bindings/DuckDBNativeObjects.cs | 14 ++++ DuckDB.NET.Bindings/DuckDBTimestamp.cs | 59 ++++++++++++++- DuckDB.NET.Bindings/DuckDBWrapperObjects.cs | 16 ++--- .../Reader/DateTimeVectorDataReader.cs | 4 +- .../Writer/DateTimeVectorDataWriter.cs | 6 +- .../Extensions/DateTimeExtensions.cs | 6 +- .../PreparedStatement/ClrToDuckDBConverter.cs | 10 +-- .../PreparedStatement/DuckDBTypeMap.cs | 3 +- 9 files changed, 159 insertions(+), 30 deletions(-) diff --git a/DuckDB.NET.Bindings/DuckDBDateOnly.cs b/DuckDB.NET.Bindings/DuckDBDateOnly.cs index db32b465..98e55bd6 100644 --- a/DuckDB.NET.Bindings/DuckDBDateOnly.cs +++ b/DuckDB.NET.Bindings/DuckDBDateOnly.cs @@ -6,6 +6,22 @@ namespace DuckDB.NET.Native; [StructLayout(LayoutKind.Sequential)] public readonly struct DuckDBDateOnly(int year, byte month, byte day) { + /// + /// Represents positive infinity for DuckDB dates. + /// + public static readonly DuckDBDateOnly PositiveInfinity = + // This is the value returned by DuckDB for positive infinity dates when + // passed to duckdb_from_date, and it is used for backwards compatibility + new(5881580, 7, 11); + + /// + /// Represents negative infinity for DuckDB dates. + /// + public static readonly DuckDBDateOnly NegativeInfinity = + // This is the value returned by DuckDB for negative infinity dates when + // passed to duckdb_from_date, and it is used for backwards compatibility. + new(-5877641, 6, 24); + public int Year { get; } = year; public byte Month { get; } = month; @@ -14,19 +30,60 @@ public readonly struct DuckDBDateOnly(int year, byte month, byte day) internal static readonly DuckDBDateOnly MinValue = FromDateTime(DateTime.MinValue); + /// + /// Returns true if this date represents positive or negative infinity. + /// + public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity; + + /// + /// Returns true if this date represents positive infinity. + /// + public bool IsPositiveInfinity => Equals(PositiveInfinity); + + /// + /// Returns true if this date represents negative infinity. + /// + public bool IsNegativeInfinity => Equals(NegativeInfinity); + public static DuckDBDateOnly FromDateTime(DateTime dateTime) => new DuckDBDateOnly(dateTime.Year, (byte)dateTime.Month, (byte)dateTime.Day); public DateTime ToDateTime() => new DateTime(Year, Month, Day); + /// + /// Converts a DuckDBDate to DuckDBDateOnly, handling infinity values. + /// + public static DuckDBDateOnly FromDuckDBDate(DuckDBDate date) + { + if (date.IsPositiveInfinity) + return PositiveInfinity; + if (date.IsNegativeInfinity) + return NegativeInfinity; + + return NativeMethods.DateTimeHelpers.DuckDBFromDate(date); + } + + /// + /// Converts this DuckDBDateOnly to a DuckDBDate, handling infinity values. + /// + public DuckDBDate ToDuckDBDate() + { + if (IsPositiveInfinity) + return DuckDBDate.PositiveInfinity; + if (IsNegativeInfinity) + return DuckDBDate.NegativeInfinity; + + return NativeMethods.DateTimeHelpers.DuckDBToDate(this); + } + public static explicit operator DateTime(DuckDBDateOnly dateOnly) => dateOnly.ToDateTime(); - + public static explicit operator DuckDBDateOnly(DateTime dateTime) => FromDateTime(dateTime); - + #if NET6_0_OR_GREATER - + public static implicit operator DateOnly(DuckDBDateOnly dateOnly) => new DateOnly(dateOnly.Year, dateOnly.Month, dateOnly.Day); - - public static implicit operator DuckDBDateOnly(DateOnly date) => new DuckDBDateOnly(date.Year, (byte)date.Month, (byte) date.Day); - + + public static implicit operator DuckDBDateOnly(DateOnly date) => new DuckDBDateOnly(date.Year, (byte)date.Month, (byte)date.Day); + #endif -} \ No newline at end of file +} diff --git a/DuckDB.NET.Bindings/DuckDBNativeObjects.cs b/DuckDB.NET.Bindings/DuckDBNativeObjects.cs index d13ceaa3..663f957c 100644 --- a/DuckDB.NET.Bindings/DuckDBNativeObjects.cs +++ b/DuckDB.NET.Bindings/DuckDBNativeObjects.cs @@ -115,7 +115,14 @@ public void Close() [StructLayout(LayoutKind.Sequential)] public struct DuckDBDate { + public static readonly DuckDBDate PositiveInfinity = new() { Days = int.MaxValue }; + public static readonly DuckDBDate NegativeInfinity = new() { Days = -int.MaxValue }; + public int Days { get; set; } + + public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity; + public bool IsPositiveInfinity => Days == int.MaxValue; + public bool IsNegativeInfinity => Days == -int.MaxValue; } [StructLayout(LayoutKind.Sequential)] @@ -140,7 +147,14 @@ public struct DuckDBTimeTz [StructLayout(LayoutKind.Sequential)] public struct DuckDBTimestampStruct { + public static readonly DuckDBTimestampStruct PositiveInfinity = new() { Micros = long.MaxValue }; + public static readonly DuckDBTimestampStruct NegativeInfinity = new() { Micros = -long.MaxValue }; + public long Micros { get; set; } + + public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity; + public bool IsPositiveInfinity => Micros == long.MaxValue; + public bool IsNegativeInfinity => Micros == -long.MaxValue; } [StructLayout(LayoutKind.Sequential)] diff --git a/DuckDB.NET.Bindings/DuckDBTimestamp.cs b/DuckDB.NET.Bindings/DuckDBTimestamp.cs index e52b2d26..27f022e2 100644 --- a/DuckDB.NET.Bindings/DuckDBTimestamp.cs +++ b/DuckDB.NET.Bindings/DuckDBTimestamp.cs @@ -6,9 +6,40 @@ namespace DuckDB.NET.Native; [StructLayout(LayoutKind.Sequential)] public readonly struct DuckDBTimestamp(DuckDBDateOnly date, DuckDBTimeOnly time) { + /// + /// Represents positive infinity for DuckDB timestamps. + /// + public static readonly DuckDBTimestamp PositiveInfinity = + // The +infinity date value is not representable by the timestamp type, + // so this constant should never occur in normal usage + new(DuckDBDateOnly.PositiveInfinity, new DuckDBTimeOnly(0, 0, 0)); + + /// + /// Represents negative infinity for DuckDB timestamps. + /// + public static readonly DuckDBTimestamp NegativeInfinity = + // The -infinity date value is not representable by the timestamp type, + // so this constant should never occur in normal usage + new(DuckDBDateOnly.NegativeInfinity, new DuckDBTimeOnly(0, 0, 0)); + public DuckDBDateOnly Date { get; } = date; public DuckDBTimeOnly Time { get; } = time; + /// + /// Returns true if this timestamp represents positive or negative infinity. + /// + public bool IsInfinity => Date.IsInfinity; + + /// + /// Returns true if this timestamp represents positive infinity. + /// + public bool IsPositiveInfinity => Date.IsPositiveInfinity; + + /// + /// Returns true if this timestamp represents negative infinity. + /// + public bool IsNegativeInfinity => Date.IsNegativeInfinity; + public DateTime ToDateTime() { return new DateTime(Date.Year, Date.Month, Date.Day).AddTicks(Time.Ticks); @@ -18,4 +49,30 @@ public static DuckDBTimestamp FromDateTime(DateTime dateTime) { return new DuckDBTimestamp(DuckDBDateOnly.FromDateTime(dateTime), DuckDBTimeOnly.FromDateTime(dateTime)); } -} \ No newline at end of file + + /// + /// Converts a DuckDBTimestampStruct to DuckDBTimestamp, handling infinity values. + /// + public static DuckDBTimestamp FromDuckDBTimestampStruct(DuckDBTimestampStruct timestampStruct) + { + if (timestampStruct.IsPositiveInfinity) + return PositiveInfinity; + if (timestampStruct.IsNegativeInfinity) + return NegativeInfinity; + + return NativeMethods.DateTimeHelpers.DuckDBFromTimestamp(timestampStruct); + } + + /// + /// Converts this DuckDBTimestamp to a DuckDBTimestampStruct, handling infinity values. + /// + public DuckDBTimestampStruct ToDuckDBTimestampStruct() + { + if (IsPositiveInfinity) + return DuckDBTimestampStruct.PositiveInfinity; + if (IsNegativeInfinity) + return DuckDBTimestampStruct.NegativeInfinity; + + return NativeMethods.DateTimeHelpers.DuckDBToTimestamp(this); + } +} diff --git a/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs b/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs index 65b664bb..399967f0 100644 --- a/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs +++ b/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs @@ -101,7 +101,7 @@ protected override bool ReleaseHandle() { value.Dispose(); } - + NativeMethods.Value.DuckDBDestroyValue(ref handle); return true; } @@ -140,26 +140,26 @@ public T GetValue() DuckDBType.Float => Cast(NativeMethods.Value.DuckDBGetFloat(this)), DuckDBType.Double => Cast(NativeMethods.Value.DuckDBGetDouble(this)), - + DuckDBType.Decimal => Cast(decimal.Parse(NativeMethods.Value.DuckDBGetVarchar(this), NumberStyles.Any, CultureInfo.InvariantCulture)), - + DuckDBType.Uuid => Cast(new Guid(NativeMethods.Value.DuckDBGetVarchar(this))), - + DuckDBType.HugeInt => Cast(NativeMethods.Value.DuckDBGetHugeInt(this).ToBigInteger()), DuckDBType.UnsignedHugeInt => Cast(NativeMethods.Value.DuckDBGetUHugeInt(this).ToBigInteger()), - + DuckDBType.Varchar => Cast(NativeMethods.Value.DuckDBGetVarchar(this)), #if NET6_0_OR_GREATER - DuckDBType.Date => Cast((DateOnly)NativeMethods.DateTimeHelpers.DuckDBFromDate(NativeMethods.Value.DuckDBGetDate(this))), + DuckDBType.Date => Cast((DateOnly)DuckDBDateOnly.FromDuckDBDate(NativeMethods.Value.DuckDBGetDate(this))), DuckDBType.Time => Cast((TimeOnly)NativeMethods.DateTimeHelpers.DuckDBFromTime(NativeMethods.Value.DuckDBGetTime(this))), #else - DuckDBType.Date => Cast(NativeMethods.DateTimeHelpers.DuckDBFromDate(NativeMethods.Value.DuckDBGetDate(this)).ToDateTime()), + DuckDBType.Date => Cast(DuckDBDateOnly.FromDuckDBDate(NativeMethods.Value.DuckDBGetDate(this)).ToDateTime()), DuckDBType.Time => Cast(NativeMethods.DateTimeHelpers.DuckDBFromTime(NativeMethods.Value.DuckDBGetTime(this)).ToDateTime()), #endif //DuckDBType.TimeTz => expr, DuckDBType.Interval => Cast((TimeSpan)NativeMethods.Value.DuckDBGetInterval(this)), - DuckDBType.Timestamp => Cast(NativeMethods.DateTimeHelpers.DuckDBFromTimestamp(NativeMethods.Value.DuckDBGetTimestamp(this)).ToDateTime()), + DuckDBType.Timestamp => Cast(DuckDBTimestamp.FromDuckDBTimestampStruct(NativeMethods.Value.DuckDBGetTimestamp(this)).ToDateTime()), //DuckDBType.TimestampS => expr, //DuckDBType.TimestampMs => expr, //DuckDBType.TimestampNs => expr, diff --git a/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs b/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs index c0c45f76..0876e308 100644 --- a/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs +++ b/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs @@ -128,7 +128,7 @@ private T ReadTimestamp(ulong offset, Type targetType) // As of 1.4.2, duckdb_from_timestamp throws a ConversionException // for infinity, so infinity values are only successfully returned // for DateTime and DateOnly types. - var infinityTimestamp = NativeMethods.DateTimeHelpers.DuckDBFromTimestamp(timestampStruct); + var infinityTimestamp = DuckDBTimestamp.FromDuckDBTimestampStruct(timestampStruct); return (T)(object)infinityTimestamp; } @@ -181,7 +181,7 @@ private DuckDBTimeOnly GetTimeOnly(ulong offset) var date = GetFieldData(offset); var isFinite = NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(date); var isPositiveInfinity = date.Days == int.MaxValue; - return (NativeMethods.DateTimeHelpers.DuckDBFromDate(date), isFinite, isPositiveInfinity); + return (DuckDBDateOnly.FromDuckDBDate(date), isFinite, isPositiveInfinity); } private object GetDate(ulong offset, Type targetType) diff --git a/DuckDB.NET.Data/DataChunk/Writer/DateTimeVectorDataWriter.cs b/DuckDB.NET.Data/DataChunk/Writer/DateTimeVectorDataWriter.cs index a9e5a0db..1550cd46 100644 --- a/DuckDB.NET.Data/DataChunk/Writer/DateTimeVectorDataWriter.cs +++ b/DuckDB.NET.Data/DataChunk/Writer/DateTimeVectorDataWriter.cs @@ -10,7 +10,7 @@ internal override bool AppendDateTime(DateTime value, ulong rowIndex) { if (ColumnType == DuckDBType.Date) { - return AppendValueInternal(NativeMethods.DateTimeHelpers.DuckDBToDate((DuckDBDateOnly)value.Date), rowIndex); + return AppendValueInternal(((DuckDBDateOnly)value.Date).ToDuckDBDate(), rowIndex); } var timestamp = value.ToTimestampStruct(ColumnType); @@ -38,12 +38,12 @@ internal override bool AppendDateTimeOffset(DateTimeOffset value, ulong rowIndex } #if NET6_0_OR_GREATER - internal override bool AppendDateOnly(DateOnly value, ulong rowIndex) => AppendValueInternal(NativeMethods.DateTimeHelpers.DuckDBToDate(value), rowIndex); + internal override bool AppendDateOnly(DateOnly value, ulong rowIndex) => AppendValueInternal(((DuckDBDateOnly)value).ToDuckDBDate(), rowIndex); internal override bool AppendTimeOnly(TimeOnly value, ulong rowIndex) => AppendValueInternal(NativeMethods.DateTimeHelpers.DuckDBToTime(value), rowIndex); #endif - internal override bool AppendDateOnly(DuckDBDateOnly value, ulong rowIndex) => AppendValueInternal(NativeMethods.DateTimeHelpers.DuckDBToDate(value), rowIndex); + internal override bool AppendDateOnly(DuckDBDateOnly value, ulong rowIndex) => AppendValueInternal(value.ToDuckDBDate(), rowIndex); internal override bool AppendTimeOnly(DuckDBTimeOnly value, ulong rowIndex) => AppendValueInternal(NativeMethods.DateTimeHelpers.DuckDBToTime(value), rowIndex); } diff --git a/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs b/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs index 71193e9d..57c7a61e 100644 --- a/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs +++ b/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs @@ -28,14 +28,14 @@ public static DuckDBTimeTzStruct ToTimeTzStruct(this DateTimeOffset value) public static DuckDBTimestampStruct ToTimestampStruct(this DateTimeOffset value) { - var timestamp = NativeMethods.DateTimeHelpers.DuckDBToTimestamp(DuckDBTimestamp.FromDateTime(value.UtcDateTime)); + var timestamp = DuckDBTimestamp.FromDateTime(value.UtcDateTime).ToDuckDBTimestampStruct(); return timestamp; } public static DuckDBTimestampStruct ToTimestampStruct(this DateTime value, DuckDBType duckDBType) { - var timestamp = NativeMethods.DateTimeHelpers.DuckDBToTimestamp(DuckDBTimestamp.FromDateTime(value)); + var timestamp = DuckDBTimestamp.FromDateTime(value).ToDuckDBTimestampStruct(); if (duckDBType == DuckDBType.TimestampNs) { @@ -77,7 +77,7 @@ public static (DuckDBTimestamp result, int additionalTicks) ToDuckDBTimestamp(th timestamp.Micros *= 1000000; } - var result = NativeMethods.DateTimeHelpers.DuckDBFromTimestamp(timestamp); + var result = DuckDBTimestamp.FromDuckDBTimestampStruct(timestamp); return (result, additionalTicks); } diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index 427719d7..e5b9b49a 100644 --- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs +++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs @@ -37,9 +37,9 @@ internal static class ClrToDuckDBConverter { DbType.Date, value => { #if NET6_0_OR_GREATER - var date = NativeMethods.DateTimeHelpers.DuckDBToDate(value is DateOnly dateOnly ? (DuckDBDateOnly)dateOnly : (DuckDBDateOnly)value); + var date = (value is DateOnly dateOnly ? (DuckDBDateOnly)dateOnly : (DuckDBDateOnly)value).ToDuckDBDate(); #else - var date = NativeMethods.DateTimeHelpers.DuckDBToDate((DuckDBDateOnly)value); + var date = ((DuckDBDateOnly)value).ToDuckDBDate(); #endif return NativeMethods.Value.DuckDBCreateDate(date); } @@ -95,12 +95,12 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log (DuckDBType.TimestampTz, DateTime value) => NativeMethods.Value.DuckDBCreateTimestampTz(value.ToTimestampStruct(duckDBType)), (DuckDBType.TimestampTz, DateTimeOffset value) => NativeMethods.Value.DuckDBCreateTimestampTz(value.ToTimestampStruct()), (DuckDBType.Interval, TimeSpan value) => NativeMethods.Value.DuckDBCreateInterval(value), - (DuckDBType.Date, DateTime value) => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate((DuckDBDateOnly)value)), - (DuckDBType.Date, DuckDBDateOnly value) => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate(value)), + (DuckDBType.Date, DateTime value) => NativeMethods.Value.DuckDBCreateDate(((DuckDBDateOnly)value).ToDuckDBDate()), + (DuckDBType.Date, DuckDBDateOnly value) => NativeMethods.Value.DuckDBCreateDate(value.ToDuckDBDate()), (DuckDBType.Time, DateTime value) => NativeMethods.Value.DuckDBCreateTime(NativeMethods.DateTimeHelpers.DuckDBToTime((DuckDBTimeOnly)value)), (DuckDBType.Time, DuckDBTimeOnly value) => NativeMethods.Value.DuckDBCreateTime(NativeMethods.DateTimeHelpers.DuckDBToTime(value)), #if NET6_0_OR_GREATER - (DuckDBType.Date, DateOnly value) => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate(value)), + (DuckDBType.Date, DateOnly value) => NativeMethods.Value.DuckDBCreateDate(((DuckDBDateOnly)value).ToDuckDBDate()), (DuckDBType.Time, TimeOnly value) => NativeMethods.Value.DuckDBCreateTime(NativeMethods.DateTimeHelpers.DuckDBToTime(value)), #endif (DuckDBType.TimeTz, DateTimeOffset value) => NativeMethods.Value.DuckDBCreateTimeTz(value.ToTimeTzStruct()), diff --git a/DuckDB.NET.Data/PreparedStatement/DuckDBTypeMap.cs b/DuckDB.NET.Data/PreparedStatement/DuckDBTypeMap.cs index e6b8df4b..fcba0224 100644 --- a/DuckDB.NET.Data/PreparedStatement/DuckDBTypeMap.cs +++ b/DuckDB.NET.Data/PreparedStatement/DuckDBTypeMap.cs @@ -28,6 +28,7 @@ internal static class DuckDBTypeMap {typeof(BigInteger), DbType.VarNumeric}, {typeof(byte[]), DbType.Binary}, {typeof(DateTime), DbType.DateTime}, + {typeof(DuckDBTimestamp), DbType.DateTime}, {typeof(DateTimeOffset), DbType.DateTimeOffset}, {typeof(DuckDBDateOnly), DbType.Date}, {typeof(DuckDBTimeOnly), DbType.Time}, @@ -53,4 +54,4 @@ public static DbType GetDbTypeForValue(object? value) return DbType.Object; } -} \ No newline at end of file +} From 412f61e4be1d9afc38b4132e44fdfae2c40590c4 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 1 Dec 2025 19:47:54 -0700 Subject: [PATCH 05/15] Update infinity date/timestamp reading --- .../Reader/DateTimeVectorDataReader.cs | 54 ++-- .../DuckDBDataReaderInfinityTests.cs | 275 ++++++++++++------ 2 files changed, 213 insertions(+), 116 deletions(-) diff --git a/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs b/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs index 0876e308..de0eabf6 100644 --- a/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs +++ b/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs @@ -28,21 +28,19 @@ protected override T GetValidValue(ulong offset, Type targetType) { if (DuckDBType == DuckDBType.Date) { - var (dateOnly, isFinite, isPositiveInfinity) = GetDateOnly(offset); + var (dateOnly, isFinite) = GetDateOnly(offset); if (!isFinite) { if (targetType == DateTimeType || targetType == DateTimeNullableType) { - var dateTime = isPositiveInfinity ? DateTime.MaxValue : DateTime.MinValue; - return (T)(object)dateTime; + ThrowInfinityDateException(); } #if NET6_0_OR_GREATER if (targetType == DateOnlyType || targetType == DateOnlyNullableType) { - var dateOnlyValue = isPositiveInfinity ? DateOnly.MaxValue : DateOnly.MinValue; - return (T)(object)dateOnlyValue; + ThrowInfinityDateException(); } #endif return (T)(object)dateOnly; @@ -114,20 +112,14 @@ private T ReadTimestamp(ulong offset, Type targetType) { if (targetType == DateTimeType || targetType == DateTimeNullableType) { - var dateTime = timestampStruct.IsPositiveInfinity() ? DateTime.MaxValue : DateTime.MinValue; - return (T)(object)dateTime; + ThrowInfinityTimestampException(); } if (targetType == DateTimeOffsetType || targetType == DateTimeOffsetNullableType) { - var dateTime = timestampStruct.IsPositiveInfinity() ? DateTime.MaxValue : DateTime.MinValue; - var dateTimeOffset = new DateTimeOffset(dateTime, TimeSpan.Zero); - return (T)(object)dateTimeOffset; + ThrowInfinityTimestampException(); } - // As of 1.4.2, duckdb_from_timestamp throws a ConversionException - // for infinity, so infinity values are only successfully returned - // for DateTime and DateOnly types. var infinityTimestamp = DuckDBTimestamp.FromDuckDBTimestampStruct(timestampStruct); return (T)(object)infinityTimestamp; } @@ -176,36 +168,34 @@ private DuckDBTimeOnly GetTimeOnly(ulong offset) return NativeMethods.DateTimeHelpers.DuckDBFromTime(GetFieldData(offset)); } - private (DuckDBDateOnly dateOnly, bool IsFinite, bool IsPositiveInfinity) GetDateOnly(ulong offset) + private (DuckDBDateOnly dateOnly, bool IsFinite) GetDateOnly(ulong offset) { var date = GetFieldData(offset); var isFinite = NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(date); - var isPositiveInfinity = date.Days == int.MaxValue; - return (DuckDBDateOnly.FromDuckDBDate(date), isFinite, isPositiveInfinity); + return (DuckDBDateOnly.FromDuckDBDate(date), isFinite); } private object GetDate(ulong offset, Type targetType) { - var (dateOnly, isFinite, isPositiveInfinity) = GetDateOnly(offset); + var (dateOnly, isFinite) = GetDateOnly(offset); if (!isFinite) { if (targetType == DateTimeType) { - return isPositiveInfinity ? DateTime.MaxValue : DateTime.MinValue; + ThrowInfinityDateException(); } #if NET6_0_OR_GREATER if (targetType == DateOnlyType) { - return isPositiveInfinity ? DateOnly.MaxValue : DateOnly.MinValue; + ThrowInfinityDateException(); } #endif return dateOnly; } - if (targetType == DateTimeType) { return (DateTime)dateOnly; @@ -247,19 +237,15 @@ private object GetDateTime(ulong offset, Type targetType) { if (targetType == typeof(DateTime)) { - return timestampStruct.IsPositiveInfinity() ? DateTime.MaxValue : DateTime.MinValue; + ThrowInfinityTimestampException(); } if (targetType == DateTimeOffsetType) { - var dateTime = timestampStruct.IsPositiveInfinity() ? DateTime.MaxValue : DateTime.MinValue; - return new DateTimeOffset(dateTime, TimeSpan.Zero); + ThrowInfinityTimestampException(); } - // As of 1.4.2, duckdb_from_timestamp throws a ConversionException - // for infinity, so infinity values are only successfully returned - // for DateTime and DateOnly types. - return timestampStruct.ToDuckDBTimestamp(DuckDBType); + return DuckDBTimestamp.FromDuckDBTimestampStruct(timestampStruct); } var (timestamp, additionalTicks) = timestampStruct.ToDuckDBTimestamp(DuckDBType); @@ -291,4 +277,18 @@ private object GetDateTimeOffset(ulong offset, Type targetType) return timeTz; } + + private static void ThrowInfinityDateException() + { + throw new InvalidOperationException( + "Cannot convert infinite date value to DateTime or DateOnly. " + + "Use DuckDBDateOnly to read this value and check IsInfinity, IsPositiveInfinity, or IsNegativeInfinity before converting to .NET types."); + } + + private static void ThrowInfinityTimestampException() + { + throw new InvalidOperationException( + "Cannot convert infinite timestamp value to DateTime or DateTimeOffset. " + + "Use DuckDBTimestamp to read this value and check IsInfinity, IsPositiveInfinity, or IsNegativeInfinity before converting to .NET types."); + } } \ No newline at end of file diff --git a/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs b/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs index 7ed5122f..ab001eb7 100644 --- a/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs +++ b/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs @@ -8,6 +8,8 @@ namespace DuckDB.NET.Test; public class DuckDBDataReaderInfinityTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) { + #region Native IsFinite Tests + [Fact] public void duckdb_date_is_finite() { @@ -73,257 +75,352 @@ public void duckdb_timestamp_ns_is_finite() NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(negativeInfinity).Should().BeFalse(); } + #endregion + + #region Reading Infinity Dates as DuckDBDateOnly (Success Cases) + [Fact] - public void ReadInfinityDate() + public void ReadInfinityDateAsDuckDBDateOnly() { Command.CommandText = "SELECT 'infinity'::DATE"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MaxValue); - reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + var dateOnly = reader.GetFieldValue(0); + dateOnly.Should().Be(DuckDBDateOnly.PositiveInfinity); + dateOnly.IsPositiveInfinity.Should().BeTrue(); + dateOnly.IsInfinity.Should().BeTrue(); } [Fact] - public void ReadNegativeInfinityDate() + public void ReadNegativeInfinityDateAsDuckDBDateOnly() { Command.CommandText = "SELECT '-infinity'::DATE"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MinValue); - reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + var dateOnly = reader.GetFieldValue(0); + dateOnly.Should().Be(DuckDBDateOnly.NegativeInfinity); + dateOnly.IsNegativeInfinity.Should().BeTrue(); + dateOnly.IsInfinity.Should().BeTrue(); } -#if NET6_0_OR_GREATER + #endregion + + #region Reading Infinity Dates as DateTime/DateOnly (Exception Cases) + [Fact] - public void ReadInfinityDateAsDateOnly() + public void ReadInfinityDateAsDateTime_ThrowsException() { Command.CommandText = "SELECT 'infinity'::DATE"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(DateOnly.MaxValue); + var act = () => reader.GetDateTime(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBDateOnly*"); } [Fact] - public void ReadNegativeInfinityDateAsDateOnly() + public void ReadNegativeInfinityDateAsDateTime_ThrowsException() { Command.CommandText = "SELECT '-infinity'::DATE"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(DateOnly.MinValue); + var act = () => reader.GetDateTime(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBDateOnly*"); + } + + [Fact] + public void ReadInfinityDateAsNullableDateTime_ThrowsException() + { + Command.CommandText = "SELECT 'infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + var act = () => reader.GetFieldValue(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBDateOnly*"); + } + +#if NET6_0_OR_GREATER + [Fact] + public void ReadInfinityDateAsDateOnly_ThrowsException() + { + Command.CommandText = "SELECT 'infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + var act = () => reader.GetFieldValue(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBDateOnly*"); } -#endif [Fact] - public void ReadInfinityTimestamp() + public void ReadNegativeInfinityDateAsDateOnly_ThrowsException() { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP, 'infinity'::TIMESTAMP_NS, 'infinity'::TIMESTAMP_MS, 'infinity'::TIMESTAMP_S"; + Command.CommandText = "SELECT '-infinity'::DATE"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MaxValue); - reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); - reader.GetDateTime(1).Should().Be(DateTime.MaxValue); - reader.GetFieldValue(1).Should().Be(DateTime.MaxValue); - reader.GetDateTime(2).Should().Be(DateTime.MaxValue); - reader.GetFieldValue(2).Should().Be(DateTime.MaxValue); - reader.GetDateTime(3).Should().Be(DateTime.MaxValue); - reader.GetFieldValue(3).Should().Be(DateTime.MaxValue); + var act = () => reader.GetFieldValue(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBDateOnly*"); } [Fact] - public void ReadNegativeInfinityTimestamp() + public void ReadInfinityDateAsNullableDateOnly_ThrowsException() { - Command.CommandText = "SELECT '-infinity'::TIMESTAMP, '-infinity'::TIMESTAMP_NS, '-infinity'::TIMESTAMP_MS, '-infinity'::TIMESTAMP_S"; + Command.CommandText = "SELECT 'infinity'::DATE"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MinValue); - reader.GetFieldValue(0).Should().Be(DateTime.MinValue); - reader.GetDateTime(1).Should().Be(DateTime.MinValue); - reader.GetFieldValue(1).Should().Be(DateTime.MinValue); - reader.GetDateTime(2).Should().Be(DateTime.MinValue); - reader.GetFieldValue(2).Should().Be(DateTime.MinValue); - reader.GetDateTime(3).Should().Be(DateTime.MinValue); - reader.GetFieldValue(3).Should().Be(DateTime.MinValue); + var act = () => reader.GetFieldValue(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBDateOnly*"); } +#endif + + #endregion + + #region Reading Infinity Timestamps as DuckDBTimestamp (Success Cases) [Fact] - public void ReadInfinityTimestampAsDateTimeOffset() + public void ReadInfinityTimestampAsDuckDBTimestamp() { Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MaxValue, TimeSpan.Zero)); + var timestamp = reader.GetFieldValue(0); + timestamp.Should().Be(DuckDBTimestamp.PositiveInfinity); + timestamp.IsPositiveInfinity.Should().BeTrue(); + timestamp.IsInfinity.Should().BeTrue(); } [Fact] - public void ReadNegativeInfinityTimestampAsDateTimeOffset() + public void ReadNegativeInfinityTimestampAsDuckDBTimestamp() { Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero)); + var timestamp = reader.GetFieldValue(0); + timestamp.Should().Be(DuckDBTimestamp.NegativeInfinity); + timestamp.IsNegativeInfinity.Should().BeTrue(); + timestamp.IsInfinity.Should().BeTrue(); } [Fact] - public void ReadInfinityTimestampTz() + public void ReadInfinityTimestampVariantsAsDuckDBTimestamp() { - Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_NS, 'infinity'::TIMESTAMP_MS, 'infinity'::TIMESTAMP_S"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MaxValue); - reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); + reader.GetFieldValue(1).IsPositiveInfinity.Should().BeTrue(); + reader.GetFieldValue(2).IsPositiveInfinity.Should().BeTrue(); } [Fact] - public void ReadNegativeInfinityTimestampTz() + public void ReadNegativeInfinityTimestampVariantsAsDuckDBTimestamp() { - Command.CommandText = "SELECT '-infinity'::TIMESTAMPTZ"; + Command.CommandText = "SELECT '-infinity'::TIMESTAMP_NS, '-infinity'::TIMESTAMP_MS, '-infinity'::TIMESTAMP_S"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MinValue); - reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + reader.GetFieldValue(0).IsNegativeInfinity.Should().BeTrue(); + reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); + reader.GetFieldValue(2).IsNegativeInfinity.Should().BeTrue(); } [Fact] - public void ReadInfinityTimestampTzAsDateTimeOffset() + public void ReadInfinityTimestampTzAsDuckDBTimestamp() { Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MaxValue, TimeSpan.Zero)); + var timestamp = reader.GetFieldValue(0); + timestamp.IsPositiveInfinity.Should().BeTrue(); } [Fact] - public void ReadNegativeInfinityTimestampTzAsDateTimeOffset() + public void ReadNegativeInfinityTimestampTzAsDuckDBTimestamp() { Command.CommandText = "SELECT '-infinity'::TIMESTAMPTZ"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero)); + var timestamp = reader.GetFieldValue(0); + timestamp.IsNegativeInfinity.Should().BeTrue(); } + #endregion + + #region Reading Infinity Timestamps as DateTime/DateTimeOffset (Exception Cases) + [Fact] - public void ReadMixedInfinityDates() + public void ReadInfinityTimestampAsDateTime_ThrowsException() { - Command.CommandText = "SELECT * FROM (VALUES ('infinity'::DATE), ('-infinity'::DATE), ('2024-01-15'::DATE)) AS t(d)"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; using var reader = Command.ExecuteReader(); - - reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MaxValue); - reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MinValue); - reader.Read(); - reader.GetDateTime(0).Should().Be(new DateTime(2024, 1, 15)); + var act = () => reader.GetDateTime(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBTimestamp*"); } [Fact] - public void ReadMixedInfinityTimestamps() + public void ReadNegativeInfinityTimestampAsDateTime_ThrowsException() { - Command.CommandText = "SELECT * FROM (VALUES ('infinity'::TIMESTAMP), ('-infinity'::TIMESTAMP), ('2024-01-15 12:30:45'::TIMESTAMP)) AS t(ts)"; + Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; using var reader = Command.ExecuteReader(); - reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MaxValue); - reader.Read(); - reader.GetDateTime(0).Should().Be(DateTime.MinValue); + var act = () => reader.GetDateTime(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBTimestamp*"); + } + [Fact] + public void ReadInfinityTimestampAsNullableDateTime_ThrowsException() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetDateTime(0).Should().Be(new DateTime(2024, 1, 15, 12, 30, 45)); + + var act = () => reader.GetFieldValue(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBTimestamp*"); } [Fact] - public void ReadNullableInfinityDate() + public void ReadInfinityTimestampAsDateTimeOffset_ThrowsException() { - Command.CommandText = "SELECT 'infinity'::DATE"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + var act = () => reader.GetFieldValue(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBTimestamp*"); } [Fact] - public void ReadNullableNegativeInfinityDate() + public void ReadNegativeInfinityTimestampAsDateTimeOffset_ThrowsException() { - Command.CommandText = "SELECT '-infinity'::DATE"; + Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + var act = () => reader.GetFieldValue(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBTimestamp*"); } [Fact] - public void ReadNullableInfinityTimestamp() + public void ReadInfinityTimestampAsNullableDateTimeOffset_ThrowsException() { Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(DateTime.MaxValue); + var act = () => reader.GetFieldValue(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBTimestamp*"); } [Fact] - public void ReadNullableNegativeInfinityTimestamp() + public void ReadInfinityTimestampTzAsDateTime_ThrowsException() { - Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(DateTime.MinValue); + var act = () => reader.GetDateTime(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBTimestamp*"); } -#if NET6_0_OR_GREATER [Fact] - public void ReadNullableInfinityDateAsDateOnly() + public void ReadInfinityTimestampTzAsDateTimeOffset_ThrowsException() { - Command.CommandText = "SELECT 'infinity'::DATE"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(DateOnly.MaxValue); + var act = () => reader.GetFieldValue(0); + act.Should().Throw() + .WithMessage("*infinite*DuckDBTimestamp*"); } [Fact] - public void ReadNullableNegativeInfinityDateAsDateOnly() + public void ReadInfinityTimestampVariantsAsDateTime_ThrowsException() { - Command.CommandText = "SELECT '-infinity'::DATE"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_NS, 'infinity'::TIMESTAMP_MS, 'infinity'::TIMESTAMP_S"; using var reader = Command.ExecuteReader(); reader.Read(); - reader.GetFieldValue(0).Should().Be(DateOnly.MinValue); + var act0 = () => reader.GetDateTime(0); + act0.Should().Throw(); + + var act1 = () => reader.GetDateTime(1); + act1.Should().Throw(); + + var act2 = () => reader.GetDateTime(2); + act2.Should().Throw(); } -#endif + + #endregion + + #region Mixed Infinity and Finite Values [Fact] - public void ReadNullableInfinityTimestampAsDateTimeOffset() + public void ReadMixedInfinityDatesAsDuckDBDateOnly() { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; + Command.CommandText = "SELECT * FROM (VALUES ('infinity'::DATE), ('-infinity'::DATE), ('2024-01-15'::DATE)) AS t(d)"; using var reader = Command.ExecuteReader(); + reader.Read(); + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); - reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MaxValue, TimeSpan.Zero)); + reader.Read(); + var negativeInfinity = reader.GetFieldValue(0); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + + reader.Read(); + var finiteDate = reader.GetFieldValue(0); + finiteDate.IsInfinity.Should().BeFalse(); + finiteDate.Year.Should().Be(2024); + finiteDate.Month.Should().Be(1); + finiteDate.Day.Should().Be(15); } [Fact] - public void ReadNullableNegativeInfinityTimestampAsDateTimeOffset() + public void ReadMixedInfinityTimestampsAsDuckDBTimestamp() { - Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; + Command.CommandText = "SELECT * FROM (VALUES ('infinity'::TIMESTAMP), ('-infinity'::TIMESTAMP), ('2024-01-15 12:30:45'::TIMESTAMP)) AS t(ts)"; using var reader = Command.ExecuteReader(); + + reader.Read(); + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + reader.Read(); + var negativeInfinity = reader.GetFieldValue(0); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); - reader.GetFieldValue(0).Should().Be(new DateTimeOffset(DateTime.MinValue, TimeSpan.Zero)); + reader.Read(); + var finiteTimestamp = reader.GetFieldValue(0); + finiteTimestamp.IsInfinity.Should().BeFalse(); + finiteTimestamp.Date.Year.Should().Be(2024); + finiteTimestamp.Date.Month.Should().Be(1); + finiteTimestamp.Date.Day.Should().Be(15); } } From 496d7e5d24577d86bc408f66c2c35d466b148b97 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 1 Dec 2025 19:57:18 -0700 Subject: [PATCH 06/15] Organize tests --- .../DuckDBDataReaderInfinityTests.cs | 426 ------------------ DuckDB.NET.Test/DuckDBInfinityTests.cs | 331 ++++++++++++++ 2 files changed, 331 insertions(+), 426 deletions(-) delete mode 100644 DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs create mode 100644 DuckDB.NET.Test/DuckDBInfinityTests.cs diff --git a/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs b/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs deleted file mode 100644 index ab001eb7..00000000 --- a/DuckDB.NET.Test/DuckDBDataReaderInfinityTests.cs +++ /dev/null @@ -1,426 +0,0 @@ -using DuckDB.NET.Data; -using DuckDB.NET.Native; -using FluentAssertions; -using System; -using Xunit; - -namespace DuckDB.NET.Test; - -public class DuckDBDataReaderInfinityTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) -{ - #region Native IsFinite Tests - - [Fact] - public void duckdb_date_is_finite() - { - var positiveInfinity = new DuckDBDate { Days = Int32.MaxValue }; - var negativeInfinity = new DuckDBDate { Days = -Int32.MaxValue }; // NOTE: -MaxValue, not MinValue - var finitePositive = new DuckDBDate { Days = Int32.MaxValue - 1 }; - var finiteNegative = new DuckDBDate { Days = -Int32.MaxValue + 1 }; - NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(finitePositive).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(positiveInfinity).Should().BeFalse(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(finiteNegative).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(negativeInfinity).Should().BeFalse(); - } - - [Fact] - public void duckdb_timestamp_is_finite() - { - var positiveInfinity = new DuckDBTimestampStruct { Micros = Int64.MaxValue }; - var negativeInfinity = new DuckDBTimestampStruct { Micros = -Int64.MaxValue }; - var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; - var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(finitePositive).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(positiveInfinity).Should().BeFalse(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(finiteNegative).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(negativeInfinity).Should().BeFalse(); - } - - [Fact] - public void duckdb_timestamp_s_is_finite() - { - var positiveInfinity = new DuckDBTimestampStruct { Micros = Int64.MaxValue }; - var negativeInfinity = new DuckDBTimestampStruct { Micros = -Int64.MaxValue }; - var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; - var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(finitePositive).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(positiveInfinity).Should().BeFalse(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(finiteNegative).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(negativeInfinity).Should().BeFalse(); - } - - [Fact] - public void duckdb_timestamp_ms_is_finite() - { - var positiveInfinity = new DuckDBTimestampStruct { Micros = Int64.MaxValue }; - var negativeInfinity = new DuckDBTimestampStruct { Micros = -Int64.MaxValue }; - var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; - var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(finitePositive).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(positiveInfinity).Should().BeFalse(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(finiteNegative).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(negativeInfinity).Should().BeFalse(); - } - - [Fact] - public void duckdb_timestamp_ns_is_finite() - { - var positiveInfinity = new DuckDBTimestampStruct { Micros = Int64.MaxValue }; - var negativeInfinity = new DuckDBTimestampStruct { Micros = -Int64.MaxValue }; - var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; - var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(finitePositive).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(positiveInfinity).Should().BeFalse(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(finiteNegative).Should().BeTrue(); - NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(negativeInfinity).Should().BeFalse(); - } - - #endregion - - #region Reading Infinity Dates as DuckDBDateOnly (Success Cases) - - [Fact] - public void ReadInfinityDateAsDuckDBDateOnly() - { - Command.CommandText = "SELECT 'infinity'::DATE"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var dateOnly = reader.GetFieldValue(0); - dateOnly.Should().Be(DuckDBDateOnly.PositiveInfinity); - dateOnly.IsPositiveInfinity.Should().BeTrue(); - dateOnly.IsInfinity.Should().BeTrue(); - } - - [Fact] - public void ReadNegativeInfinityDateAsDuckDBDateOnly() - { - Command.CommandText = "SELECT '-infinity'::DATE"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var dateOnly = reader.GetFieldValue(0); - dateOnly.Should().Be(DuckDBDateOnly.NegativeInfinity); - dateOnly.IsNegativeInfinity.Should().BeTrue(); - dateOnly.IsInfinity.Should().BeTrue(); - } - - #endregion - - #region Reading Infinity Dates as DateTime/DateOnly (Exception Cases) - - [Fact] - public void ReadInfinityDateAsDateTime_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::DATE"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetDateTime(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBDateOnly*"); - } - - [Fact] - public void ReadNegativeInfinityDateAsDateTime_ThrowsException() - { - Command.CommandText = "SELECT '-infinity'::DATE"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetDateTime(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBDateOnly*"); - } - - [Fact] - public void ReadInfinityDateAsNullableDateTime_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::DATE"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetFieldValue(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBDateOnly*"); - } - -#if NET6_0_OR_GREATER - [Fact] - public void ReadInfinityDateAsDateOnly_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::DATE"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetFieldValue(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBDateOnly*"); - } - - [Fact] - public void ReadNegativeInfinityDateAsDateOnly_ThrowsException() - { - Command.CommandText = "SELECT '-infinity'::DATE"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetFieldValue(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBDateOnly*"); - } - - [Fact] - public void ReadInfinityDateAsNullableDateOnly_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::DATE"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetFieldValue(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBDateOnly*"); - } -#endif - - #endregion - - #region Reading Infinity Timestamps as DuckDBTimestamp (Success Cases) - - [Fact] - public void ReadInfinityTimestampAsDuckDBTimestamp() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var timestamp = reader.GetFieldValue(0); - timestamp.Should().Be(DuckDBTimestamp.PositiveInfinity); - timestamp.IsPositiveInfinity.Should().BeTrue(); - timestamp.IsInfinity.Should().BeTrue(); - } - - [Fact] - public void ReadNegativeInfinityTimestampAsDuckDBTimestamp() - { - Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var timestamp = reader.GetFieldValue(0); - timestamp.Should().Be(DuckDBTimestamp.NegativeInfinity); - timestamp.IsNegativeInfinity.Should().BeTrue(); - timestamp.IsInfinity.Should().BeTrue(); - } - - [Fact] - public void ReadInfinityTimestampVariantsAsDuckDBTimestamp() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP_NS, 'infinity'::TIMESTAMP_MS, 'infinity'::TIMESTAMP_S"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); - reader.GetFieldValue(1).IsPositiveInfinity.Should().BeTrue(); - reader.GetFieldValue(2).IsPositiveInfinity.Should().BeTrue(); - } - - [Fact] - public void ReadNegativeInfinityTimestampVariantsAsDuckDBTimestamp() - { - Command.CommandText = "SELECT '-infinity'::TIMESTAMP_NS, '-infinity'::TIMESTAMP_MS, '-infinity'::TIMESTAMP_S"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - reader.GetFieldValue(0).IsNegativeInfinity.Should().BeTrue(); - reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); - reader.GetFieldValue(2).IsNegativeInfinity.Should().BeTrue(); - } - - [Fact] - public void ReadInfinityTimestampTzAsDuckDBTimestamp() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var timestamp = reader.GetFieldValue(0); - timestamp.IsPositiveInfinity.Should().BeTrue(); - } - - [Fact] - public void ReadNegativeInfinityTimestampTzAsDuckDBTimestamp() - { - Command.CommandText = "SELECT '-infinity'::TIMESTAMPTZ"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var timestamp = reader.GetFieldValue(0); - timestamp.IsNegativeInfinity.Should().BeTrue(); - } - - #endregion - - #region Reading Infinity Timestamps as DateTime/DateTimeOffset (Exception Cases) - - [Fact] - public void ReadInfinityTimestampAsDateTime_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetDateTime(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBTimestamp*"); - } - - [Fact] - public void ReadNegativeInfinityTimestampAsDateTime_ThrowsException() - { - Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetDateTime(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBTimestamp*"); - } - - [Fact] - public void ReadInfinityTimestampAsNullableDateTime_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetFieldValue(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBTimestamp*"); - } - - [Fact] - public void ReadInfinityTimestampAsDateTimeOffset_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetFieldValue(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBTimestamp*"); - } - - [Fact] - public void ReadNegativeInfinityTimestampAsDateTimeOffset_ThrowsException() - { - Command.CommandText = "SELECT '-infinity'::TIMESTAMP"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetFieldValue(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBTimestamp*"); - } - - [Fact] - public void ReadInfinityTimestampAsNullableDateTimeOffset_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetFieldValue(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBTimestamp*"); - } - - [Fact] - public void ReadInfinityTimestampTzAsDateTime_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetDateTime(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBTimestamp*"); - } - - [Fact] - public void ReadInfinityTimestampTzAsDateTimeOffset_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act = () => reader.GetFieldValue(0); - act.Should().Throw() - .WithMessage("*infinite*DuckDBTimestamp*"); - } - - [Fact] - public void ReadInfinityTimestampVariantsAsDateTime_ThrowsException() - { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP_NS, 'infinity'::TIMESTAMP_MS, 'infinity'::TIMESTAMP_S"; - using var reader = Command.ExecuteReader(); - reader.Read(); - - var act0 = () => reader.GetDateTime(0); - act0.Should().Throw(); - - var act1 = () => reader.GetDateTime(1); - act1.Should().Throw(); - - var act2 = () => reader.GetDateTime(2); - act2.Should().Throw(); - } - - #endregion - - #region Mixed Infinity and Finite Values - - [Fact] - public void ReadMixedInfinityDatesAsDuckDBDateOnly() - { - Command.CommandText = "SELECT * FROM (VALUES ('infinity'::DATE), ('-infinity'::DATE), ('2024-01-15'::DATE)) AS t(d)"; - using var reader = Command.ExecuteReader(); - - reader.Read(); - var positiveInfinity = reader.GetFieldValue(0); - positiveInfinity.IsPositiveInfinity.Should().BeTrue(); - - reader.Read(); - var negativeInfinity = reader.GetFieldValue(0); - negativeInfinity.IsNegativeInfinity.Should().BeTrue(); - - reader.Read(); - var finiteDate = reader.GetFieldValue(0); - finiteDate.IsInfinity.Should().BeFalse(); - finiteDate.Year.Should().Be(2024); - finiteDate.Month.Should().Be(1); - finiteDate.Day.Should().Be(15); - } - - [Fact] - public void ReadMixedInfinityTimestampsAsDuckDBTimestamp() - { - Command.CommandText = "SELECT * FROM (VALUES ('infinity'::TIMESTAMP), ('-infinity'::TIMESTAMP), ('2024-01-15 12:30:45'::TIMESTAMP)) AS t(ts)"; - using var reader = Command.ExecuteReader(); - - reader.Read(); - var positiveInfinity = reader.GetFieldValue(0); - positiveInfinity.IsPositiveInfinity.Should().BeTrue(); - - reader.Read(); - var negativeInfinity = reader.GetFieldValue(0); - negativeInfinity.IsNegativeInfinity.Should().BeTrue(); - - reader.Read(); - var finiteTimestamp = reader.GetFieldValue(0); - finiteTimestamp.IsInfinity.Should().BeFalse(); - finiteTimestamp.Date.Year.Should().Be(2024); - finiteTimestamp.Date.Month.Should().Be(1); - finiteTimestamp.Date.Day.Should().Be(15); - } -} diff --git a/DuckDB.NET.Test/DuckDBInfinityTests.cs b/DuckDB.NET.Test/DuckDBInfinityTests.cs new file mode 100644 index 00000000..0f06d44e --- /dev/null +++ b/DuckDB.NET.Test/DuckDBInfinityTests.cs @@ -0,0 +1,331 @@ +using DuckDB.NET.Data; +using DuckDB.NET.Native; +using FluentAssertions; +using System; +using Xunit; + +namespace DuckDB.NET.Test; + +public class DuckDBInfinityTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) +{ + #region Native IsFinite Tests + + [Fact] + public void duckdb_date_is_finite() + { + var positiveInfinity = DuckDBDate.PositiveInfinity; + var negativeInfinity = DuckDBDate.NegativeInfinity; + var finitePositive = new DuckDBDate { Days = Int32.MaxValue - 1 }; + var finiteNegative = new DuckDBDate { Days = -Int32.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(negativeInfinity).Should().BeFalse(); + } + + [Fact] + public void duckdb_timestamp_is_finite() + { + var positiveInfinity = DuckDBTimestampStruct.PositiveInfinity; + var negativeInfinity = DuckDBTimestampStruct.NegativeInfinity; + var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; + var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(negativeInfinity).Should().BeFalse(); + } + + [Fact] + public void duckdb_timestamp_s_is_finite() + { + var positiveInfinity = DuckDBTimestampStruct.PositiveInfinity; + var negativeInfinity = DuckDBTimestampStruct.NegativeInfinity; + var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; + var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(negativeInfinity).Should().BeFalse(); + } + + [Fact] + public void duckdb_timestamp_ms_is_finite() + { + var positiveInfinity = DuckDBTimestampStruct.PositiveInfinity; + var negativeInfinity = DuckDBTimestampStruct.NegativeInfinity; + var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; + var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(negativeInfinity).Should().BeFalse(); + } + + [Fact] + public void duckdb_timestamp_ns_is_finite() + { + var positiveInfinity = DuckDBTimestampStruct.PositiveInfinity; + var negativeInfinity = DuckDBTimestampStruct.NegativeInfinity; + var finitePositive = new DuckDBTimestampStruct { Micros = Int64.MaxValue - 1 }; + var finiteNegative = new DuckDBTimestampStruct { Micros = -Int64.MaxValue + 1 }; + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(finitePositive).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(positiveInfinity).Should().BeFalse(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(finiteNegative).Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(negativeInfinity).Should().BeFalse(); + } + + #endregion + + #region Infinity Date Tests + + [Fact] + public void ReadInfinityDate() + { + Command.CommandText = "SELECT 'infinity'::DATE, '-infinity'::DATE"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + // Positive infinity + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.Should().Be(DuckDBDateOnly.PositiveInfinity); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + positiveInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(0).Should().Be(DuckDBDateOnly.PositiveInfinity); + + // Negative infinity + var negativeInfinity = reader.GetFieldValue(1); + negativeInfinity.Should().Be(DuckDBDateOnly.NegativeInfinity); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + negativeInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(1).Should().Be(DuckDBDateOnly.NegativeInfinity); + + // Reading as DateTime throws + var actPositive = () => reader.GetDateTime(0); + actPositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + var actNegative = () => reader.GetDateTime(1); + actNegative.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + + // Reading as nullable DateTime throws + var actNullablePositive = () => reader.GetFieldValue(0); + actNullablePositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + +#if NET6_0_OR_GREATER + // Reading as DateOnly throws + var actDateOnlyPositive = () => reader.GetFieldValue(0); + actDateOnlyPositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + var actDateOnlyNegative = () => reader.GetFieldValue(1); + actDateOnlyNegative.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + + // Reading as nullable DateOnly throws + var actNullableDateOnly = () => reader.GetFieldValue(0); + actNullableDateOnly.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); +#endif + } + + #endregion + + #region Infinity Timestamp Tests + + [Fact] + public void ReadInfinityTimestamp() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP, '-infinity'::TIMESTAMP"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + // Positive infinity + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.Should().Be(DuckDBTimestamp.PositiveInfinity); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + positiveInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(0).Should().Be(DuckDBTimestamp.PositiveInfinity); + + // Negative infinity + var negativeInfinity = reader.GetFieldValue(1); + negativeInfinity.Should().Be(DuckDBTimestamp.NegativeInfinity); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + negativeInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(1).Should().Be(DuckDBTimestamp.NegativeInfinity); + + // Reading as DateTime throws + var actPositive = () => reader.GetDateTime(0); + actPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + var actNegative = () => reader.GetDateTime(1); + actNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + + // Reading as nullable DateTime throws + var actNullable = () => reader.GetFieldValue(0); + actNullable.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + + // Reading as DateTimeOffset throws + var actOffsetPositive = () => reader.GetFieldValue(0); + actOffsetPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + var actOffsetNegative = () => reader.GetFieldValue(1); + actOffsetNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + + // Reading as nullable DateTimeOffset throws + var actNullableOffset = () => reader.GetFieldValue(0); + actNullableOffset.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + } + + [Fact] + public void ReadInfinityTimestampNs() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_NS, '-infinity'::TIMESTAMP_NS"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + // Positive infinity + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + positiveInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); + + // Negative infinity + var negativeInfinity = reader.GetFieldValue(1); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + negativeInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); + + // Reading as DateTime throws + var actPositive = () => reader.GetDateTime(0); + actPositive.Should().Throw(); + var actNegative = () => reader.GetDateTime(1); + actNegative.Should().Throw(); + } + + [Fact] + public void ReadInfinityTimestampMs() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_MS, '-infinity'::TIMESTAMP_MS"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + // Positive infinity + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + positiveInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); + + // Negative infinity + var negativeInfinity = reader.GetFieldValue(1); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + negativeInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); + + // Reading as DateTime throws + var actPositive = () => reader.GetDateTime(0); + actPositive.Should().Throw(); + var actNegative = () => reader.GetDateTime(1); + actNegative.Should().Throw(); + } + + [Fact] + public void ReadInfinityTimestampS() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_S, '-infinity'::TIMESTAMP_S"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + // Positive infinity + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + positiveInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); + + // Negative infinity + var negativeInfinity = reader.GetFieldValue(1); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + negativeInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); + + // Reading as DateTime throws + var actPositive = () => reader.GetDateTime(0); + actPositive.Should().Throw(); + var actNegative = () => reader.GetDateTime(1); + actNegative.Should().Throw(); + } + + [Fact] + public void ReadInfinityTimestampTz() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ, '-infinity'::TIMESTAMPTZ"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + // Positive infinity + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + positiveInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); + + // Negative infinity + var negativeInfinity = reader.GetFieldValue(1); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + negativeInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); + + // Reading as DateTime throws + var actPositive = () => reader.GetDateTime(0); + actPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + var actNegative = () => reader.GetDateTime(1); + actNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + + // Reading as DateTimeOffset throws + var actOffsetPositive = () => reader.GetFieldValue(0); + actOffsetPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + var actOffsetNegative = () => reader.GetFieldValue(1); + actOffsetNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + } + + #endregion + + #region Mixed Infinity and Finite Values + + [Fact] + public void ReadMixedInfinityDatesAsDuckDBDateOnly() + { + Command.CommandText = "SELECT * FROM (VALUES ('infinity'::DATE), ('-infinity'::DATE), ('2024-01-15'::DATE)) AS t(d)"; + using var reader = Command.ExecuteReader(); + + reader.Read(); + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + + reader.Read(); + var negativeInfinity = reader.GetFieldValue(0); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + + reader.Read(); + var finiteDate = reader.GetFieldValue(0); + finiteDate.IsInfinity.Should().BeFalse(); + finiteDate.Year.Should().Be(2024); + finiteDate.Month.Should().Be(1); + finiteDate.Day.Should().Be(15); + } + + [Fact] + public void ReadMixedInfinityTimestampsAsDuckDBTimestamp() + { + Command.CommandText = "SELECT * FROM (VALUES ('infinity'::TIMESTAMP), ('-infinity'::TIMESTAMP), ('2024-01-15 12:30:45'::TIMESTAMP)) AS t(ts)"; + using var reader = Command.ExecuteReader(); + + reader.Read(); + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + + reader.Read(); + var negativeInfinity = reader.GetFieldValue(0); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + + reader.Read(); + var finiteTimestamp = reader.GetFieldValue(0); + finiteTimestamp.IsInfinity.Should().BeFalse(); + finiteTimestamp.Date.Year.Should().Be(2024); + finiteTimestamp.Date.Month.Should().Be(1); + finiteTimestamp.Date.Day.Should().Be(15); + } + + #endregion +} From b01a9f6f4d23c886c8d280e3cc962a6857c929d5 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 1 Dec 2025 20:54:26 -0700 Subject: [PATCH 07/15] cleanup --- DuckDB.NET.Data/Extensions/DateTimeExtensions.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs b/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs index 57c7a61e..84d0c397 100644 --- a/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs +++ b/DuckDB.NET.Data/Extensions/DateTimeExtensions.cs @@ -82,6 +82,8 @@ public static (DuckDBTimestamp result, int additionalTicks) ToDuckDBTimestamp(th return (result, additionalTicks); } + /// Uses the native method corresponding to the timestamp type, as opposed + /// to comparing with a constant directly. public static bool IsFinite(this DuckDBTimestampStruct timestamp, DuckDBType duckDBType) { return duckDBType switch @@ -92,9 +94,4 @@ public static bool IsFinite(this DuckDBTimestampStruct timestamp, DuckDBType duc _ => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(timestamp) }; } - - public static bool IsPositiveInfinity(this DuckDBTimestampStruct timestamp) - { - return timestamp.Micros == long.MaxValue; - } -} \ No newline at end of file +} From 11bfc1ca8d97fd0ac0aef08e9c5dc652a9e4f3ae Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 1 Dec 2025 20:54:43 -0700 Subject: [PATCH 08/15] Test native is_finite calls and infinities as parameters --- DuckDB.NET.Test/DuckDBInfinityTests.cs | 288 ++++++++++++++----------- 1 file changed, 158 insertions(+), 130 deletions(-) diff --git a/DuckDB.NET.Test/DuckDBInfinityTests.cs b/DuckDB.NET.Test/DuckDBInfinityTests.cs index 0f06d44e..aec47c21 100644 --- a/DuckDB.NET.Test/DuckDBInfinityTests.cs +++ b/DuckDB.NET.Test/DuckDBInfinityTests.cs @@ -8,6 +8,98 @@ namespace DuckDB.NET.Test; public class DuckDBInfinityTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) { + #region Assertion Helpers + + private static void AssertInfinityDateValues(DuckDBDataReader reader) + { + // Positive infinity + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.Should().Be(DuckDBDateOnly.PositiveInfinity); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + positiveInfinity.IsInfinity.Should().BeTrue(); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(positiveInfinity.ToDuckDBDate()).Should().BeFalse(); + + // Negative infinity + var negativeInfinity = reader.GetFieldValue(1); + negativeInfinity.Should().Be(DuckDBDateOnly.NegativeInfinity); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + negativeInfinity.IsInfinity.Should().BeTrue(); + reader.GetFieldValue(1).Should().Be(DuckDBDateOnly.NegativeInfinity); + NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(negativeInfinity.ToDuckDBDate()).Should().BeFalse(); + + // Reading as DateTime throws + var actPositive = () => reader.GetDateTime(0); + actPositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + var actNegative = () => reader.GetDateTime(1); + actNegative.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + + // Reading as nullable DateTime throws + var actNullablePositive = () => reader.GetFieldValue(0); + actNullablePositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + +#if NET6_0_OR_GREATER + // Reading as DateOnly throws + var actDateOnlyPositive = () => reader.GetFieldValue(0); + actDateOnlyPositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + var actDateOnlyNegative = () => reader.GetFieldValue(1); + actDateOnlyNegative.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + + // Reading as nullable DateOnly throws + var actNullableDateOnly = () => reader.GetFieldValue(0); + actNullableDateOnly.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); +#endif + } + + private static void AssertInfinityTimestampValues(DuckDBDataReader reader, DuckDBType duckDBType) + { + bool IsFinite(DuckDBTimestampStruct timestamp) + { + return duckDBType switch + { + DuckDBType.TimestampNs => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(timestamp), + DuckDBType.TimestampMs => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(timestamp), + DuckDBType.TimestampS => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(timestamp), + _ => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(timestamp) + }; + } + + // Positive infinity + var positiveInfinity = reader.GetFieldValue(0); + positiveInfinity.Should().Be(DuckDBTimestamp.PositiveInfinity); + positiveInfinity.IsPositiveInfinity.Should().BeTrue(); + positiveInfinity.IsInfinity.Should().BeTrue(); + IsFinite(positiveInfinity.ToDuckDBTimestampStruct()).Should().BeFalse(); + + // Negative infinity + var negativeInfinity = reader.GetFieldValue(1); + negativeInfinity.Should().Be(DuckDBTimestamp.NegativeInfinity); + negativeInfinity.IsNegativeInfinity.Should().BeTrue(); + negativeInfinity.IsInfinity.Should().BeTrue(); + IsFinite(negativeInfinity.ToDuckDBTimestampStruct()).Should().BeFalse(); + + // Reading as DateTime throws + var actPositive = () => reader.GetDateTime(0); + actPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + var actNegative = () => reader.GetDateTime(1); + actNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + + // Reading as nullable DateTime throws + var actNullable = () => reader.GetFieldValue(0); + actNullable.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + + // Reading as DateTimeOffset throws + var actOffsetPositive = () => reader.GetFieldValue(0); + actOffsetPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + var actOffsetNegative = () => reader.GetFieldValue(1); + actOffsetNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + + // Reading as nullable DateTimeOffset throws + var actNullableOffset = () => reader.GetFieldValue(0); + actNullableOffset.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + } + + #endregion + #region Native IsFinite Tests [Fact] @@ -86,41 +178,19 @@ public void ReadInfinityDate() using var reader = Command.ExecuteReader(); reader.Read(); - // Positive infinity - var positiveInfinity = reader.GetFieldValue(0); - positiveInfinity.Should().Be(DuckDBDateOnly.PositiveInfinity); - positiveInfinity.IsPositiveInfinity.Should().BeTrue(); - positiveInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(0).Should().Be(DuckDBDateOnly.PositiveInfinity); - - // Negative infinity - var negativeInfinity = reader.GetFieldValue(1); - negativeInfinity.Should().Be(DuckDBDateOnly.NegativeInfinity); - negativeInfinity.IsNegativeInfinity.Should().BeTrue(); - negativeInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(1).Should().Be(DuckDBDateOnly.NegativeInfinity); - - // Reading as DateTime throws - var actPositive = () => reader.GetDateTime(0); - actPositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); - var actNegative = () => reader.GetDateTime(1); - actNegative.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); - - // Reading as nullable DateTime throws - var actNullablePositive = () => reader.GetFieldValue(0); - actNullablePositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + AssertInfinityDateValues(reader); + } -#if NET6_0_OR_GREATER - // Reading as DateOnly throws - var actDateOnlyPositive = () => reader.GetFieldValue(0); - actDateOnlyPositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); - var actDateOnlyNegative = () => reader.GetFieldValue(1); - actDateOnlyNegative.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); + [Fact] + public void ReadInfinityDateWithParameters() + { + Command.CommandText = "SELECT $1::DATE, $2::DATE"; + Command.Parameters.Add(new DuckDBParameter(DuckDBDateOnly.PositiveInfinity)); + Command.Parameters.Add(new DuckDBParameter(DuckDBDateOnly.NegativeInfinity)); + using var reader = Command.ExecuteReader(); + reader.Read(); - // Reading as nullable DateOnly throws - var actNullableDateOnly = () => reader.GetFieldValue(0); - actNullableDateOnly.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); -#endif + AssertInfinityDateValues(reader); } #endregion @@ -134,39 +204,19 @@ public void ReadInfinityTimestamp() using var reader = Command.ExecuteReader(); reader.Read(); - // Positive infinity - var positiveInfinity = reader.GetFieldValue(0); - positiveInfinity.Should().Be(DuckDBTimestamp.PositiveInfinity); - positiveInfinity.IsPositiveInfinity.Should().BeTrue(); - positiveInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(0).Should().Be(DuckDBTimestamp.PositiveInfinity); - - // Negative infinity - var negativeInfinity = reader.GetFieldValue(1); - negativeInfinity.Should().Be(DuckDBTimestamp.NegativeInfinity); - negativeInfinity.IsNegativeInfinity.Should().BeTrue(); - negativeInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(1).Should().Be(DuckDBTimestamp.NegativeInfinity); - - // Reading as DateTime throws - var actPositive = () => reader.GetDateTime(0); - actPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); - var actNegative = () => reader.GetDateTime(1); - actNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); - - // Reading as nullable DateTime throws - var actNullable = () => reader.GetFieldValue(0); - actNullable.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + AssertInfinityTimestampValues(reader, DuckDBType.Timestamp); + } - // Reading as DateTimeOffset throws - var actOffsetPositive = () => reader.GetFieldValue(0); - actOffsetPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); - var actOffsetNegative = () => reader.GetFieldValue(1); - actOffsetNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + [Fact] + public void ReadInfinityTimestampWithParameters() + { + Command.CommandText = "SELECT $1::TIMESTAMP, $2::TIMESTAMP"; + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); + using var reader = Command.ExecuteReader(); + reader.Read(); - // Reading as nullable DateTimeOffset throws - var actNullableOffset = () => reader.GetFieldValue(0); - actNullableOffset.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + AssertInfinityTimestampValues(reader, DuckDBType.Timestamp); } [Fact] @@ -176,23 +226,19 @@ public void ReadInfinityTimestampNs() using var reader = Command.ExecuteReader(); reader.Read(); - // Positive infinity - var positiveInfinity = reader.GetFieldValue(0); - positiveInfinity.IsPositiveInfinity.Should().BeTrue(); - positiveInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); + AssertInfinityTimestampValues(reader, DuckDBType.TimestampNs); + } - // Negative infinity - var negativeInfinity = reader.GetFieldValue(1); - negativeInfinity.IsNegativeInfinity.Should().BeTrue(); - negativeInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); + [Fact] + public void ReadInfinityTimestampNsWithParameters() + { + Command.CommandText = "SELECT $1::TIMESTAMP_NS, $2::TIMESTAMP_NS"; + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); + using var reader = Command.ExecuteReader(); + reader.Read(); - // Reading as DateTime throws - var actPositive = () => reader.GetDateTime(0); - actPositive.Should().Throw(); - var actNegative = () => reader.GetDateTime(1); - actNegative.Should().Throw(); + AssertInfinityTimestampValues(reader, DuckDBType.TimestampNs); } [Fact] @@ -202,23 +248,19 @@ public void ReadInfinityTimestampMs() using var reader = Command.ExecuteReader(); reader.Read(); - // Positive infinity - var positiveInfinity = reader.GetFieldValue(0); - positiveInfinity.IsPositiveInfinity.Should().BeTrue(); - positiveInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); + AssertInfinityTimestampValues(reader, DuckDBType.TimestampMs); + } - // Negative infinity - var negativeInfinity = reader.GetFieldValue(1); - negativeInfinity.IsNegativeInfinity.Should().BeTrue(); - negativeInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); + [Fact] + public void ReadInfinityTimestampMsWithParameters() + { + Command.CommandText = "SELECT $1::TIMESTAMP_MS, $2::TIMESTAMP_MS"; + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); + using var reader = Command.ExecuteReader(); + reader.Read(); - // Reading as DateTime throws - var actPositive = () => reader.GetDateTime(0); - actPositive.Should().Throw(); - var actNegative = () => reader.GetDateTime(1); - actNegative.Should().Throw(); + AssertInfinityTimestampValues(reader, DuckDBType.TimestampMs); } [Fact] @@ -228,23 +270,19 @@ public void ReadInfinityTimestampS() using var reader = Command.ExecuteReader(); reader.Read(); - // Positive infinity - var positiveInfinity = reader.GetFieldValue(0); - positiveInfinity.IsPositiveInfinity.Should().BeTrue(); - positiveInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); + AssertInfinityTimestampValues(reader, DuckDBType.TimestampS); + } - // Negative infinity - var negativeInfinity = reader.GetFieldValue(1); - negativeInfinity.IsNegativeInfinity.Should().BeTrue(); - negativeInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); + [Fact] + public void ReadInfinityTimestampSWithParameters() + { + Command.CommandText = "SELECT $1::TIMESTAMP_S, $2::TIMESTAMP_S"; + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); + using var reader = Command.ExecuteReader(); + reader.Read(); - // Reading as DateTime throws - var actPositive = () => reader.GetDateTime(0); - actPositive.Should().Throw(); - var actNegative = () => reader.GetDateTime(1); - actNegative.Should().Throw(); + AssertInfinityTimestampValues(reader, DuckDBType.TimestampS); } [Fact] @@ -254,29 +292,19 @@ public void ReadInfinityTimestampTz() using var reader = Command.ExecuteReader(); reader.Read(); - // Positive infinity - var positiveInfinity = reader.GetFieldValue(0); - positiveInfinity.IsPositiveInfinity.Should().BeTrue(); - positiveInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); - - // Negative infinity - var negativeInfinity = reader.GetFieldValue(1); - negativeInfinity.IsNegativeInfinity.Should().BeTrue(); - negativeInfinity.IsInfinity.Should().BeTrue(); - reader.GetFieldValue(1).IsNegativeInfinity.Should().BeTrue(); + AssertInfinityTimestampValues(reader, DuckDBType.TimestampTz); + } - // Reading as DateTime throws - var actPositive = () => reader.GetDateTime(0); - actPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); - var actNegative = () => reader.GetDateTime(1); - actNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + [Fact] + public void ReadInfinityTimestampTzWithParameters() + { + Command.CommandText = "SELECT $1::TIMESTAMPTZ, $2::TIMESTAMPTZ"; + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); + Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); + using var reader = Command.ExecuteReader(); + reader.Read(); - // Reading as DateTimeOffset throws - var actOffsetPositive = () => reader.GetFieldValue(0); - actOffsetPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); - var actOffsetNegative = () => reader.GetFieldValue(1); - actOffsetNegative.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); + AssertInfinityTimestampValues(reader, DuckDBType.TimestampTz); } #endregion From e9e0265672b3811c450b3da8bac247cc8329f31f Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 1 Dec 2025 20:55:14 -0700 Subject: [PATCH 09/15] Fix DuckDBTimestamp casting --- DuckDB.NET.Bindings/DuckDBTimestamp.cs | 3 +++ .../PreparedStatement/ClrToDuckDBConverter.cs | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/DuckDB.NET.Bindings/DuckDBTimestamp.cs b/DuckDB.NET.Bindings/DuckDBTimestamp.cs index 27f022e2..535d43db 100644 --- a/DuckDB.NET.Bindings/DuckDBTimestamp.cs +++ b/DuckDB.NET.Bindings/DuckDBTimestamp.cs @@ -75,4 +75,7 @@ public DuckDBTimestampStruct ToDuckDBTimestampStruct() return NativeMethods.DateTimeHelpers.DuckDBToTimestamp(this); } + + public static implicit operator DateTime(DuckDBTimestamp timestamp) => timestamp.ToDateTime(); + public static implicit operator DuckDBTimestamp(DateTime timestamp) => DuckDBTimestamp.FromDateTime(timestamp); } diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index e5b9b49a..e51cb8ea 100644 --- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs +++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs @@ -54,7 +54,12 @@ internal static class ClrToDuckDBConverter return NativeMethods.Value.DuckDBCreateTime(time); } }, - { DbType.DateTime, value => NativeMethods.Value.DuckDBCreateTimestamp(((DateTime)value).ToTimestampStruct(DuckDBType.Timestamp))}, + { DbType.DateTime, value => + { + var dateTime = (value is DateTime dt ? (DuckDBTimestamp)dt : (DuckDBTimestamp)value).ToDuckDBTimestampStruct(); + return NativeMethods.Value.DuckDBCreateTimestamp(dateTime); + } + }, { DbType.DateTimeOffset, value => NativeMethods.Value.DuckDBCreateTimestampTz(((DateTimeOffset)value).ToTimestampStruct()) }, }; @@ -177,4 +182,4 @@ private static DuckDBValue DecimalToDuckDBValue(decimal value) return NativeMethods.Value.DuckDBCreateDecimal(new DuckDBDecimal((byte)width, scale, new DuckDBHugeInt(result))); } -} \ No newline at end of file +} From c8db8b44fc7aeaae5c0fa0c668362203beb5d5ba Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Tue, 2 Dec 2025 11:16:08 -0700 Subject: [PATCH 10/15] Add explicit DateOnly conversion methods for DuckDBDateOnly --- DuckDB.NET.Bindings/DuckDBDateOnly.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/DuckDB.NET.Bindings/DuckDBDateOnly.cs b/DuckDB.NET.Bindings/DuckDBDateOnly.cs index 98e55bd6..993786b4 100644 --- a/DuckDB.NET.Bindings/DuckDBDateOnly.cs +++ b/DuckDB.NET.Bindings/DuckDBDateOnly.cs @@ -49,6 +49,14 @@ public readonly struct DuckDBDateOnly(int year, byte month, byte day) public DateTime ToDateTime() => new DateTime(Year, Month, Day); +#if NET6_0_OR_GREATER + + public static DuckDBDateOnly FromDateOnly(DateOnly dateOnly) => new DuckDBDateOnly(dateOnly.Year, (byte)dateOnly.Month, (byte)dateOnly.Day); + + public DateOnly ToDateOnly() => new DateOnly(Year, Month, Day); + +#endif + /// /// Converts a DuckDBDate to DuckDBDateOnly, handling infinity values. /// @@ -81,9 +89,9 @@ public DuckDBDate ToDuckDBDate() #if NET6_0_OR_GREATER - public static implicit operator DateOnly(DuckDBDateOnly dateOnly) => new DateOnly(dateOnly.Year, dateOnly.Month, dateOnly.Day); + public static implicit operator DateOnly(DuckDBDateOnly dateOnly) => dateOnly.ToDateOnly(); - public static implicit operator DuckDBDateOnly(DateOnly date) => new DuckDBDateOnly(date.Year, (byte)date.Month, (byte)date.Day); + public static implicit operator DuckDBDateOnly(DateOnly date) => DuckDBDateOnly.FromDateOnly(date); #endif } From 5fc278bcfb46b05bf9329f4a4d64a3982a4f722b Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Tue, 2 Dec 2025 12:09:40 -0700 Subject: [PATCH 11/15] Update infinity tests to check `isinf()` --- DuckDB.NET.Test/DuckDBInfinityTests.cs | 32 ++++++++++++++++---------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/DuckDB.NET.Test/DuckDBInfinityTests.cs b/DuckDB.NET.Test/DuckDBInfinityTests.cs index aec47c21..07983377 100644 --- a/DuckDB.NET.Test/DuckDBInfinityTests.cs +++ b/DuckDB.NET.Test/DuckDBInfinityTests.cs @@ -27,6 +27,10 @@ private static void AssertInfinityDateValues(DuckDBDataReader reader) reader.GetFieldValue(1).Should().Be(DuckDBDateOnly.NegativeInfinity); NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(negativeInfinity.ToDuckDBDate()).Should().BeFalse(); + // isinf() function results + reader.GetBoolean(2).Should().BeTrue("isinf() should return true for positive infinity date"); + reader.GetBoolean(3).Should().BeTrue("isinf() should return true for negative infinity date"); + // Reading as DateTime throws var actPositive = () => reader.GetDateTime(0); actPositive.Should().Throw().WithMessage("*infinite*DuckDBDateOnly*"); @@ -77,6 +81,10 @@ bool IsFinite(DuckDBTimestampStruct timestamp) negativeInfinity.IsInfinity.Should().BeTrue(); IsFinite(negativeInfinity.ToDuckDBTimestampStruct()).Should().BeFalse(); + // isinf() function results + reader.GetBoolean(2).Should().BeTrue("isinf() should return true for positive infinity timestamp"); + reader.GetBoolean(3).Should().BeTrue("isinf() should return true for negative infinity timestamp"); + // Reading as DateTime throws var actPositive = () => reader.GetDateTime(0); actPositive.Should().Throw().WithMessage("*infinite*DuckDBTimestamp*"); @@ -174,7 +182,7 @@ public void duckdb_timestamp_ns_is_finite() [Fact] public void ReadInfinityDate() { - Command.CommandText = "SELECT 'infinity'::DATE, '-infinity'::DATE"; + Command.CommandText = "SELECT 'infinity'::DATE, '-infinity'::DATE, isinf('infinity'::DATE), isinf('-infinity'::DATE)"; using var reader = Command.ExecuteReader(); reader.Read(); @@ -184,7 +192,7 @@ public void ReadInfinityDate() [Fact] public void ReadInfinityDateWithParameters() { - Command.CommandText = "SELECT $1::DATE, $2::DATE"; + Command.CommandText = "SELECT $1::DATE, $2::DATE, isinf($1::DATE), isinf($2::DATE)"; Command.Parameters.Add(new DuckDBParameter(DuckDBDateOnly.PositiveInfinity)); Command.Parameters.Add(new DuckDBParameter(DuckDBDateOnly.NegativeInfinity)); using var reader = Command.ExecuteReader(); @@ -200,7 +208,7 @@ public void ReadInfinityDateWithParameters() [Fact] public void ReadInfinityTimestamp() { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP, '-infinity'::TIMESTAMP"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMP, '-infinity'::TIMESTAMP, isinf('infinity'::TIMESTAMP), isinf('-infinity'::TIMESTAMP)"; using var reader = Command.ExecuteReader(); reader.Read(); @@ -210,7 +218,7 @@ public void ReadInfinityTimestamp() [Fact] public void ReadInfinityTimestampWithParameters() { - Command.CommandText = "SELECT $1::TIMESTAMP, $2::TIMESTAMP"; + Command.CommandText = "SELECT $1::TIMESTAMP, $2::TIMESTAMP, isinf($1::TIMESTAMP), isinf($2::TIMESTAMP)"; Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); using var reader = Command.ExecuteReader(); @@ -222,7 +230,7 @@ public void ReadInfinityTimestampWithParameters() [Fact] public void ReadInfinityTimestampNs() { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP_NS, '-infinity'::TIMESTAMP_NS"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_NS, '-infinity'::TIMESTAMP_NS, isinf('infinity'::TIMESTAMP_NS), isinf('-infinity'::TIMESTAMP_NS)"; using var reader = Command.ExecuteReader(); reader.Read(); @@ -232,7 +240,7 @@ public void ReadInfinityTimestampNs() [Fact] public void ReadInfinityTimestampNsWithParameters() { - Command.CommandText = "SELECT $1::TIMESTAMP_NS, $2::TIMESTAMP_NS"; + Command.CommandText = "SELECT $1::TIMESTAMP_NS, $2::TIMESTAMP_NS, isinf($1::TIMESTAMP_NS), isinf($2::TIMESTAMP_NS)"; Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); using var reader = Command.ExecuteReader(); @@ -244,7 +252,7 @@ public void ReadInfinityTimestampNsWithParameters() [Fact] public void ReadInfinityTimestampMs() { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP_MS, '-infinity'::TIMESTAMP_MS"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_MS, '-infinity'::TIMESTAMP_MS, isinf('infinity'::TIMESTAMP_MS), isinf('-infinity'::TIMESTAMP_MS)"; using var reader = Command.ExecuteReader(); reader.Read(); @@ -254,7 +262,7 @@ public void ReadInfinityTimestampMs() [Fact] public void ReadInfinityTimestampMsWithParameters() { - Command.CommandText = "SELECT $1::TIMESTAMP_MS, $2::TIMESTAMP_MS"; + Command.CommandText = "SELECT $1::TIMESTAMP_MS, $2::TIMESTAMP_MS, isinf($1::TIMESTAMP_MS), isinf($2::TIMESTAMP_MS)"; Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); using var reader = Command.ExecuteReader(); @@ -266,7 +274,7 @@ public void ReadInfinityTimestampMsWithParameters() [Fact] public void ReadInfinityTimestampS() { - Command.CommandText = "SELECT 'infinity'::TIMESTAMP_S, '-infinity'::TIMESTAMP_S"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_S, '-infinity'::TIMESTAMP_S, isinf('infinity'::TIMESTAMP_S), isinf('-infinity'::TIMESTAMP_S)"; using var reader = Command.ExecuteReader(); reader.Read(); @@ -276,7 +284,7 @@ public void ReadInfinityTimestampS() [Fact] public void ReadInfinityTimestampSWithParameters() { - Command.CommandText = "SELECT $1::TIMESTAMP_S, $2::TIMESTAMP_S"; + Command.CommandText = "SELECT $1::TIMESTAMP_S, $2::TIMESTAMP_S, isinf($1::TIMESTAMP_S), isinf($2::TIMESTAMP_S)"; Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); using var reader = Command.ExecuteReader(); @@ -288,7 +296,7 @@ public void ReadInfinityTimestampSWithParameters() [Fact] public void ReadInfinityTimestampTz() { - Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ, '-infinity'::TIMESTAMPTZ"; + Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ, '-infinity'::TIMESTAMPTZ, isinf('infinity'::TIMESTAMPTZ), isinf('-infinity'::TIMESTAMPTZ)"; using var reader = Command.ExecuteReader(); reader.Read(); @@ -298,7 +306,7 @@ public void ReadInfinityTimestampTz() [Fact] public void ReadInfinityTimestampTzWithParameters() { - Command.CommandText = "SELECT $1::TIMESTAMPTZ, $2::TIMESTAMPTZ"; + Command.CommandText = "SELECT $1::TIMESTAMPTZ, $2::TIMESTAMPTZ, isinf($1::TIMESTAMPTZ), isinf($2::TIMESTAMPTZ)"; Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.PositiveInfinity)); Command.Parameters.Add(new DuckDBParameter(DuckDBTimestamp.NegativeInfinity)); using var reader = Command.ExecuteReader(); From 862f31ef87339a8b402b3992f3534b006e17824f Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 8 Dec 2025 12:29:05 -0700 Subject: [PATCH 12/15] Add doc comments for +/-infinity native objects --- DuckDB.NET.Bindings/DuckDBNativeObjects.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/DuckDB.NET.Bindings/DuckDBNativeObjects.cs b/DuckDB.NET.Bindings/DuckDBNativeObjects.cs index 663f957c..42676f57 100644 --- a/DuckDB.NET.Bindings/DuckDBNativeObjects.cs +++ b/DuckDB.NET.Bindings/DuckDBNativeObjects.cs @@ -115,7 +115,15 @@ public void Close() [StructLayout(LayoutKind.Sequential)] public struct DuckDBDate { + /// + /// Represents DuckDB's positive infinity date value. + /// This is the value used in the DuckDB source code for +infinity dates. + /// public static readonly DuckDBDate PositiveInfinity = new() { Days = int.MaxValue }; + /// + /// Represents DuckDB's negative infinity date value. + /// This is the value used in the DuckDB source code for -infinity dates. + /// public static readonly DuckDBDate NegativeInfinity = new() { Days = -int.MaxValue }; public int Days { get; set; } @@ -147,7 +155,15 @@ public struct DuckDBTimeTz [StructLayout(LayoutKind.Sequential)] public struct DuckDBTimestampStruct { + /// + /// Represents DuckDB's positive infinity timestamp value. + /// This is the value used in the DuckDB source code for +infinity timestamps. + /// public static readonly DuckDBTimestampStruct PositiveInfinity = new() { Micros = long.MaxValue }; + /// + /// Represents DuckDB's negative infinity timestamp value. + /// This is the value used in the DuckDB source code for -infinity timestamps. + /// public static readonly DuckDBTimestampStruct NegativeInfinity = new() { Micros = -long.MaxValue }; public long Micros { get; set; } From 5211704bcce15d3e28cf1eb3d4f5ce81b21e50e1 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 8 Dec 2025 12:29:54 -0700 Subject: [PATCH 13/15] Use duckdb native timestamp struct +/-infinity values for constants --- DuckDB.NET.Bindings/DuckDBDateOnly.cs | 2 ++ DuckDB.NET.Bindings/DuckDBTimestamp.cs | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/DuckDB.NET.Bindings/DuckDBDateOnly.cs b/DuckDB.NET.Bindings/DuckDBDateOnly.cs index 993786b4..f5723fe4 100644 --- a/DuckDB.NET.Bindings/DuckDBDateOnly.cs +++ b/DuckDB.NET.Bindings/DuckDBDateOnly.cs @@ -12,6 +12,7 @@ public readonly struct DuckDBDateOnly(int year, byte month, byte day) public static readonly DuckDBDateOnly PositiveInfinity = // This is the value returned by DuckDB for positive infinity dates when // passed to duckdb_from_date, and it is used for backwards compatibility + // 5881580-07-10 new(5881580, 7, 11); /// @@ -20,6 +21,7 @@ public readonly struct DuckDBDateOnly(int year, byte month, byte day) public static readonly DuckDBDateOnly NegativeInfinity = // This is the value returned by DuckDB for negative infinity dates when // passed to duckdb_from_date, and it is used for backwards compatibility. + // 5877642-06-25 (BC) new(-5877641, 6, 24); public int Year { get; } = year; diff --git a/DuckDB.NET.Bindings/DuckDBTimestamp.cs b/DuckDB.NET.Bindings/DuckDBTimestamp.cs index 535d43db..47a4e938 100644 --- a/DuckDB.NET.Bindings/DuckDBTimestamp.cs +++ b/DuckDB.NET.Bindings/DuckDBTimestamp.cs @@ -12,7 +12,8 @@ public readonly struct DuckDBTimestamp(DuckDBDateOnly date, DuckDBTimeOnly time) public static readonly DuckDBTimestamp PositiveInfinity = // The +infinity date value is not representable by the timestamp type, // so this constant should never occur in normal usage - new(DuckDBDateOnly.PositiveInfinity, new DuckDBTimeOnly(0, 0, 0)); + // 294247-01-10 04:00:54 + new(new DuckDBDateOnly(294247, 1, 10), new DuckDBTimeOnly(4, 0, 54)); /// /// Represents negative infinity for DuckDB timestamps. @@ -20,7 +21,8 @@ public readonly struct DuckDBTimestamp(DuckDBDateOnly date, DuckDBTimeOnly time) public static readonly DuckDBTimestamp NegativeInfinity = // The -infinity date value is not representable by the timestamp type, // so this constant should never occur in normal usage - new(DuckDBDateOnly.NegativeInfinity, new DuckDBTimeOnly(0, 0, 0)); + // 290309-12-22 (BC) 00:00:00 + new(new DuckDBDateOnly(-290308, 12, 21), new DuckDBTimeOnly(0, 0, 0)); public DuckDBDateOnly Date { get; } = date; public DuckDBTimeOnly Time { get; } = time; @@ -28,17 +30,17 @@ public readonly struct DuckDBTimestamp(DuckDBDateOnly date, DuckDBTimeOnly time) /// /// Returns true if this timestamp represents positive or negative infinity. /// - public bool IsInfinity => Date.IsInfinity; + public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity; /// /// Returns true if this timestamp represents positive infinity. /// - public bool IsPositiveInfinity => Date.IsPositiveInfinity; + public bool IsPositiveInfinity => Equals(PositiveInfinity); /// /// Returns true if this timestamp represents negative infinity. /// - public bool IsNegativeInfinity => Date.IsNegativeInfinity; + public bool IsNegativeInfinity => Equals(NegativeInfinity); public DateTime ToDateTime() { From 3ce74095c782598c036c4b0fa9cead3ab8f6feed Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 8 Dec 2025 14:16:24 -0700 Subject: [PATCH 14/15] Update test_all_types test for now supported infinity cases --- .../DuckDBDataReaderTestAllTypes.cs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/DuckDB.NET.Test/DuckDBDataReaderTestAllTypes.cs b/DuckDB.NET.Test/DuckDBDataReaderTestAllTypes.cs index c02434ab..3365dc28 100644 --- a/DuckDB.NET.Test/DuckDBDataReaderTestAllTypes.cs +++ b/DuckDB.NET.Test/DuckDBDataReaderTestAllTypes.cs @@ -398,23 +398,37 @@ public void ReadDateList() VerifyDataList("date_array", 36, new List> { new(), new() { new DuckDBDateOnly(1970, 1, 1), - new DuckDBDateOnly(5881580, 7, 11), - new DuckDBDateOnly(-5877641, 6, 24), + DuckDBDateOnly.PositiveInfinity, + DuckDBDateOnly.NegativeInfinity, null, new DuckDBDateOnly(2022,5,12), } }); } - [Fact(Skip = "These dates can't be expressed by DateTime or is unsupported by this library")] + [Fact()] public void ReadTimeStampList() { - + VerifyDataList("timestamp_array", 37, new List> { new(), new() + { + new DuckDBTimestamp(new DuckDBDateOnly(1970,1,1), new DuckDBTimeOnly(0,0,0)), + DuckDBTimestamp.PositiveInfinity, + DuckDBTimestamp.NegativeInfinity, + null, + new DuckDBTimestamp(new DuckDBDateOnly(2022,5,12), new DuckDBTimeOnly(16,23,45)) + } }); } - [Fact(Skip = "These dates can't be expressed by DateTime or is unsupported by this library")] + [Fact()] public void ReadTimeStampTZList() { - + VerifyDataList("timestamptz_array", 38, new List> { new(), new() + { + new DuckDBTimestamp(new DuckDBDateOnly(1970,1,1), new DuckDBTimeOnly(0,0,0)), + DuckDBTimestamp.PositiveInfinity, + DuckDBTimestamp.NegativeInfinity, + null, + new DuckDBTimestamp(new DuckDBDateOnly(2022,5,12), new DuckDBTimeOnly(23,23,45)) + } }); } [Fact] From d2273a88467b16d6420446684dd71bb7b7ff4ba4 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 8 Dec 2025 14:45:38 -0700 Subject: [PATCH 15/15] Use max/min +/- one interval for +/-infinity constants --- DuckDB.NET.Bindings/DuckDBDateOnly.cs | 8 +++++--- DuckDB.NET.Bindings/DuckDBTimestamp.cs | 14 ++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/DuckDB.NET.Bindings/DuckDBDateOnly.cs b/DuckDB.NET.Bindings/DuckDBDateOnly.cs index f5723fe4..9133432f 100644 --- a/DuckDB.NET.Bindings/DuckDBDateOnly.cs +++ b/DuckDB.NET.Bindings/DuckDBDateOnly.cs @@ -11,8 +11,9 @@ public readonly struct DuckDBDateOnly(int year, byte month, byte day) /// public static readonly DuckDBDateOnly PositiveInfinity = // This is the value returned by DuckDB for positive infinity dates when - // passed to duckdb_from_date, and it is used for backwards compatibility - // 5881580-07-10 + // passed to duckdb_from_date, and it is used for backwards compatibility. + // It is theoretically equal to the max date value plus one day: + // '5881580-07-10'::date + 1 new(5881580, 7, 11); /// @@ -21,7 +22,8 @@ public readonly struct DuckDBDateOnly(int year, byte month, byte day) public static readonly DuckDBDateOnly NegativeInfinity = // This is the value returned by DuckDB for negative infinity dates when // passed to duckdb_from_date, and it is used for backwards compatibility. - // 5877642-06-25 (BC) + // It is theoretically equal to the min date value minus one day: + // '5877642-06-25 (BC)'::date - 1 new(-5877641, 6, 24); public int Year { get; } = year; diff --git a/DuckDB.NET.Bindings/DuckDBTimestamp.cs b/DuckDB.NET.Bindings/DuckDBTimestamp.cs index 47a4e938..e2b6e110 100644 --- a/DuckDB.NET.Bindings/DuckDBTimestamp.cs +++ b/DuckDB.NET.Bindings/DuckDBTimestamp.cs @@ -10,19 +10,17 @@ public readonly struct DuckDBTimestamp(DuckDBDateOnly date, DuckDBTimeOnly time) /// Represents positive infinity for DuckDB timestamps. /// public static readonly DuckDBTimestamp PositiveInfinity = - // The +infinity date value is not representable by the timestamp type, - // so this constant should never occur in normal usage - // 294247-01-10 04:00:54 - new(new DuckDBDateOnly(294247, 1, 10), new DuckDBTimeOnly(4, 0, 54)); + // This is the max timestamp value + 1 microsecond (because timestamps are represented as an int64 of microseconds) + // Theoretically: '294247-01-10 04:00:54.775806'::timestamp + INTERVAL '1 microsecond' + new(new DuckDBDateOnly(294247, 1, 10), new DuckDBTimeOnly(4, 0, 54, 775807)); /// /// Represents negative infinity for DuckDB timestamps. /// public static readonly DuckDBTimestamp NegativeInfinity = - // The -infinity date value is not representable by the timestamp type, - // so this constant should never occur in normal usage - // 290309-12-22 (BC) 00:00:00 - new(new DuckDBDateOnly(-290308, 12, 21), new DuckDBTimeOnly(0, 0, 0)); + // This is the min timestamp value - 1 microsecond (because timestamps are represented as an int64 of microseconds) + // Theoretically: '290309-12-22 (BC) 00:00:00.000000'::timestamp - INTERVAL '1 microsecond' + new(new DuckDBDateOnly(-290308, 12, 21), new DuckDBTimeOnly(23, 59, 59, 999999)); public DuckDBDateOnly Date { get; } = date; public DuckDBTimeOnly Time { get; } = time;