Skip to content

Commit 50332c7

Browse files
committed
Merge branch 'Bind-Complex-Types' of https://github.com/Giorgi/DuckDB.NET into Bind-Complex-Types
2 parents 9f25a34 + a496c4f commit 50332c7

File tree

5 files changed

+204
-18
lines changed

5 files changed

+204
-18
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ public static class Value
5959
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_time")]
6060
public static extern DuckDBValue DuckDBCreateTime(DuckDBTime value);
6161

62-
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_time_tz")]
63-
public static extern DuckDBValue DuckDBCreateTimeTz(DuckDBTimeTz value);
62+
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_time_tz_value")]
63+
public static extern DuckDBValue DuckDBCreateTimeTz(DuckDBTimeTzStruct value);
6464

6565
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_timestamp")]
6666
public static extern DuckDBValue DuckDBCreateTimestamp(DuckDBTimestampStruct value);

DuckDB.NET.Data/Internal/ClrToDuckDBConverter.cs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Globalization;
45
using System.Numerics;
@@ -27,19 +28,26 @@ public static DuckDBValue ToDuckDBValue(this object value) =>
2728
Guid val => GuidToDuckDBValue(val),
2829
BigInteger val => NativeMethods.Value.DuckDBCreateHugeInt(new DuckDBHugeInt(val)),
2930
byte[] val => NativeMethods.Value.DuckDBCreateBlob(val, val.Length),
31+
TimeSpan val => NativeMethods.Value.DuckDBCreateInterval(val),
3032
DateTime val => NativeMethods.Value.DuckDBCreateTimestamp(NativeMethods.DateTimeHelpers.DuckDBToTimestamp(DuckDBTimestamp.FromDateTime(val))),
33+
DateTimeOffset val => DateTimeOffsetToDuckDBValue(val),
3134
DuckDBDateOnly val => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate(val)),
32-
#if NET6_0_OR_GREATER
33-
DateOnly val => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate(val)),
34-
#endif
3535
DuckDBTimeOnly val => NativeMethods.Value.DuckDBCreateTime(NativeMethods.DateTimeHelpers.DuckDBToTime(val)),
3636
#if NET6_0_OR_GREATER
37+
DateOnly val => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate(val)),
3738
TimeOnly val => NativeMethods.Value.DuckDBCreateTime(NativeMethods.DateTimeHelpers.DuckDBToTime(val)),
3839
#endif
39-
ICollection<int> val => CreateListValue(DuckDBType.Integer, val),
40+
ICollection val => CreateCollectionValue(val),
4041
_ => throw new InvalidCastException($"Cannot convert value of type {value.GetType().FullName} to DuckDBValue.")
4142
};
4243

44+
private static DuckDBValue DateTimeOffsetToDuckDBValue(DateTimeOffset val)
45+
{
46+
var duckDBToTime = NativeMethods.DateTimeHelpers.DuckDBToTime((DuckDBTimeOnly)val.DateTime);
47+
var duckDBCreateTimeTz = NativeMethods.DateTimeHelpers.DuckDBCreateTimeTz(duckDBToTime.Micros, (int)val.Offset.TotalSeconds);
48+
return NativeMethods.Value.DuckDBCreateTimeTz(duckDBCreateTimeTz);
49+
}
50+
4351
private static DuckDBValue GuidToDuckDBValue(Guid value)
4452
{
4553
using var handle = value.ToString().ToUnmanagedString();
@@ -58,20 +66,53 @@ private static DuckDBValue StringToDuckDBValue(string value)
5866
return NativeMethods.Value.DuckDBCreateVarchar(handle);
5967
}
6068

