Skip to content

Commit 2a4f314

Browse files
committed
Support binding ICollection parameters when DuckDB type is unknown
When a parameter's DuckDB type can't be inferred (e.g. unnest($p) where the prepared statement reports an ambiguous type), fall back to building a list from the CLR element type so collections bind correctly. Add UnnestTests covering string/int/long/double/decimal lists, nulls, and nested int lists.
1 parent 2938dc9 commit 2a4f314

2 files changed

Lines changed: 158 additions & 8 deletions

File tree

DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log
9393
(DuckDBType.Blob, byte[] value) => NativeMethods.Value.DuckDBCreateBlob(value, value.Length),
9494
(DuckDBType.List, ICollection value) => CreateCollectionValue(logicalType, value, true, dbType),
9595
(DuckDBType.Array, ICollection value) => CreateCollectionValue(logicalType, value, false, dbType),
96+
(_, ICollection value) when item is not byte[] => CreateListFromClrType(value, dbType),
9697
_ when ValueCreators.TryGetValue(dbType, out var converter) => converter(item),
9798
_ => NativeMethods.Value.DuckDBCreateVarchar(item.ToString())
9899
};
@@ -120,23 +121,42 @@ bool TryConvertTo<T>(out T result) where T : struct
120121

121122
private static DuckDBValue CreateCollectionValue(DuckDBLogicalType logicalType, ICollection collection, bool isList, DbType dbType)
122123
{
123-
using var collectionItemType = isList ? NativeMethods.LogicalType.DuckDBListTypeChildType(logicalType) :
124-
NativeMethods.LogicalType.DuckDBArrayTypeChildType(logicalType);
124+
using var childType = isList ? NativeMethods.LogicalType.DuckDBListTypeChildType(logicalType) :
125+
NativeMethods.LogicalType.DuckDBArrayTypeChildType(logicalType);
125126

126-
var duckDBType = NativeMethods.LogicalType.DuckDBGetTypeId(collectionItemType);
127+
var values = BuildValues(childType, collection, dbType);
127128

129+
return isList ? NativeMethods.Value.DuckDBCreateListValue(childType, values, collection.Count)
130+
: NativeMethods.Value.DuckDBCreateArrayValue(childType, values, collection.Count);
131+
}
132+
133+
private static DuckDBValue CreateListFromClrType(ICollection collection, DbType dbType)
134+
{
135+
var elementType = collection.GetType().GetInterface(typeof(IEnumerable<>).Name)?.GetGenericArguments()[0];
136+
137+
if (elementType == null)
138+
{
139+
return NativeMethods.Value.DuckDBCreateVarchar(collection.ToString());
140+
}
141+
142+
using var childType = elementType.GetLogicalType();
143+
var values = BuildValues(childType, collection, dbType);
144+
145+
return NativeMethods.Value.DuckDBCreateListValue(childType, values, collection.Count);
146+
}
147+
148+
private static DuckDBValue[] BuildValues(DuckDBLogicalType childType, ICollection collection, DbType dbType)
149+
{
150+
var childDuckDBType = NativeMethods.LogicalType.DuckDBGetTypeId(childType);
128151
var values = new DuckDBValue[collection.Count];
129152

130153
var index = 0;
131154
foreach (var item in collection)
132155
{
133-
var duckDBValue = item.ToDuckDBValue(collectionItemType, duckDBType, dbType);
134-
values[index] = duckDBValue;
135-
index++;
156+
values[index++] = item.ToDuckDBValue(childType, childDuckDBType, dbType);
136157
}
137158

138-
return isList ? NativeMethods.Value.DuckDBCreateListValue(collectionItemType, values, collection.Count)
139-
: NativeMethods.Value.DuckDBCreateArrayValue(collectionItemType, values, collection.Count);
159+
return values;
140160
}
141161

142162
private static DuckDBValue DecimalToDuckDBValue(decimal value)

