Skip to content

Commit

Permalink
Merge branch 'Bind-Complex-Types' of https://github.com/Giorgi/DuckDB…
Browse files Browse the repository at this point in the history
….NET into Bind-Complex-Types
  • Loading branch information
Giorgi committed Oct 23, 2024
2 parents 9f25a34 + a496c4f commit 50332c7
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 18 deletions.
4 changes: 2 additions & 2 deletions DuckDB.NET.Bindings/NativeMethods/NativeMethods.Value.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ public static class Value
[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_time")]
public static extern DuckDBValue DuckDBCreateTime(DuckDBTime value);

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

[DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_create_timestamp")]
public static extern DuckDBValue DuckDBCreateTimestamp(DuckDBTimestampStruct value);
Expand Down
55 changes: 48 additions & 7 deletions DuckDB.NET.Data/Internal/ClrToDuckDBConverter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
Expand Down Expand Up @@ -27,19 +28,26 @@ public static DuckDBValue ToDuckDBValue(this object value) =>
Guid val => GuidToDuckDBValue(val),
BigInteger val => NativeMethods.Value.DuckDBCreateHugeInt(new DuckDBHugeInt(val)),
byte[] val => NativeMethods.Value.DuckDBCreateBlob(val, val.Length),
TimeSpan val => NativeMethods.Value.DuckDBCreateInterval(val),
DateTime val => NativeMethods.Value.DuckDBCreateTimestamp(NativeMethods.DateTimeHelpers.DuckDBToTimestamp(DuckDBTimestamp.FromDateTime(val))),
DateTimeOffset val => DateTimeOffsetToDuckDBValue(val),
DuckDBDateOnly val => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate(val)),
#if NET6_0_OR_GREATER
DateOnly val => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate(val)),
#endif
DuckDBTimeOnly val => NativeMethods.Value.DuckDBCreateTime(NativeMethods.DateTimeHelpers.DuckDBToTime(val)),
#if NET6_0_OR_GREATER
DateOnly val => NativeMethods.Value.DuckDBCreateDate(NativeMethods.DateTimeHelpers.DuckDBToDate(val)),
TimeOnly val => NativeMethods.Value.DuckDBCreateTime(NativeMethods.DateTimeHelpers.DuckDBToTime(val)),
#endif
ICollection<int> val => CreateListValue(DuckDBType.Integer, val),
ICollection val => CreateCollectionValue(val),
_ => throw new InvalidCastException($"Cannot convert value of type {value.GetType().FullName} to DuckDBValue.")
};

private static DuckDBValue DateTimeOffsetToDuckDBValue(DateTimeOffset val)
{
var duckDBToTime = NativeMethods.DateTimeHelpers.DuckDBToTime((DuckDBTimeOnly)val.DateTime);
var duckDBCreateTimeTz = NativeMethods.DateTimeHelpers.DuckDBCreateTimeTz(duckDBToTime.Micros, (int)val.Offset.TotalSeconds);
return NativeMethods.Value.DuckDBCreateTimeTz(duckDBCreateTimeTz);
}

private static DuckDBValue GuidToDuckDBValue(Guid value)
{
using var handle = value.ToString().ToUnmanagedString();
Expand All @@ -58,20 +66,53 @@ private static DuckDBValue StringToDuckDBValue(string value)
return NativeMethods.Value.DuckDBCreateVarchar(handle);
}

