From ca3858ac1f9480e8403e02e44e642fcb7f4d39ca Mon Sep 17 00:00:00 2001 From: Skuirrels Date: Tue, 21 Jul 2026 10:06:58 +0100 Subject: [PATCH] Pre-size list results during materialization --- .../DataChunk/Reader/ListVectorDataReader.cs | 42 +++++++++++++++++-- DuckDB.NET.Test/DuckDBDataReaderListTests.cs | 15 ++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/DuckDB.NET.Data/DataChunk/Reader/ListVectorDataReader.cs b/DuckDB.NET.Data/DataChunk/Reader/ListVectorDataReader.cs index 26990932..a1bc8a61 100644 --- a/DuckDB.NET.Data/DataChunk/Reader/ListVectorDataReader.cs +++ b/DuckDB.NET.Data/DataChunk/Reader/ListVectorDataReader.cs @@ -4,6 +4,8 @@ internal sealed class ListVectorDataReader : VectorDataReaderBase { private readonly ulong arraySize; private readonly VectorDataReaderBase listDataReader; + private Type? cachedListType; + private IListFactory? cachedListFactory; public bool IsList => DuckDBType == DuckDBType.List; @@ -51,8 +53,7 @@ private object GetList(Type returnType, ulong listOffset, ulong length) var allowNulls = listType.AllowsNullValue(out _, out var nullableType); - var list = Activator.CreateInstance(returnType) as IList - ?? throw new ArgumentException($"The type '{returnType.Name}' specified in parameter {nameof(returnType)} cannot be instantiated as an IList."); + var list = CreateList(returnType, length); //Special case for specific types to avoid boxing return list switch @@ -105,6 +106,31 @@ IList BuildListCommon(IList result, Type targetType) } } + private IList CreateList(Type returnType, ulong length) + { + if (returnType != cachedListType) + { + cachedListType = returnType; + cachedListFactory = returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(List<>) + ? CreateListFactory(returnType) + : null; + } + + if (cachedListFactory != null) + { + return cachedListFactory.Create(checked((int)length)); + } + + return Activator.CreateInstance(returnType) as IList + ?? throw new ArgumentException($"The type '{returnType.Name}' specified in parameter {nameof(returnType)} cannot be instantiated as an IList."); + } + + private static IListFactory CreateListFactory(Type returnType) + { + var factoryType = typeof(ListFactory<>).MakeGenericType(returnType.GetGenericArguments()[0]); + return (IListFactory)Activator.CreateInstance(factoryType)!; + } + internal override void Reset(IntPtr vector) { base.Reset(vector); @@ -119,4 +145,14 @@ public override void Dispose() listDataReader.Dispose(); base.Dispose(); } -} \ No newline at end of file + + private interface IListFactory + { + IList Create(int capacity); + } + + private sealed class ListFactory : IListFactory + { + public IList Create(int capacity) => new List(capacity); + } +} diff --git a/DuckDB.NET.Test/DuckDBDataReaderListTests.cs b/DuckDB.NET.Test/DuckDBDataReaderListTests.cs index bca7fe27..32686930 100644 --- a/DuckDB.NET.Test/DuckDBDataReaderListTests.cs +++ b/DuckDB.NET.Test/DuckDBDataReaderListTests.cs @@ -2,6 +2,19 @@ public class DuckDBDataReaderListTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) { + [Fact] + public void PreSizesListResults() + { + Command.CommandText = "SELECT range(17)::INTEGER[];"; + using var reader = Command.ExecuteReader(); + + reader.Read(); + var list = reader.GetFieldValue>(0); + + list.Should().HaveCount(17); + list.Capacity.Should().Be(17); + } + [Fact] public void ReadListOfIntegers() { @@ -251,4 +264,4 @@ class Person public List Ids { get; set; } public DuckDBDataReaderEnumTests.Mood Mood { get; set; } } -} \ No newline at end of file +}