DuckDB.NET.Test/UnnestTests.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
namespace DuckDB.NET.Test;
2+
3+
public class UnnestTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db)
4+
{
5+
[Fact]
6+
public void UnnestStringList()
7+
{
8+
var names = new List<string> { "Bob", "Sam" };
9+
Command.CommandText = "SELECT unnest($names);";
10+
Command.Parameters.Add(new DuckDBParameter("names", names));
11+
12+
using var reader = Command.ExecuteReader();
13+
14+
var results = new List<string>();
15+
while (reader.Read())
16+
{
17+
results.Add(reader.GetString(0));
18+
}
19+
20+
results.Should().BeEquivalentTo(names);
21+
}
22+
23+
[Fact]
24+
public void UnnestStringListWithNull()
25+
{
26+
var names = new List<string> { "Bob", null, "Sam" };
27+
Command.CommandText = "SELECT unnest($names);";
28+
Command.Parameters.Add(new DuckDBParameter("names", names));
29+
30+
using var reader = Command.ExecuteReader();
31+
32+
var results = new List<string>();
33+
while (reader.Read())
34+
{
35+
results.Add(reader.IsDBNull(0) ? null : reader.GetString(0));
36+
}
37+
38+
results.Should().BeEquivalentTo(names);
39+
}
40+
41+
[Fact]
42+
public void UnnestIntList()
43+
{
44+
var numbers = new List<int> { 1, 2, 3, 42 };
45+
Command.CommandText = "SELECT unnest($numbers);";
46+
Command.Parameters.Add(new DuckDBParameter("numbers", numbers));
47+
48+
using var reader = Command.ExecuteReader();
49+
50+
var results = new List<int>();
51+
while (reader.Read())
52+
{
53+
results.Add(reader.GetInt32(0));
54+
}
55+
56+
results.Should().BeEquivalentTo(numbers);
57+
}
58+
59+
[Fact]
60+
public void UnnestLongList()
61+
{
62+
var numbers = new List<long> { 1L, 2L, long.MaxValue };
63+
Command.CommandText = "SELECT unnest($numbers);";
64+
Command.Parameters.Add(new DuckDBParameter("numbers", numbers));
65+
66+
using var reader = Command.ExecuteReader();
67+
68+
var results = new List<long>();
69+
while (reader.Read())
70+
{
71+
results.Add(reader.GetInt64(0));
72+
}
73+
74+
results.Should().BeEquivalentTo(numbers);
75+
}
76+
77+
[Fact]
78+
public void UnnestDoubleList()
79+
{
80+
var numbers = new List<double> { 1.5, 2.5, 3.14 };
81+
Command.CommandText = "SELECT unnest($numbers);";
82+
Command.Parameters.Add(new DuckDBParameter("numbers", numbers));
83+
84+
using var reader = Command.ExecuteReader();
85+
86+
var results = new List<double>();
87+
while (reader.Read())
88+
{
89+
results.Add(reader.GetDouble(0));
90+
}
91+
92+
results.Should().BeEquivalentTo(numbers);
93+
}
94+
95+
[Fact]
96+
public void UnnestDecimalList()
97+
{
98+
var numbers = new List<decimal> { 1.1m, 2.22m, 3.333m };
99+
Command.CommandText = "SELECT unnest($numbers);";
100+
Command.Parameters.Add(new DuckDBParameter("numbers", numbers));
101+
102+
using var reader = Command.ExecuteReader();
103+
104+
var results = new List<decimal>();
105+
while (reader.Read())
106+
{
107+
results.Add(reader.GetDecimal(0));
108+
}
109+
110+
results.Should().BeEquivalentTo(numbers);
111+
}
112+
113+
[Fact]
114+
public void UnnestNestedIntList()
115+
{
116+
var nested = new List<List<int>> { new() { 1, 2 }, new() { 3, 4, 5 } };
117+
Command.CommandText = "SELECT unnest($nested);";
118+
Command.Parameters.Add(new DuckDBParameter("nested", nested));
119+
120+
using var reader = Command.ExecuteReader();
121+
122+
var results = new List<List<int>>();
123+
while (reader.Read())
124+
{
125+
results.Add(((IEnumerable<int>)reader.GetValue(0)).ToList());
126+
}
127+
128+
results.Should().BeEquivalentTo(nested);
129+
}
130+
}

0 commit comments

Comments
 (0)