Skip to content

Commit

Permalink
Merge branch 'Struct-Support' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Giorgi committed Nov 3, 2023
2 parents 59b5c7e + 745e108 commit 5d1f428
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ public static class LogicalType
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_list_type_child_type")]
public static extern DuckDBLogicalType DuckDBListTypeChildType(DuckDBLogicalType type);

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_struct_type_child_count")]
public static extern long DuckDBStructTypeChildCount(DuckDBLogicalType type);

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_struct_type_child_name")]
public static extern IntPtr DuckDBStructTypeChildName(DuckDBLogicalType type, long index);

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_struct_type_child_type")]
public static extern DuckDBLogicalType DuckDBStructTypeChildType(DuckDBLogicalType type, long index);

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_destroy_logical_type")]
public static extern void DuckDBDestroyLogicalType(out IntPtr type);
}
Expand Down
12 changes: 2 additions & 10 deletions DuckDB.NET.Data/DuckDBDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,19 +195,11 @@ public override T GetFieldValue<T>(int ordinal)
{
DuckDBType.List => (T)vectorReaders[ordinal].GetList(rowsReadFromCurrentChunk - 1, typeof(T)),
DuckDBType.Enum => (T)vectorReaders[ordinal].GetEnum(rowsReadFromCurrentChunk - 1, typeof(T)),
DuckDBType.Struct => (T)vectorReaders[ordinal].GetStruct(rowsReadFromCurrentChunk - 1, typeof(T)),
_ => (T)vectorReaders[ordinal].GetValue(rowsReadFromCurrentChunk - 1)
};

if (value is not null)
return (T)value;

if (default(T) is null && typeof(T).IsValueType)
return default!;

if (typeof(T) == typeof(object))
return (T)(object)DBNull.Value;

throw new InvalidCastException();
return value;
}

public override object GetValue(int ordinal)
Expand Down
4 changes: 4 additions & 0 deletions DuckDB.NET.Data/Internal/IsExternalInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}
11 changes: 11 additions & 0 deletions DuckDB.NET.Data/Internal/TypeDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;

namespace DuckDB.NET.Data.Internal;

class TypeDetails
{
public Dictionary<string, PropertyDetails> Properties { get; set; } = new();
}

record PropertyDetails(Type PropertyType, bool Nullable, Action<object, object> Setter);
118 changes: 117 additions & 1 deletion DuckDB.NET.Data/VectorDataReader.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using DuckDB.NET.Data.Internal;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq.Expressions;
using System.Numerics;
using System.Text;

Expand All @@ -21,11 +24,14 @@ internal class VectorDataReader : IDisposable
internal DuckDBType DuckDBType { get; }

private readonly VectorDataReader? listDataReader;
private readonly Dictionary<string, VectorDataReader>? structDataReaders;

private readonly byte scale;
private readonly DuckDBType decimalType;
private readonly DuckDBType enumType;

private static readonly ConcurrentDictionary<Type, TypeDetails> TypeCache = new();

internal unsafe VectorDataReader(IntPtr vector, void* dataPointer, ulong* validityMaskPointer, DuckDBType columnType)
{
this.vector = vector;
Expand Down Expand Up @@ -58,6 +64,26 @@ internal unsafe VectorDataReader(IntPtr vector, void* dataPointer, ulong* validi
listDataReader = new VectorDataReader(childVector, childVectorData, childVectorValidity, type);
break;
}
case DuckDBType.Struct:
{
var memberCount = NativeMethods.LogicalType.DuckDBStructTypeChildCount(logicalType);
structDataReaders = new Dictionary<string, VectorDataReader>(StringComparer.OrdinalIgnoreCase);

for (int index = 0; index < memberCount; index++)
{
var name = NativeMethods.LogicalType.DuckDBStructTypeChildName(logicalType, index).ToManagedString();
var childVector = NativeMethods.DataChunks.DuckDBStructVectorGetChild(vector, index);

var childVectorData = NativeMethods.DataChunks.DuckDBVectorGetData(childVector);
var childVectorValidity = NativeMethods.DataChunks.DuckDBVectorGetValidity(childVector);

using var childType = NativeMethods.LogicalType.DuckDBStructTypeChildType(logicalType, index);
var type = NativeMethods.LogicalType.DuckDBGetTypeId(childType);

structDataReaders[name] = new VectorDataReader(childVector, childVectorData, childVectorValidity, type);
}
break;
}
}

ClrType = DuckDBType switch
Expand All @@ -84,6 +110,7 @@ internal unsafe VectorDataReader(IntPtr vector, void* dataPointer, ulong* validi
DuckDBType.Blob => typeof(Stream),
DuckDBType.Enum => typeof(string),
DuckDBType.List => typeof(List<>).MakeGenericType(listDataReader!.ClrType),
DuckDBType.Struct => typeof(Dictionary<string, object>),
var type => throw new ArgumentException($"Unrecognised type {type} ({(int)type})")
};
}
Expand Down Expand Up @@ -199,6 +226,86 @@ internal object GetEnum(ulong offset, Type returnType)
return enumItem;
}