private static DuckDBValue CreateListValue<T>(DuckDBType duckDBType, ICollection<T> collection)
private static DuckDBValue CreateCollectionValue(ICollection collection)
{
return collection switch
{
ICollection<bool> items => CreateCollectionValue(DuckDBType.Boolean, items),

ICollection<sbyte> items => CreateCollectionValue(DuckDBType.TinyInt, items),
ICollection<byte> items => CreateCollectionValue(DuckDBType.UnsignedTinyInt, items),
ICollection<short> items => CreateCollectionValue(DuckDBType.SmallInt, items),
ICollection<ushort> items => CreateCollectionValue(DuckDBType.UnsignedSmallInt, items),
ICollection<int> items => CreateCollectionValue(DuckDBType.Integer, items),
ICollection<uint> items => CreateCollectionValue(DuckDBType.UnsignedInteger, items),
ICollection<long> items => CreateCollectionValue(DuckDBType.BigInt, items),
ICollection<ulong> items => CreateCollectionValue(DuckDBType.UnsignedBigInt, items),
ICollection<float> items => CreateCollectionValue(DuckDBType.Float, items),
ICollection<double> items => CreateCollectionValue(DuckDBType.Double, items),
ICollection<BigInteger> items => CreateCollectionValue(DuckDBType.HugeInt, items),
ICollection<decimal> items => CreateCollectionValue(DuckDBType.Varchar, items),
ICollection<Guid> items => CreateCollectionValue(DuckDBType.Varchar, items),
ICollection<string> items => CreateCollectionValue(DuckDBType.Varchar, items),
ICollection<DateTime> items => CreateCollectionValue(DuckDBType.Date, items),
ICollection<DateTimeOffset> items => CreateCollectionValue(DuckDBType.TimeTz, items),
ICollection<TimeSpan> items => CreateCollectionValue(DuckDBType.Interval, items),
ICollection<object> items => CreateCollectionValue(DuckDBType.List, items),
_ => throw new InvalidOperationException($"Cannot convert collection type {collection.GetType().FullName} to DuckDBValue.")
};
}

