Skip to content

Commit cd0339c

Browse files
committed
Merge branch 'develop' into copilot/fix-ef813b45-625f-45c9-9f30-ea14874fa9e7
2 parents 0425d0e + 368787c commit cd0339c

22 files changed

+896
-101
lines changed

DuckDB.NET.Bindings/Bindings.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
<PropertyGroup>
44
<Description>DuckDB Bindings for C#.</Description>
55
<PackageReleaseNotes>
6+
Added support for handling ± dates and timestamps. Credits to @rynoV for the PR
67

7-
Updated to DuckDB v1.3.2
8+
Updated to DuckDB v1.4.3
89
</PackageReleaseNotes>
910
<RootNamespace>DuckDB.NET.Native</RootNamespace>
1011
<RuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm64;osx</RuntimeIdentifiers>
11-
<DuckDbArtifactRoot Condition=" '$(DuckDbArtifactRoot)' == '' ">https://github.com/duckdb/duckdb/releases/download/v1.4.0</DuckDbArtifactRoot>
12+
<DuckDbArtifactRoot Condition=" '$(DuckDbArtifactRoot)' == '' ">https://github.com/duckdb/duckdb/releases/download/v1.4.3</DuckDbArtifactRoot>
1213
<SignAssembly>True</SignAssembly>
1314
<AssemblyOriginatorKeyFile>..\keyPair.snk</AssemblyOriginatorKeyFile>
1415
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -40,7 +41,7 @@ Updated to DuckDB v1.3.2
4041
</None>
4142
</ItemGroup>
4243
<ItemGroup Condition="'$(CI)' == 'true'">
43-
<PackageReference Include="GitVersion.MsBuild" Version="5.11.1" PrivateAssets="all" />
44+
<PackageReference Include="GitVersion.MsBuild" Version="6.4.0" PrivateAssets="all" />
4445
</ItemGroup>
4546
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
4647
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.0" />
Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
<Project>
2-
<Target Name="Downloadnativenatives" Condition="!Exists('$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native')">
3-
<MakeDir Directories="$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native" />
4-
<DownloadFile
5-
DestinationFolder="$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)"
6-
SourceUrl="$(libUrl)" Retries="3"
7-
DestinationFileName="native.zip"/>
8-
<Unzip DestinationFolder="$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native"
9-
SourceFiles="$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native.zip"/>
10-
<Delete Files="$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native.zip"/>
11-
</Target>
2+
<Target Name="Downloadnativenatives" Condition="!Exists('$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native')">
3+
<PropertyGroup>
4+
<NativeZipPath>$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native.zip</NativeZipPath>
5+
</PropertyGroup>
6+
<MakeDir Directories="$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native" />
7+
<Delete Files="$(NativeZipPath)"
8+
Condition="Exists('$(NativeZipPath)')"
9+
ContinueOnError="true"/>
10+
<DownloadFile
11+
DestinationFolder="$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)"
12+
SourceUrl="$(libUrl)"
13+
Retries="5"
14+
RetryDelayMilliseconds="1000"
15+
DestinationFileName="native.zip"/>
16+
<Unzip DestinationFolder="$(MSBuildProjectDirectory)\obj\runtimes\$(Rid)\native"
17+
SourceFiles="$(NativeZipPath)"/>
18+
<Delete Files="$(NativeZipPath)"/>
19+
</Target>
1220
</Project>