61-
private static DuckDBValue CreateListValue<T>(DuckDBType duckDBType, ICollection<T> collection)
69+
private static DuckDBValue CreateCollectionValue(ICollection collection)
70+
{
71+
return collection switch
72+
{
73+
ICollection<bool> items => CreateCollectionValue(DuckDBType.Boolean, items),
74+
75+
ICollection<sbyte> items => CreateCollectionValue(DuckDBType.TinyInt, items),
76+
ICollection<byte> items => CreateCollectionValue(DuckDBType.UnsignedTinyInt, items),
77+
ICollection<short> items => CreateCollectionValue(DuckDBType.SmallInt, items),
78+
ICollection<ushort> items => CreateCollectionValue(DuckDBType.UnsignedSmallInt, items),
79+
ICollection<int> items => CreateCollectionValue(DuckDBType.Integer, items),
80+
ICollection<uint> items => CreateCollectionValue(DuckDBType.UnsignedInteger, items),
81+
ICollection<long> items => CreateCollectionValue(DuckDBType.BigInt, items),
82+
ICollection<ulong> items => CreateCollectionValue(DuckDBType.UnsignedBigInt, items),
83+
ICollection<float> items => CreateCollectionValue(DuckDBType.Float, items),
84+
ICollection<double> items => CreateCollectionValue(DuckDBType.Double, items),
85+
ICollection<BigInteger> items => CreateCollectionValue(DuckDBType.HugeInt, items),
86+
ICollection<decimal> items => CreateCollectionValue(DuckDBType.Varchar, items),
87+
ICollection<Guid> items => CreateCollectionValue(DuckDBType.Varchar, items),
88+
ICollection<string> items => CreateCollectionValue(DuckDBType.Varchar, items),
89+
ICollection<DateTime> items => CreateCollectionValue(DuckDBType.Date, items),
90+
ICollection<DateTimeOffset> items => CreateCollectionValue(DuckDBType.TimeTz, items),
91+
ICollection<TimeSpan> items => CreateCollectionValue(DuckDBType.Interval, items),
92+
ICollection<object> items => CreateCollectionValue(DuckDBType.List, items),
93+
_ => throw new InvalidOperationException($"Cannot convert collection type {collection.GetType().FullName} to DuckDBValue.")
94+
};
95+
}
96+
97+
private static DuckDBValue CreateCollectionValue<T>(DuckDBType duckDBType, ICollection<T> collection)
6298
{
63-
using var logicalType = NativeMethods.LogicalType.DuckDBCreateLogicalType(duckDBType);
99+
using var listItemType = NativeMethods.LogicalType.DuckDBCreateLogicalType(duckDBType);
64100

65101
var values = new DuckDBValue[collection.Count];
66102

67103
var index = 0;
68104
foreach (var item in collection)
69105
{
106+
if (item == null)
107+
{
108+
throw new InvalidOperationException($"Cannot convert null to DuckDBValue.");
109+
}
110+
70111
var duckDBValue = item.ToDuckDBValue();
71112
values[index] = duckDBValue;
72113
index++;
73114
}
74115

75-
return NativeMethods.Value.DuckDBCreateListValue(logicalType, values, collection.Count);
116+
return NativeMethods.Value.DuckDBCreateListValue(listItemType, values, collection.Count);
76117
}
77118
}