private static DuckDBValue CreateCollectionValue<T>(DuckDBType duckDBType, ICollection<T> collection)
{
using var logicalType = NativeMethods.LogicalType.DuckDBCreateLogicalType(duckDBType);
using var listItemType = NativeMethods.LogicalType.DuckDBCreateLogicalType(duckDBType);

var values = new DuckDBValue[collection.Count];

var index = 0;
foreach (var item in collection)
{
if (item == null)
{
throw new InvalidOperationException($"Cannot convert null to DuckDBValue.");
}

var duckDBValue = item.ToDuckDBValue();
values[index] = duckDBValue;
index++;
}

return NativeMethods.Value.DuckDBCreateListValue(logicalType, values, collection.Count);
return NativeMethods.Value.DuckDBCreateListValue(listItemType, values, collection.Count);
}
}
2 changes: 1 addition & 1 deletion DuckDB.NET.Test/Parameters/IntegerParametersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private void TestSimple<TValue>(string duckDbType, TValue? expectedValue, Func<D
var scalar = Command.ExecuteScalar();
scalar.Should().Be(expectedValue);

var reader = Command.ExecuteReader();
using var reader = Command.ExecuteReader();
reader.Read();
var value = getValue(reader);

Expand Down
149 changes: 142 additions & 7 deletions DuckDB.NET.Test/Parameters/ListParameterTests.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Bogus;
using DuckDB.NET.Data;
using DuckDB.NET.Native;
using FluentAssertions;
using Xunit;

namespace DuckDB.NET.Test.Parameters;

public class ListParameterTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db)
{
[Fact]
public void CanBindList()
private void TestInsertSelect<T>(string duckDbType, Func<Faker, T> generator, int? length = null)
{
Command.CommandText = "Select ?";
var parameter = new DuckDBParameter(new List<int> {1,2,3});

Command.Parameters.Add(parameter);
var list = GetRandomList(generator, length ?? Random.Shared.Next(10, 200));
var nestedList = Enumerable.Range(0, 5).SelectMany(i => GetRandomList(generator));

Command.CommandText = $"CREATE OR REPLACE TABLE ParameterListTest (a {duckDbType}[], b {duckDbType}[10], c {duckDbType}[][]);";
Command.ExecuteNonQuery();

Command.CommandText = $"INSERT INTO ParameterListTest (a, b) VALUES ($list, $array);";
Command.Parameters.Add(new DuckDBParameter(list));
Command.Parameters.Add(new DuckDBParameter(list.Take(10).ToList()));
Command.Parameters.Add(new DuckDBParameter(nestedList.ToList()));
Command.ExecuteNonQuery();

Command.CommandText = $"SELECT * FROM ParameterListTest;";

using var reader = Command.ExecuteReader();
reader.Read();
reader.GetFieldValue<List<int>>(0).Should().BeEquivalentTo(new List<int>{1,2,3});
var value = reader.GetFieldValue<List<T>>(0);

value.Should().BeEquivalentTo(list);

var arrayValue = reader.GetFieldValue<List<T>>(1);
arrayValue.Should().BeEquivalentTo(list.Take(10));

Command.CommandText = $"DROP TABLE ParameterListTest";
Command.ExecuteNonQuery();
}

[Fact]
public void CanBindBoolList()
{
TestInsertSelect("bool", faker => faker.Random.Bool());
}

[Fact]
public void CanBindSByteList()
{
TestInsertSelect("tinyint", faker => faker.Random.SByte());
}

[Fact]
public void CanBindShortList()
{
TestInsertSelect("SmallInt", faker => faker.Random.Short());
}

[Fact]
public void CanBindIntegerList()
{
TestInsertSelect("int", faker => faker.Random.Int());
}

[Fact]
public void CanBindLongList()
{
TestInsertSelect("BigInt", faker => faker.Random.Long());
}

[Fact]
public void CanBindHugeIntList()
{
TestInsertSelect("HugeInt",
faker => BigInteger.Subtract(DuckDBHugeInt.HugeIntMaxValue, faker.Random.Int(min: 0)));
}

[Fact]
public void CanBindByteList()
{
TestInsertSelect("UTinyInt", faker => faker.Random.Byte());
}

[Fact]
public void CanBindUShortList()
{
TestInsertSelect("USmallInt", faker => faker.Random.UShort());
}

[Fact]
public void CanBindUIntList()
{
TestInsertSelect("UInteger", faker => faker.Random.UInt());
}

[Fact]
public void CanBindULongList()
{
TestInsertSelect("UBigInt", faker => faker.Random.UInt());
}

[Fact]
public void CanBindFloatList()
{
TestInsertSelect("Float", faker => faker.Random.Float());
}

[Fact]
public void CanBindDoubleList()
{
TestInsertSelect("Double", faker => faker.Random.Double());
}

[Fact]
public void CanBindDecimalList()
{
TestInsertSelect("Decimal(38, 28)", faker => faker.Random.Decimal());
}

[Fact]
public void CanBindGuidList()
{
TestInsertSelect("UUID", faker => faker.Random.Uuid());
}

[Fact]
public void CanBindDateTimeList()
{
TestInsertSelect("Date", faker => faker.Date.Past().Date);
}

[Fact]
public void CanBindDateTimeOffsetList()
{
TestInsertSelect("TimeTZ", faker => faker.Date.PastOffset(),1);
}

[Fact]
public void CanBindStringList()
{
TestInsertSelect("String", faker => faker.Random.Utf16String());
}

[Fact]
public void CanBindIntervalList()
{
TestInsertSelect("Interval", faker =>
{
var timespan = faker.Date.Timespan();

return TimeSpan.FromTicks(timespan.Ticks - timespan.Ticks % 10);
});
}
}
12 changes: 11 additions & 1 deletion DuckDB.NET.Test/Parameters/TimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,17 @@ public void QueryTimeTzReaderTest(int hour, int minute, int second, int microsec
dateTimeOffset.Second.Should().Be((byte)second);
dateTimeOffset.Ticks.Should().Be(new TimeOnly(hour, minute, second).Add(TimeSpan.FromTicks(microsecond * 10)).Ticks);

dateTimeOffset.Offset.Should().Be(new TimeSpan(offsetHours, offsetHours >= 0 ? offsetMinutes : -offsetMinutes, 0));
var timeSpan = new TimeSpan(offsetHours, offsetHours >= 0 ? offsetMinutes : -offsetMinutes, 0);
dateTimeOffset.Offset.Should().Be(timeSpan);

Command.CommandText = "SELECT ?";
Command.Parameters.Add(new DuckDBParameter(dateTimeOffset));

using var reader = Command.ExecuteReader();
reader.Read();

var fieldValue = reader.GetFieldValue<DateTimeOffset>(0);
fieldValue.Offset.Should().Be(timeSpan);
}

[Theory]
Expand Down

0 comments on commit 50332c7

Please sign in to comment.