Skip to content

Commit

Permalink
Fix parameter binding in statements with cast. Fixes 245
Browse files Browse the repository at this point in the history
  • Loading branch information
Giorgi committed Feb 15, 2025
1 parent 96c6c9e commit 6025a67
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 15 deletions.
41 changes: 27 additions & 14 deletions DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal static class ClrToDuckDBConverter
{
public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType logicalType, DuckDBType duckDBType)
{
if (item.IsNull())
if (item.IsNull() || item == null) //item == null is redundant but net standard can't understand that item isn't null after this point.
{
return NativeMethods.Value.DuckDBCreateNullValue();
}
Expand All @@ -20,15 +20,15 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log
{
(DuckDBType.Boolean, bool value) => NativeMethods.Value.DuckDBCreateBool(value),

(DuckDBType.TinyInt, _) => NativeMethods.Value.DuckDBCreateInt8(ConvertTo<sbyte>()),
(DuckDBType.SmallInt, _) => NativeMethods.Value.DuckDBCreateInt16(ConvertTo<short>()),
(DuckDBType.Integer, _) => NativeMethods.Value.DuckDBCreateInt32(ConvertTo<int>()),
(DuckDBType.BigInt, _) => NativeMethods.Value.DuckDBCreateInt64(ConvertTo<long>()),
(DuckDBType.TinyInt, _) => TryConvertTo<sbyte>(out var result) ? NativeMethods.Value.DuckDBCreateInt8(result) : StringToDuckDBValue(item.ToString()),
(DuckDBType.SmallInt, _) => TryConvertTo<short>(out var result) ? NativeMethods.Value.DuckDBCreateInt16(result) : StringToDuckDBValue(item.ToString()),
(DuckDBType.Integer, _) => TryConvertTo<int>(out var result) ? NativeMethods.Value.DuckDBCreateInt32(result) : StringToDuckDBValue(item.ToString()),
(DuckDBType.BigInt, _) => TryConvertTo<long>(out var result) ? NativeMethods.Value.DuckDBCreateInt64(result) : StringToDuckDBValue(item.ToString()),

(DuckDBType.UnsignedTinyInt, _) => NativeMethods.Value.DuckDBCreateUInt8(ConvertTo<byte>()),
(DuckDBType.UnsignedSmallInt, _) => NativeMethods.Value.DuckDBCreateUInt16(ConvertTo<ushort>()),
(DuckDBType.UnsignedInteger, _) => NativeMethods.Value.DuckDBCreateUInt32(ConvertTo<uint>()),
(DuckDBType.UnsignedBigInt, _) => NativeMethods.Value.DuckDBCreateUInt64(ConvertTo<ulong>()),
(DuckDBType.UnsignedTinyInt, _) => TryConvertTo<byte>(out var result) ? NativeMethods.Value.DuckDBCreateUInt8(result) : StringToDuckDBValue(item.ToString()),
(DuckDBType.UnsignedSmallInt, _) => TryConvertTo<ushort>(out var result) ? NativeMethods.Value.DuckDBCreateUInt16(result) : StringToDuckDBValue(item.ToString()),
(DuckDBType.UnsignedInteger, _) => TryConvertTo<uint>(out var result) ? NativeMethods.Value.DuckDBCreateUInt32(result) : StringToDuckDBValue(item.ToString()),
(DuckDBType.UnsignedBigInt, _) => TryConvertTo<ulong>(out var result) ? NativeMethods.Value.DuckDBCreateUInt64(result) : StringToDuckDBValue(item.ToString()),

(DuckDBType.Float, float value) => NativeMethods.Value.DuckDBCreateFloat(value),
(DuckDBType.Double, double value) => NativeMethods.Value.DuckDBCreateDouble(value),
Expand Down Expand Up @@ -58,18 +58,31 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log
(DuckDBType.Blob, byte[] value) => NativeMethods.Value.DuckDBCreateBlob(value, value.Length),
(DuckDBType.List, ICollection value) => CreateCollectionValue(logicalType, value, true),
(DuckDBType.Array, ICollection value) => CreateCollectionValue(logicalType, value, false),
_ => throw new InvalidOperationException($"Cannot bind parameter type {item!.GetType().FullName} to column of type {duckDBType}")
_ => StringToDuckDBValue(item.ToString())
};

T ConvertTo<T>()
bool TryConvertTo<T>(out T result) where T : struct
#if NET8_0_OR_GREATER
, IParsable<T>?
#endif
{
try
{
return (T)Convert.ChangeType(item, typeof(T));
#if NET8_0_OR_GREATER
if (T.TryParse(item.ToString(), CultureInfo.InvariantCulture, out result))
{
return true;
}
return false;
#else
result = (T)Convert.ChangeType(item, typeof(T));
return false;
#endif
}
catch (Exception)
{
throw new ArgumentOutOfRangeException($"Cannot bind parameter '{item}' type {item!.GetType().FullName} to column of type {duckDBType}");
result = default;
return false;
}
}
}
Expand Down Expand Up @@ -101,7 +114,7 @@ private static DuckDBValue GuidToDuckDBValue(Guid value)
return NativeMethods.Value.DuckDBCreateVarchar(handle);
}

