Skip to content

Commit 9717bc5

Browse files
authored
Updates GetLogicalType() to support creating a List type for IList<T> columns in table functions. (#325)
1 parent 7794d61 commit 9717bc5

5 files changed

Lines changed: 124 additions & 0 deletions

File tree

DuckDB.NET.Bindings/NativeMethods/NativeMethods.LogicalType.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ public static partial class LogicalType
99
[LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_create_logical_type")]
1010
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
1111
public static partial DuckDBLogicalType DuckDBCreateLogicalType(DuckDBType type);
12+
13+
[LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_create_list_type")]
14+
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
15+
public static partial DuckDBLogicalType DuckDBCreateListType(DuckDBLogicalType type);
1216

1317
// Maybe [SuppressGCTransition]: new LogicalType + DecimalType — two small allocations
1418
[LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_create_decimal_type")]

DuckDB.NET.Bindings/NativeMethods/NativeMethods.Vectors.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public static partial class Vectors
4949
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
5050
public static partial DuckDBState DuckDBListVectorReserve(IntPtr vector, ulong requiredCapacity);
5151

52+
[SuppressGCTransition]
53+
[LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_list_vector_set_size")]
54+
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
55+
public static partial DuckDBState DuckDBListVectorSetSize(IntPtr vector, ulong size);
56+
5257
[SuppressGCTransition]
5358
[LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_struct_vector_get_child")]
5459
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]

DuckDB.NET.Data/DataChunk/Writer/ListVectorDataWriter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ internal override bool AppendCollection(ICollection value, ulong rowIndex)
8686

8787
offset += count;
8888

89+
if (IsList)
90+
{
91+
NativeMethods.Vectors.DuckDBListVectorSetSize(Vector, offset);
92+
}
93+
8994
return result;
9095

9196
int WriteItems<T>(IEnumerable<T> items)

DuckDB.NET.Data/Extensions/TypeExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ public static DuckDBLogicalType GetLogicalType(this Type type)
8989
return NativeMethods.LogicalType.DuckDBCreateDecimalType(38, 18);
9090
}
9191

92+
if (type.IsAssignableTo(typeof(IList)) && type.IsGenericType)
93+
{
94+
var genericTypeParameter = type.GetGenericArguments()[0];
95+
using var nestedType = genericTypeParameter.GetLogicalType();
96+
return NativeMethods.LogicalType.DuckDBCreateListType(nestedType);
97+
}
98+
9299
if (ClrToDuckDBTypeMap.TryGetValue(type, out var duckDBType))
93100
{
94101
return NativeMethods.LogicalType.DuckDBCreateLogicalType(duckDBType);

DuckDB.NET.Test/TableFunctionTests.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,4 +471,107 @@ public void RegisterTableFunctionWithCardinality_AppearsInQueryPlan(int cardinal
471471

472472
reported.Should().Be(cardinality.ToString());
473473
}
474+
475+
[Fact]
476+
public void RegisterTableFunctionWithListColumn()
477+
{
478+
var sourceData = new List<List<int>>
479+
{
480+
new() { 1, 2, 3 },
481+
new() { 4, 5 },
482+
new() { },
483+
null,
484+
};
485+
486+
Connection.RegisterTableFunction("list_func", () =>
487+
{
488+
return new TableFunction(new List<ColumnInfo>
489+
{
490+
new("numbers", typeof(List<int>)),
491+
}, sourceData);
492+
}, (item, writers, rowIndex) =>
493+
{
494+
writers[0].WriteValue((List<int>)item, rowIndex);
495+
});
496+
497+
using var command = Connection.CreateCommand();
498+
command.CommandText = "SELECT numbers FROM list_func();";
499+
using var reader = command.ExecuteReader();
500+
501+
reader.Read();
502+
reader.GetFieldValue<List<int>>(0).Should().BeEquivalentTo(new List<int> { 1, 2, 3 });
503+
504+
reader.Read();
505+
reader.GetFieldValue<List<int>>(0).Should().BeEquivalentTo(new List<int> { 4, 5 });
506+
507+
reader.Read();
508+
reader.GetFieldValue<List<int>>(0).Should().BeEquivalentTo(new List<int>());
509+
510+
reader.Read();
511+
reader.IsDBNull(0).Should().BeTrue();
512+
513+
reader.Read().Should().BeFalse();
514+
}
515+
516+
[Fact]
517+
public void RegisterTableFunctionWithListColumn_ListHasAny()
518+
{
519+
var sourceData = new List<List<int>>
520+
{
521+
new() { 1, 2, 3 },
522+
new() { 4, 5 },
523+
new() { 6, 7, 8, 9 },
524+
new() { },
525+
};
526+
527+
Connection.RegisterTableFunction("list_func_has_any", () =>
528+
{
529+
return new TableFunction(new List<ColumnInfo>
530+
{
531+
new("numbers", typeof(List<int>)),
532+
}, sourceData);
533+
}, (item, writers, rowIndex) =>
534+
{
535+
writers[0].WriteValue((List<int>)item, rowIndex);
536+
});
537+
538+
var data = Connection.Query<bool>("SELECT list_has_any(numbers, [3]) FROM list_func_has_any();").ToList();
539+
data.Should().BeEquivalentTo([true, false, false, false]);
540+
}
541+
542+
[Fact]
543+
public void RegisterTableFunctionWithNestedListColumn()
544+
{
545+
var sourceData = new List<List<List<int?>>>
546+
{
547+
new() { new() { 1, 2 }, new() { 3, null, 5 } },
548+
new() { new() { 10 }, null, new() { 20, 30 }, new() { 40 } },
549+
};
550+
551+
Connection.RegisterTableFunction("list_func_nested", () =>
552+
{
553+
return new TableFunction(new List<ColumnInfo>
554+
{
555+
new("nested", typeof(List<List<int?>>)),
556+
}, sourceData);
557+
}, (item, writers, rowIndex) =>
558+
{
559+
writers[0].WriteValue((List<List<int?>>)item, rowIndex);
560+
});
561+
562+
// flatten reduces one level of nesting
563+
using var command = Connection.CreateCommand();
564+
command.CommandText = "SELECT flatten(nested) FROM list_func_nested();";
565+
using var reader = command.ExecuteReader();
566+
567+
reader.Read();
568+
reader.GetFieldValue<List<int?>>(0).Should().BeEquivalentTo([1, 2, 3, (int?)null, 5],
569+
options => options.WithStrictOrdering());
570+
571+
reader.Read();
572+
reader.GetFieldValue<List<int?>>(0).Should().BeEquivalentTo([10, 20, 30, 40],
573+
options => options.WithStrictOrdering());
574+
575+
reader.Read().Should().BeFalse();
576+
}
474577
}

0 commit comments

Comments
 (0)