internal object GetStruct(ulong offset, Type returnType)
{
if (structDataReaders is null)
{
throw new InvalidOperationException("Can't get a struct from a non-struct vector.");
}

var result = Activator.CreateInstance(returnType);

if (result is Dictionary<string, object?> dictionary)
{
foreach (var reader in structDataReaders)
{
var value = reader.Value.IsValid(offset) ? reader.Value.GetValue(offset) : null;
dictionary.Add(reader.Key, value);
}

return result;
}

var typeDetails = TypeCache.GetOrAdd(returnType, type =>
{
var propertyInfos = returnType.GetProperties();
var details = new TypeDetails();

foreach (var propertyInfo in propertyInfos)
{
if (propertyInfo.SetMethod == null)
{
continue;
}

var isNullable = !propertyInfo.PropertyType.IsValueType || Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;

var instanceParam = Expression.Parameter(typeof(object));
var argumentParam = Expression.Parameter(typeof(object));

var setAction = Expression.Lambda<Action<object, object>>(
Expression.Call(Expression.Convert(instanceParam, type), propertyInfo.SetMethod, Expression.Convert(argumentParam, propertyInfo.PropertyType)),
instanceParam, argumentParam
).Compile();

details.Properties.Add(propertyInfo.Name, new PropertyDetails(propertyInfo.PropertyType, isNullable, setAction));
}

return details;
});

foreach (var properties in typeDetails.Properties)
{
structDataReaders.TryGetValue(properties.Key, out var reader);
var isNullable = properties.Value.Nullable;

if (reader == null)
{
if (!isNullable)
{
throw new NullReferenceException($"Property '{properties.Key}' not found in struct");
}

continue;
}

if (reader.IsValid(offset))
{
var value = reader.GetValue(offset, properties.Value.PropertyType);
properties.Value.Setter(result!, value);
}
else
{
if (!isNullable)
{
throw new NullReferenceException($"Property '{properties.Key}' is not nullable but struct contains null value");
}
}
}

return result!;
}

internal unsafe object GetList(ulong offset, Type returnType)
{
if (listDataReader is null)
Expand Down Expand Up @@ -268,6 +375,7 @@ internal object GetValue(ulong offset, Type? targetType = null)
DuckDBType.Blob => GetStream(offset),
DuckDBType.List => GetList(offset, targetType ?? ClrType),
DuckDBType.Enum => GetEnum(offset, targetType ?? ClrType),
DuckDBType.Struct => GetStruct(offset, targetType ?? ClrType),
var type => throw new ArgumentException($"Unrecognised type {type} ({(int)type})")
};
}
Expand Down Expand Up @@ -322,5 +430,13 @@ public void Dispose()
{
listDataReader?.Dispose();
logicalType?.Dispose();

if (structDataReaders != null)
{
foreach (var item in structDataReaders)
{
item.Value.Dispose();
}
}
}
}
16 changes: 6 additions & 10 deletions DuckDB.NET.Test/DuckDBDataReaderEnumTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using FluentAssertions;
using Xunit;

Expand All @@ -23,8 +22,7 @@ public DuckDBDataReaderEnumTests(DuckDBDatabaseFixture db) : base(db)
public void SelectEnumValues()
{
Command.CommandText = "Select * from person order by name desc";
var reader = Command.ExecuteReader();

using var reader = Command.ExecuteReader();
reader.Read();
reader.GetFieldValue<Mood>(1).Should().Be(Mood.Happy);

Expand All @@ -42,19 +40,18 @@ public void SelectEnumValues()
public void SelectEnumList()
{
Command.CommandText = "Select ['happy'::mood, 'ok'::mood]";
var reader = Command.ExecuteReader();

using var reader = Command.ExecuteReader();
reader.Read();
var list = reader.GetFieldValue<List<Mood>>(0);

list.Should().BeEquivalentTo(new List<Mood> { Mood.Happy, Mood.Ok });
}

[Fact]
public void SelectEnumValuesAsNullable()
{
Command.CommandText = "Select * from person order by name desc";
var reader = Command.ExecuteReader();

using var reader = Command.ExecuteReader();
reader.Read();
reader.GetFieldValue<Mood?>(1).Should().Be(Mood.Happy);

Expand All @@ -75,8 +72,7 @@ public void SelectEnumValuesAsNullable()
public void SelectEnumValuesAsString()
{
Command.CommandText = "Select * from person order by name desc";
var reader = Command.ExecuteReader();

using var reader = Command.ExecuteReader();
reader.Read();
reader.GetFieldValue<string>(1).Should().BeEquivalentTo(Mood.Happy.ToString());

Expand Down
Loading

0 comments on commit 5d1f428

Please sign in to comment.