diff --git a/DuckDB.NET.Bindings/DuckDBDateOnly.cs b/DuckDB.NET.Bindings/DuckDBDateOnly.cs index db32b46..9133432 100644 --- a/DuckDB.NET.Bindings/DuckDBDateOnly.cs +++ b/DuckDB.NET.Bindings/DuckDBDateOnly.cs @@ -6,6 +6,26 @@ 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. + // It is theoretically equal to the max date value plus one day: + // '5881580-07-10'::date + 1 + 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. + // 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; public byte Month { get; } = month; @@ -14,19 +34,68 @@ 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); +#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. + /// + 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 DateOnly(DuckDBDateOnly dateOnly) => dateOnly.ToDateOnly(); + + public static implicit operator DuckDBDateOnly(DateOnly date) => DuckDBDateOnly.FromDateOnly(date); + #endif -} \ No newline at end of file +} diff --git a/DuckDB.NET.Bindings/DuckDBNativeObjects.cs b/DuckDB.NET.Bindings/DuckDBNativeObjects.cs index d13ceaa..42676f5 100644 --- a/DuckDB.NET.Bindings/DuckDBNativeObjects.cs +++ b/DuckDB.NET.Bindings/DuckDBNativeObjects.cs @@ -115,7 +115,22 @@ 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; } + + public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity; + public bool IsPositiveInfinity => Days == int.MaxValue; + public bool IsNegativeInfinity => Days == -int.MaxValue; } [StructLayout(LayoutKind.Sequential)] @@ -140,7 +155,22 @@ 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; } + + 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 e52b2d2..e2b6e11 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 = + // 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 = + // 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; + /// + /// Returns true if this timestamp represents positive or negative infinity. + /// + public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity; + + /// + /// Returns true if this timestamp represents positive infinity. + /// + public bool IsPositiveInfinity => Equals(PositiveInfinity); + + /// + /// Returns true if this timestamp represents negative infinity. + /// + public bool IsNegativeInfinity => Equals(NegativeInfinity); + public DateTime ToDateTime() { return new DateTime(Date.Year, Date.Month, Date.Day).AddTicks(Time.Ticks); @@ -18,4 +49,33 @@ 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); + } + + public static implicit operator DateTime(DuckDBTimestamp timestamp) => timestamp.ToDateTime(); + public static implicit operator DuckDBTimestamp(DateTime timestamp) => DuckDBTimestamp.FromDateTime(timestamp); +} diff --git a/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs b/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs index 65b664b..399967f 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.Bindings/NativeMethods/NativeMethods.DateTime.cs b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs index f631a9f..da594d0 100644 --- a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs +++ b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs @@ -30,5 +30,27 @@ 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.Data/DataChunk/Reader/DateTimeVectorDataReader.cs b/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs index 37f678f..de0eabf 100644 --- a/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs +++ b/DuckDB.NET.Data/DataChunk/Reader/DateTimeVectorDataReader.cs @@ -28,7 +28,23 @@ protected override T GetValidValue(ulong offset, Type targetType) { if (DuckDBType == DuckDBType.Date) { - var dateOnly = GetDateOnly(offset); + var (dateOnly, isFinite) = GetDateOnly(offset); + + if (!isFinite) + { + if (targetType == DateTimeType || targetType == DateTimeNullableType) + { + ThrowInfinityDateException(); + } + +#if NET6_0_OR_GREATER + if (targetType == DateOnlyType || targetType == DateOnlyNullableType) + { + ThrowInfinityDateException(); + } +#endif + return (T)(object)dateOnly; + } if (targetType == DateTimeType || targetType == DateTimeNullableType) { @@ -81,8 +97,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 +106,25 @@ 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) + { + ThrowInfinityTimestampException(); + } + + if (targetType == DateTimeOffsetType || targetType == DateTimeOffsetNullableType) + { + ThrowInfinityTimestampException(); + } + + var infinityTimestamp = DuckDBTimestamp.FromDuckDBTimestampStruct(timestampStruct); + return (T)(object)infinityTimestamp; + } + + var (timestamp, additionalTicks) = timestampStruct.ToDuckDBTimestamp(DuckDBType); if (targetType == DateTimeType || targetType == DateTimeNullableType) { @@ -115,7 +149,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 +168,34 @@ private DuckDBTimeOnly GetTimeOnly(ulong offset) return NativeMethods.DateTimeHelpers.DuckDBFromTime(GetFieldData(offset)); } - private DuckDBDateOnly GetDateOnly(ulong offset) + private (DuckDBDateOnly dateOnly, bool IsFinite) GetDateOnly(ulong offset) { - return NativeMethods.DateTimeHelpers.DuckDBFromDate(GetFieldData(offset)); + var date = GetFieldData(offset); + var isFinite = NativeMethods.DateTimeHelpers.DuckDBIsFiniteDate(date); + return (DuckDBDateOnly.FromDuckDBDate(date), isFinite); } private object GetDate(ulong offset, Type targetType) { - var dateOnly = GetDateOnly(offset); + var (dateOnly, isFinite) = GetDateOnly(offset); + + if (!isFinite) + { + if (targetType == DateTimeType) + { + ThrowInfinityDateException(); + } + +#if NET6_0_OR_GREATER + if (targetType == DateOnlyType) + { + ThrowInfinityDateException(); + } +#endif + + return dateOnly; + } + if (targetType == DateTimeType) { return (DateTime)dateOnly; @@ -177,7 +231,24 @@ 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)) + { + ThrowInfinityTimestampException(); + } + + if (targetType == DateTimeOffsetType) + { + ThrowInfinityTimestampException(); + } + + return DuckDBTimestamp.FromDuckDBTimestampStruct(timestampStruct); + } + + var (timestamp, additionalTicks) = timestampStruct.ToDuckDBTimestamp(DuckDBType); if (targetType == typeof(DateTime)) { @@ -206,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.Data/DataChunk/Writer/DateTimeVectorDataWriter.cs b/DuckDB.NET.Data/DataChunk/Writer/DateTimeVectorDataWriter.cs index a9e5a0d..1550cd4 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 3c2c20a..84d0c39 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,8 +77,21 @@ 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); } -} \ No newline at end of file + + /// 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 + { + DuckDBType.TimestampNs => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampNs(timestamp), + DuckDBType.TimestampMs => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampMs(timestamp), + DuckDBType.TimestampS => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestampS(timestamp), + _ => NativeMethods.DateTimeHelpers.DuckDBIsFiniteTimestamp(timestamp) + }; + } +} diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index 427719d..e51cb8e 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); } @@ -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()) }, }; @@ -95,12 +100,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()), @@ -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 +} diff --git a/DuckDB.NET.Data/PreparedStatement/DuckDBTypeMap.cs b/DuckDB.NET.Data/PreparedStatement/DuckDBTypeMap.cs index e6b8df4..fcba022 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 +} diff --git a/DuckDB.NET.Test/DuckDBDataReaderTestAllTypes.cs b/DuckDB.NET.Test/DuckDBDataReaderTestAllTypes.cs index c02434a..3365dc2 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] diff --git a/DuckDB.NET.Test/DuckDBInfinityTests.cs b/DuckDB.NET.Test/DuckDBInfinityTests.cs new file mode 100644 index 0000000..0798337 --- /dev/null +++ b/DuckDB.NET.Test/DuckDBInfinityTests.cs @@ -0,0 +1,367 @@ +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 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(); + + // 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*"); + 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(); + + // 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*"); + 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] + 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, isinf('infinity'::DATE), isinf('-infinity'::DATE)"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + AssertInfinityDateValues(reader); + } + + [Fact] + public void ReadInfinityDateWithParameters() + { + 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(); + reader.Read(); + + AssertInfinityDateValues(reader); + } + + #endregion + + #region Infinity Timestamp Tests + + [Fact] + public void ReadInfinityTimestamp() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP, '-infinity'::TIMESTAMP, isinf('infinity'::TIMESTAMP), isinf('-infinity'::TIMESTAMP)"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.Timestamp); + } + + [Fact] + public void ReadInfinityTimestampWithParameters() + { + 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(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.Timestamp); + } + + [Fact] + public void ReadInfinityTimestampNs() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_NS, '-infinity'::TIMESTAMP_NS, isinf('infinity'::TIMESTAMP_NS), isinf('-infinity'::TIMESTAMP_NS)"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.TimestampNs); + } + + [Fact] + public void ReadInfinityTimestampNsWithParameters() + { + 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(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.TimestampNs); + } + + [Fact] + public void ReadInfinityTimestampMs() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_MS, '-infinity'::TIMESTAMP_MS, isinf('infinity'::TIMESTAMP_MS), isinf('-infinity'::TIMESTAMP_MS)"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.TimestampMs); + } + + [Fact] + public void ReadInfinityTimestampMsWithParameters() + { + 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(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.TimestampMs); + } + + [Fact] + public void ReadInfinityTimestampS() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMP_S, '-infinity'::TIMESTAMP_S, isinf('infinity'::TIMESTAMP_S), isinf('-infinity'::TIMESTAMP_S)"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.TimestampS); + } + + [Fact] + public void ReadInfinityTimestampSWithParameters() + { + 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(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.TimestampS); + } + + [Fact] + public void ReadInfinityTimestampTz() + { + Command.CommandText = "SELECT 'infinity'::TIMESTAMPTZ, '-infinity'::TIMESTAMPTZ, isinf('infinity'::TIMESTAMPTZ), isinf('-infinity'::TIMESTAMPTZ)"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.TimestampTz); + } + + [Fact] + public void ReadInfinityTimestampTzWithParameters() + { + 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(); + reader.Read(); + + AssertInfinityTimestampValues(reader, DuckDBType.TimestampTz); + } + + #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 +}