DuckDB.NET.Bindings/DuckDBDateOnly.cs

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@ namespace DuckDB.NET.Native;
66
[StructLayout(LayoutKind.Sequential)]
77
public readonly struct DuckDBDateOnly(int year, byte month, byte day)
88
{
9+
/// <summary>
10+
/// Represents positive infinity for DuckDB dates.
11+
/// </summary>
12+
public static readonly DuckDBDateOnly PositiveInfinity =
13+
// This is the value returned by DuckDB for positive infinity dates when
14+
// passed to duckdb_from_date, and it is used for backwards compatibility.
15+
// It is theoretically equal to the max date value plus one day:
16+
// '5881580-07-10'::date + 1
17+
new(5881580, 7, 11);
18+
19+
/// <summary>
20+
/// Represents negative infinity for DuckDB dates.
21+
/// </summary>
22+
public static readonly DuckDBDateOnly NegativeInfinity =
23+
// This is the value returned by DuckDB for negative infinity dates when
24+
// passed to duckdb_from_date, and it is used for backwards compatibility.
25+
// It is theoretically equal to the min date value minus one day:
26+
// '5877642-06-25 (BC)'::date - 1
27+
new(-5877641, 6, 24);
28+
929
public int Year { get; } = year;
1030

1131
public byte Month { get; } = month;
@@ -14,19 +34,68 @@ public readonly struct DuckDBDateOnly(int year, byte month, byte day)
1434

1535
internal static readonly DuckDBDateOnly MinValue = FromDateTime(DateTime.MinValue);
1636

37+
/// <summary>
38+
/// Returns true if this date represents positive or negative infinity.
39+
/// </summary>
40+
public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity;
41+
42+
/// <summary>
43+
/// Returns true if this date represents positive infinity.
44+
/// </summary>
45+
public bool IsPositiveInfinity => Equals(PositiveInfinity);
46+
47+
/// <summary>
48+
/// Returns true if this date represents negative infinity.
49+
/// </summary>
50+
public bool IsNegativeInfinity => Equals(NegativeInfinity);
51+
1752
public static DuckDBDateOnly FromDateTime(DateTime dateTime) => new DuckDBDateOnly(dateTime.Year, (byte)dateTime.Month, (byte)dateTime.Day);
1853

1954
public DateTime ToDateTime() => new DateTime(Year, Month, Day);
2055

56+
#if NET6_0_OR_GREATER
57+
58+
public static DuckDBDateOnly FromDateOnly(DateOnly dateOnly) => new DuckDBDateOnly(dateOnly.Year, (byte)dateOnly.Month, (byte)dateOnly.Day);
59+
60+
public DateOnly ToDateOnly() => new DateOnly(Year, Month, Day);
61+
62+
#endif
63+
64+
/// <summary>
65+
/// Converts a DuckDBDate to DuckDBDateOnly, handling infinity values.
66+
/// </summary>
67+
public static DuckDBDateOnly FromDuckDBDate(DuckDBDate date)
68+
{
69+
if (date.IsPositiveInfinity)
70+
return PositiveInfinity;
71+
if (date.IsNegativeInfinity)
72+
return NegativeInfinity;
73+
74+
return NativeMethods.DateTimeHelpers.DuckDBFromDate(date);
75+
}
76+
77+
/// <summary>
78+
/// Converts this DuckDBDateOnly to a DuckDBDate, handling infinity values.
79+
/// </summary>
80+
public DuckDBDate ToDuckDBDate()
81+
{
82+
if (IsPositiveInfinity)
83+
return DuckDBDate.PositiveInfinity;
84+
if (IsNegativeInfinity)
85+
return DuckDBDate.NegativeInfinity;
86+
87+
return NativeMethods.DateTimeHelpers.DuckDBToDate(this);
88+
}
89+
2190
public static explicit operator DateTime(DuckDBDateOnly dateOnly) => dateOnly.ToDateTime();
22-
91+
2392
public static explicit operator DuckDBDateOnly(DateTime dateTime) => FromDateTime(dateTime);
24-
93+
2594
#if NET6_0_OR_GREATER
26-
27-
public static implicit operator DateOnly(DuckDBDateOnly dateOnly) => new DateOnly(dateOnly.Year, dateOnly.Month, dateOnly.Day);
28-
29-
public static implicit operator DuckDBDateOnly(DateOnly date) => new DuckDBDateOnly(date.Year, (byte)date.Month, (byte) date.Day);
30-
95+
96+
public static implicit operator DateOnly(DuckDBDateOnly dateOnly) => dateOnly.ToDateOnly();
97+
98+
public static implicit operator DuckDBDateOnly(DateOnly date) => DuckDBDateOnly.FromDateOnly(date);
99+
31100
#endif
32-
}
101+
}