private static DuckDBValue StringToDuckDBValue(string value)
private static DuckDBValue StringToDuckDBValue(string? value)
{
using var handle = value.ToUnmanagedString();
return NativeMethods.Value.DuckDBCreateVarchar(handle);
Expand Down
66 changes: 66 additions & 0 deletions DuckDB.NET.Test/Parameters/ExplicitCastTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using DuckDB.NET.Data;
using FluentAssertions;
using Xunit;

namespace DuckDB.NET.Test.Parameters;

public class ExplicitCastTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db)
{
[Fact]
public void CastWithFunction()
{
using var command = Connection.CreateCommand();
command.CommandText = "SELECT strptime($date_as_text, '%Y-%m-%d %H:%M:%S') AS example;";
command.Parameters.Add(new DuckDBParameter("date_as_text", "2023-04-02 01:01:00"));
var scalar = command.ExecuteScalar();

scalar.Should().BeOfType<DateTime>();
}

[Fact]
public void CastWithExplicitCastToTimestamp()
{
using var command = Connection.CreateCommand();
command.CommandText = "SELECT $date_as_text::timestamp AS example;";
command.Parameters.Add(new DuckDBParameter("date_as_text", "2023-04-02 01:01:00"));
var scalar = command.ExecuteScalar();

scalar.Should().BeOfType<DateTime>();
}

[Fact]
public void CastWithExplicitCastFromInt()
{
using var command = Connection.CreateCommand();
command.CommandText = "SELECT $my_num::varchar AS example;";
command.Parameters.Add(new DuckDBParameter("my_num", 42));
var scalar = command.ExecuteScalar();

scalar.Should().BeOfType<string>();
scalar.Should().Be("42");
}

[Fact]
public void CastWithExplicitCastToInt()
{
using var command = Connection.CreateCommand();
command.CommandText = "SELECT $my_num::int AS example;";
command.Parameters.Add(new DuckDBParameter("my_num", "42"));
var scalar = command.ExecuteScalar();

scalar.Should().BeOfType<int>();
scalar.Should().Be(42);
}

[Fact]
public void CastWithExplicitCastToIntWrong()
{
using var command = Connection.CreateCommand();
command.CommandText = "SELECT $my_num::int AS example;";
command.Parameters.Add(new DuckDBParameter("my_num", "Giorgi"));

command.Invoking(c => c.ExecuteScalar())
.Should().Throw<DuckDBException>().WithMessage("*Conversion Error*");
}
}
2 changes: 1 addition & 1 deletion DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public void BindMultipleValuesInvalidOrderTest(string queryStatement)
Command.Parameters.Clear();
Command.Parameters.Add(new DuckDBParameter(42));
Command.Parameters.Add(new DuckDBParameter("hello"));
Command.Invoking(cmd => cmd.ExecuteNonQuery()).Should().ThrowExactly<InvalidOperationException>();
Command.Invoking(cmd => cmd.ExecuteNonQuery()).Should().ThrowExactly<DuckDBException>();
}

[Theory]
Expand Down

0 comments on commit 6025a67

Please sign in to comment.