DuckDB.NET.Test/Parameters/IntegerParametersTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ private void TestSimple<TValue>(string duckDbType, TValue? expectedValue, Func<D
4141
var scalar = Command.ExecuteScalar();
4242
scalar.Should().Be(expectedValue);
4343

44-
var reader = Command.ExecuteReader();
44+
using var reader = Command.ExecuteReader();
4545
reader.Read();
4646
var value = getValue(reader);
4747

Lines changed: 142 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,157 @@
1+
using System;
12
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Numerics;
5+
using Bogus;
26
using DuckDB.NET.Data;
7+
using DuckDB.NET.Native;
38
using FluentAssertions;
49
using Xunit;
510

611
namespace DuckDB.NET.Test.Parameters;
712

813
public class ListParameterTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db)
914
{
10-
[Fact]
11-
public void CanBindList()
15+
private void TestInsertSelect<T>(string duckDbType, Func<Faker, T> generator, int? length = null)
1216
{
13-
Command.CommandText = "Select ?";
14-
var parameter = new DuckDBParameter(new List<int> {1,2,3});
15-
16-
Command.Parameters.Add(parameter);
17+
var list = GetRandomList(generator, length ?? Random.Shared.Next(10, 200));
18+
var nestedList = Enumerable.Range(0, 5).SelectMany(i => GetRandomList(generator));
19+
20+
Command.CommandText = $"CREATE OR REPLACE TABLE ParameterListTest (a {duckDbType}[], b {duckDbType}[10], c {duckDbType}[][]);";
21+
Command.ExecuteNonQuery();
22+
23+
Command.CommandText = $"INSERT INTO ParameterListTest (a, b) VALUES ($list, $array);";
24+
Command.Parameters.Add(new DuckDBParameter(list));
25+
Command.Parameters.Add(new DuckDBParameter(list.Take(10).ToList()));
26+
Command.Parameters.Add(new DuckDBParameter(nestedList.ToList()));
27+
Command.ExecuteNonQuery();
28+
29+
Command.CommandText = $"SELECT * FROM ParameterListTest;";
1730

1831
using var reader = Command.ExecuteReader();
1932
reader.Read();
20-
reader.GetFieldValue<List<int>>(0).Should().BeEquivalentTo(new List<int>{1,2,3});
33+
var value = reader.GetFieldValue<List<T>>(0);
34+
35+
value.Should().BeEquivalentTo(list);
36+
37+
var arrayValue = reader.GetFieldValue<List<T>>(1);
38+
arrayValue.Should().BeEquivalentTo(list.Take(10));
39+
40+
Command.CommandText = $"DROP TABLE ParameterListTest";
41+
Command.ExecuteNonQuery();
42+
}
43+
44+
[Fact]
45+
public void CanBindBoolList()
46+
{
47+
TestInsertSelect("bool", faker => faker.Random.Bool());
48+
}
49+
50+
[Fact]
51+
public void CanBindSByteList()
52+
{
53+
TestInsertSelect("tinyint", faker => faker.Random.SByte());
54+
}
55+
56+
[Fact]
57+
public void CanBindShortList()
58+
{
59+
TestInsertSelect("SmallInt", faker => faker.Random.Short());
60+
}
61+
62+
[Fact]
63+
public void CanBindIntegerList()
64+
{
65+
TestInsertSelect("int", faker => faker.Random.Int());
66+
}
67+
68+
[Fact]
69+
public void CanBindLongList()
70+
{
71+
TestInsertSelect("BigInt", faker => faker.Random.Long());
72+
}
73+
74+
[Fact]
75+
public void CanBindHugeIntList()
76+
{
77+
TestInsertSelect("HugeInt",
78+
faker => BigInteger.Subtract(DuckDBHugeInt.HugeIntMaxValue, faker.Random.Int(min: 0)));
79+
}
80+
81+
[Fact]
82+
public void CanBindByteList()
83+
{
84+
TestInsertSelect("UTinyInt", faker => faker.Random.Byte());
85+
}
86+
87+
[Fact]
88+
public void CanBindUShortList()
89+
{
90+
TestInsertSelect("USmallInt", faker => faker.Random.UShort());
91+
}
92+
93+
[Fact]
94+
public void CanBindUIntList()
95+
{
96+
TestInsertSelect("UInteger", faker => faker.Random.UInt());
97+
}
98+
99+
[Fact]
100+
public void CanBindULongList()
101+
{
102+
TestInsertSelect("UBigInt", faker => faker.Random.UInt());
103+
}
104+
105+
[Fact]
106+
public void CanBindFloatList()
107+
{
108+
TestInsertSelect("Float", faker => faker.Random.Float());
109+
}
110+
111+
[Fact]
112+
public void CanBindDoubleList()
113+
{
114+
TestInsertSelect("Double", faker => faker.Random.Double());
115+
}
116+
117+
[Fact]
118+
public void CanBindDecimalList()
119+
{
120+
TestInsertSelect("Decimal(38, 28)", faker => faker.Random.Decimal());
121+
}
122+
123+
[Fact]
124+
public void CanBindGuidList()
125+
{
126+
TestInsertSelect("UUID", faker => faker.Random.Uuid());
127+
}
128+
129+
[Fact]
130+
public void CanBindDateTimeList()
131+
{
132+
TestInsertSelect("Date", faker => faker.Date.Past().Date);
133+
}
134+
135+
[Fact]
136+
public void CanBindDateTimeOffsetList()
137+
{
138+
TestInsertSelect("TimeTZ", faker => faker.Date.PastOffset(),1);
139+
}
140+
141+
[Fact]
142+
public void CanBindStringList()
143+
{
144+
TestInsertSelect("String", faker => faker.Random.Utf16String());
145+
}
146+
147+
[Fact]
148+
public void CanBindIntervalList()
149+
{
150+
TestInsertSelect("Interval", faker =>
151+
{
152+
var timespan = faker.Date.Timespan();
153+
154+
return TimeSpan.FromTicks(timespan.Ticks - timespan.Ticks % 10);
155+
});
21156
}
22157
}

DuckDB.NET.Test/Parameters/TimeTests.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,17 @@ public void QueryTimeTzReaderTest(int hour, int minute, int second, int microsec
157157
dateTimeOffset.Second.Should().Be((byte)second);
158158
dateTimeOffset.Ticks.Should().Be(new TimeOnly(hour, minute, second).Add(TimeSpan.FromTicks(microsecond * 10)).Ticks);
159159

160-
dateTimeOffset.Offset.Should().Be(new TimeSpan(offsetHours, offsetHours >= 0 ? offsetMinutes : -offsetMinutes, 0));
160+
var timeSpan = new TimeSpan(offsetHours, offsetHours >= 0 ? offsetMinutes : -offsetMinutes, 0);
161+
dateTimeOffset.Offset.Should().Be(timeSpan);
162+
163+
Command.CommandText = "SELECT ?";
164+
Command.Parameters.Add(new DuckDBParameter(dateTimeOffset));
165+
166+
using var reader = Command.ExecuteReader();
167+
reader.Read();
168+
169+
var fieldValue = reader.GetFieldValue<DateTimeOffset>(0);
170+
fieldValue.Offset.Should().Be(timeSpan);
161171
}
162172

163173
[Theory]

0 commit comments

Comments
 (0)