DuckDB.NET.Bindings/DuckDBDecimal.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
namespace DuckDB.NET.Native;
44

55
[StructLayout(LayoutKind.Sequential)]
6-
public readonly struct DuckDBDecimal
6+
public readonly struct DuckDBDecimal(byte width, byte scale, DuckDBHugeInt value)
77
{
8-
public byte Width { get; }
9-
public byte Scale { get; }
8+
public byte Width { get; } = width;
9+
public byte Scale { get; } = scale;
1010

11-
public DuckDBHugeInt Value { get; }
11+
public DuckDBHugeInt Value { get; } = value;
1212
}

DuckDB.NET.Bindings/DuckDBNativeObjects.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,22 @@ public void Close()
115115
[StructLayout(LayoutKind.Sequential)]
116116
public struct DuckDBDate
117117
{
118+
/// <summary>
119+
/// Represents DuckDB's positive infinity date value.
120+
/// This is the value used in the DuckDB source code for +infinity dates.
121+
/// </summary>
122+
public static readonly DuckDBDate PositiveInfinity = new() { Days = int.MaxValue };
123+
/// <summary>
124+
/// Represents DuckDB's negative infinity date value.
125+
/// This is the value used in the DuckDB source code for -infinity dates.
126+
/// </summary>
127+
public static readonly DuckDBDate NegativeInfinity = new() { Days = -int.MaxValue };
128+
118129
public int Days { get; set; }
130+
131+
public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity;
132+
public bool IsPositiveInfinity => Days == int.MaxValue;
133+
public bool IsNegativeInfinity => Days == -int.MaxValue;
119134
}
120135

121136
[StructLayout(LayoutKind.Sequential)]
@@ -140,7 +155,22 @@ public struct DuckDBTimeTz
140155
[StructLayout(LayoutKind.Sequential)]
141156
public struct DuckDBTimestampStruct
142157
{
158+
/// <summary>
159+
/// Represents DuckDB's positive infinity timestamp value.
160+
/// This is the value used in the DuckDB source code for +infinity timestamps.
161+
/// </summary>
162+
public static readonly DuckDBTimestampStruct PositiveInfinity = new() { Micros = long.MaxValue };
163+
/// <summary>
164+
/// Represents DuckDB's negative infinity timestamp value.
165+
/// This is the value used in the DuckDB source code for -infinity timestamps.
166+
/// </summary>
167+
public static readonly DuckDBTimestampStruct NegativeInfinity = new() { Micros = -long.MaxValue };
168+
143169
public long Micros { get; set; }
170+
171+
public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity;
172+
public bool IsPositiveInfinity => Micros == long.MaxValue;
173+
public bool IsNegativeInfinity => Micros == -long.MaxValue;
144174
}
145175

146176
[StructLayout(LayoutKind.Sequential)]

