diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index b8345d5..4cbec3c 100644 --- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs +++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs @@ -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(); } @@ -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()), - (DuckDBType.SmallInt, _) => NativeMethods.Value.DuckDBCreateInt16(ConvertTo()), - (DuckDBType.Integer, _) => NativeMethods.Value.DuckDBCreateInt32(ConvertTo()), - (DuckDBType.BigInt, _) => NativeMethods.Value.DuckDBCreateInt64(ConvertTo()), + (DuckDBType.TinyInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateInt8(result) : StringToDuckDBValue(item.ToString()), + (DuckDBType.SmallInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateInt16(result) : StringToDuckDBValue(item.ToString()), + (DuckDBType.Integer, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateInt32(result) : StringToDuckDBValue(item.ToString()), + (DuckDBType.BigInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateInt64(result) : StringToDuckDBValue(item.ToString()), - (DuckDBType.UnsignedTinyInt, _) => NativeMethods.Value.DuckDBCreateUInt8(ConvertTo()), - (DuckDBType.UnsignedSmallInt, _) => NativeMethods.Value.DuckDBCreateUInt16(ConvertTo()), - (DuckDBType.UnsignedInteger, _) => NativeMethods.Value.DuckDBCreateUInt32(ConvertTo()), - (DuckDBType.UnsignedBigInt, _) => NativeMethods.Value.DuckDBCreateUInt64(ConvertTo()), + (DuckDBType.UnsignedTinyInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateUInt8(result) : StringToDuckDBValue(item.ToString()), + (DuckDBType.UnsignedSmallInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateUInt16(result) : StringToDuckDBValue(item.ToString()), + (DuckDBType.UnsignedInteger, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateUInt32(result) : StringToDuckDBValue(item.ToString()), + (DuckDBType.UnsignedBigInt, _) => TryConvertTo(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), @@ -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() + bool TryConvertTo(out T result) where T : struct +#if NET8_0_OR_GREATER + , IParsable? +#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; } } } @@ -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); diff --git a/DuckDB.NET.Test/Parameters/ExplicitCastTests.cs b/DuckDB.NET.Test/Parameters/ExplicitCastTests.cs new file mode 100644 index 0000000..647ed5d --- /dev/null +++ b/DuckDB.NET.Test/Parameters/ExplicitCastTests.cs @@ -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(); + } + + [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(); + } + + [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(); + 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(); + 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().WithMessage("*Conversion Error*"); + } +} \ No newline at end of file diff --git a/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs b/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs index c29cca3..9f3beae 100644 --- a/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs +++ b/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs @@ -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(); + Command.Invoking(cmd => cmd.ExecuteNonQuery()).Should().ThrowExactly(); } [Theory]