DuckDB.NET.Bindings/DuckDBTimestamp.cs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,40 @@ namespace DuckDB.NET.Native;
66
[StructLayout(LayoutKind.Sequential)]
77
public readonly struct DuckDBTimestamp(DuckDBDateOnly date, DuckDBTimeOnly time)
88
{
9+
/// <summary>
10+
/// Represents positive infinity for DuckDB timestamps.
11+
/// </summary>
12+
public static readonly DuckDBTimestamp PositiveInfinity =
13+
// This is the max timestamp value + 1 microsecond (because timestamps are represented as an int64 of microseconds)
14+
// Theoretically: '294247-01-10 04:00:54.775806'::timestamp + INTERVAL '1 microsecond'
15+
new(new DuckDBDateOnly(294247, 1, 10), new DuckDBTimeOnly(4, 0, 54, 775807));
16+
17+
/// <summary>
18+
/// Represents negative infinity for DuckDB timestamps.
19+
/// </summary>
20+
public static readonly DuckDBTimestamp NegativeInfinity =
21+
// This is the min timestamp value - 1 microsecond (because timestamps are represented as an int64 of microseconds)
22+
// Theoretically: '290309-12-22 (BC) 00:00:00.000000'::timestamp - INTERVAL '1 microsecond'
23+
new(new DuckDBDateOnly(-290308, 12, 21), new DuckDBTimeOnly(23, 59, 59, 999999));
24+
925
public DuckDBDateOnly Date { get; } = date;
1026
public DuckDBTimeOnly Time { get; } = time;
1127

28+
/// <summary>
29+
/// Returns true if this timestamp represents positive or negative infinity.
30+
/// </summary>
31+
public bool IsInfinity => IsPositiveInfinity || IsNegativeInfinity;
32+
33+
/// <summary>
34+
/// Returns true if this timestamp represents positive infinity.
35+
/// </summary>
36+
public bool IsPositiveInfinity => Equals(PositiveInfinity);
37+
38+
/// <summary>
39+
/// Returns true if this timestamp represents negative infinity.
40+
/// </summary>
41+
public bool IsNegativeInfinity => Equals(NegativeInfinity);
42+
1243
public DateTime ToDateTime()
1344
{
1445
return new DateTime(Date.Year, Date.Month, Date.Day).AddTicks(Time.Ticks);
@@ -18,4 +49,33 @@ public static DuckDBTimestamp FromDateTime(DateTime dateTime)
1849
{
1950
return new DuckDBTimestamp(DuckDBDateOnly.FromDateTime(dateTime), DuckDBTimeOnly.FromDateTime(dateTime));
2051
}
21-
}
52+
53+
/// <summary>
54+
/// Converts a DuckDBTimestampStruct to DuckDBTimestamp, handling infinity values.
55+
/// </summary>
56+
public static DuckDBTimestamp FromDuckDBTimestampStruct(DuckDBTimestampStruct timestampStruct)
57+
{
58+
if (timestampStruct.IsPositiveInfinity)
59+
return PositiveInfinity;
60+
if (timestampStruct.IsNegativeInfinity)
61+
return NegativeInfinity;
62+
63+
return NativeMethods.DateTimeHelpers.DuckDBFromTimestamp(timestampStruct);
64+
}
65+
66+
/// <summary>
67+
/// Converts this DuckDBTimestamp to a DuckDBTimestampStruct, handling infinity values.
68+
/// </summary>
69+
public DuckDBTimestampStruct ToDuckDBTimestampStruct()
70+
{
71+
if (IsPositiveInfinity)
72+
return DuckDBTimestampStruct.PositiveInfinity;
73+
if (IsNegativeInfinity)
74+
return DuckDBTimestampStruct.NegativeInfinity;
75+
76+
return NativeMethods.DateTimeHelpers.DuckDBToTimestamp(this);
77+
}
78+
79+
public static implicit operator DateTime(DuckDBTimestamp timestamp) => timestamp.ToDateTime();
80+
public static implicit operator DuckDBTimestamp(DateTime timestamp) => DuckDBTimestamp.FromDateTime(timestamp);
81+
}

DuckDB.NET.Bindings/DuckDBWrapperObjects.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ protected override bool ReleaseHandle()
101101
{
102102
value.Dispose();
103103
}
104-
104+
105105
NativeMethods.Value.DuckDBDestroyValue(ref handle);
106106
return true;
107107
}
@@ -140,26 +140,26 @@ public T GetValue<T>()
140140

141141
DuckDBType.Float => Cast(NativeMethods.Value.DuckDBGetFloat(this)),
142142
DuckDBType.Double => Cast(NativeMethods.Value.DuckDBGetDouble(this)),
143-
143+
144144
DuckDBType.Decimal => Cast(decimal.Parse(NativeMethods.Value.DuckDBGetVarchar(this), NumberStyles.Any, CultureInfo.InvariantCulture)),
145-
145+
146146
DuckDBType.Uuid => Cast(new Guid(NativeMethods.Value.DuckDBGetVarchar(this))),
147-
147+
148148
DuckDBType.HugeInt => Cast(NativeMethods.Value.DuckDBGetHugeInt(this).ToBigInteger()),
149149
DuckDBType.UnsignedHugeInt => Cast(NativeMethods.Value.DuckDBGetUHugeInt(this).ToBigInteger()),
150-
150+
151151
DuckDBType.Varchar => Cast(NativeMethods.Value.DuckDBGetVarchar(this)),
152152

153153
#if NET6_0_OR_GREATER
154-
DuckDBType.Date => Cast((DateOnly)NativeMethods.DateTimeHelpers.DuckDBFromDate(NativeMethods.Value.DuckDBGetDate(this))),
154+
DuckDBType.Date => Cast((DateOnly)DuckDBDateOnly.FromDuckDBDate(NativeMethods.Value.DuckDBGetDate(this))),
155155
DuckDBType.Time => Cast((TimeOnly)NativeMethods.DateTimeHelpers.DuckDBFromTime(NativeMethods.Value.DuckDBGetTime(this))),
156156
#else
157-
DuckDBType.Date => Cast(NativeMethods.DateTimeHelpers.DuckDBFromDate(NativeMethods.Value.DuckDBGetDate(this)).ToDateTime()),
157+
DuckDBType.Date => Cast(DuckDBDateOnly.FromDuckDBDate(NativeMethods.Value.DuckDBGetDate(this)).ToDateTime()),
158158
DuckDBType.Time => Cast(NativeMethods.DateTimeHelpers.DuckDBFromTime(NativeMethods.Value.DuckDBGetTime(this)).ToDateTime()),
159159
#endif
160160
//DuckDBType.TimeTz => expr,
161161
DuckDBType.Interval => Cast((TimeSpan)NativeMethods.Value.DuckDBGetInterval(this)),
162-
DuckDBType.Timestamp => Cast(NativeMethods.DateTimeHelpers.DuckDBFromTimestamp(NativeMethods.Value.DuckDBGetTimestamp(this)).ToDateTime()),
162+
DuckDBType.Timestamp => Cast(DuckDBTimestamp.FromDuckDBTimestampStruct(NativeMethods.Value.DuckDBGetTimestamp(this)).ToDateTime()),
163163
//DuckDBType.TimestampS => expr,
164164
//DuckDBType.TimestampMs => expr,
165165
//DuckDBType.TimestampNs => expr,

DuckDB.NET.Bindings/NativeMethods/NativeMethods.DateTime.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,27 @@ public static class DateTimeHelpers
3030

3131
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_to_timestamp")]
3232
public static extern DuckDBTimestampStruct DuckDBToTimestamp(DuckDBTimestamp dateStruct);
33+
34+
// NOTE: for boolean return values, MarshalAs(UnmanagedType.I1) is used because the default is to use 4-byte Win32 BOOLs
35+
// https://learn.microsoft.com/en-us/dotnet/standard/native-interop/customize-struct-marshalling#customizing-boolean-field-marshalling
36+
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_date")]
37+
[return: MarshalAs(UnmanagedType.I1)]
38+
public static extern bool DuckDBIsFiniteDate(DuckDBDate date);
39+
40+
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp")]
41+
[return: MarshalAs(UnmanagedType.I1)]
42+
public static extern bool DuckDBIsFiniteTimestamp(DuckDBTimestampStruct ts);
43+
44+
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp_s")]
45+
[return: MarshalAs(UnmanagedType.I1)]
46+
public static extern bool DuckDBIsFiniteTimestampS(DuckDBTimestampStruct ts);
47+
48+
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp_ms")]
49+
[return: MarshalAs(UnmanagedType.I1)]
50+
public static extern bool DuckDBIsFiniteTimestampMs(DuckDBTimestampStruct ts);
51+
52+
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_is_finite_timestamp_ns")]
53+
[return: MarshalAs(UnmanagedType.I1)]
54+
public static extern bool DuckDBIsFiniteTimestampNs(DuckDBTimestampStruct ts);
3355
}
34-
}
56+
}

DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ public static class Value
4747
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_uhugeint")]
4848
public static extern DuckDBValue DuckDBCreateUHugeInt(DuckDBUHugeInt value);
4949

50+
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_decimal")]
51+
public static extern DuckDBValue DuckDBCreateDecimal(DuckDBDecimal value);
52+
5053
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_float")]
5154
public static extern DuckDBValue DuckDBCreateFloat(float value);
5255

0 commit comments

Comments
 (0)