diff --git a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.PreparedStatements.cs b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.PreparedStatements.cs index 68f20cf3..120ff571 100644 --- a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.PreparedStatements.cs +++ b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.PreparedStatements.cs @@ -62,6 +62,10 @@ public static partial class PreparedStatements [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial DuckDBState DuckDBBindHugeInt(DuckDBPreparedStatement preparedStatement, long index, DuckDBHugeInt val); + [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_bind_decimal")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial DuckDBState DuckDBBindDecimal(DuckDBPreparedStatement preparedStatement, long index, DuckDBDecimal val); + [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_bind_uint8")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial DuckDBState DuckDBBindUInt8(DuckDBPreparedStatement preparedStatement, long index, byte val); @@ -98,6 +102,14 @@ public static partial class PreparedStatements [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial DuckDBState DuckDBBindTimestamp(DuckDBPreparedStatement preparedStatement, long index, DuckDBTimestampStruct val); + [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_bind_timestamp_tz")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial DuckDBState DuckDBBindTimestampTz(DuckDBPreparedStatement preparedStatement, long index, DuckDBTimestampStruct val); + + [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_bind_interval")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + public static partial DuckDBState DuckDBBindInterval(DuckDBPreparedStatement preparedStatement, long index, DuckDBInterval val); + [LibraryImport(DuckDbLibrary, EntryPoint = "duckdb_bind_varchar", StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial DuckDBState DuckDBBindVarchar(DuckDBPreparedStatement preparedStatement, long index, string val); diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index 321fdd2a..8cb2440a 100644 --- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs +++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs @@ -57,15 +57,15 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log { (DuckDBType.Boolean, bool value) => NativeMethods.Value.DuckDBCreateBool(value), - (DuckDBType.TinyInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateInt8(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), - (DuckDBType.SmallInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateInt16(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), - (DuckDBType.Integer, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateInt32(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), - (DuckDBType.BigInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateInt64(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), + (DuckDBType.TinyInt, _) => TryConvertTo(item, out sbyte result) ? NativeMethods.Value.DuckDBCreateInt8(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), + (DuckDBType.SmallInt, _) => TryConvertTo(item, out short result) ? NativeMethods.Value.DuckDBCreateInt16(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), + (DuckDBType.Integer, _) => TryConvertTo(item, out int result) ? NativeMethods.Value.DuckDBCreateInt32(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), + (DuckDBType.BigInt, _) => TryConvertTo(item, out long result) ? NativeMethods.Value.DuckDBCreateInt64(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), - (DuckDBType.UnsignedTinyInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateUInt8(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), - (DuckDBType.UnsignedSmallInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateUInt16(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), - (DuckDBType.UnsignedInteger, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateUInt32(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), - (DuckDBType.UnsignedBigInt, _) => TryConvertTo(out var result) ? NativeMethods.Value.DuckDBCreateUInt64(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), + (DuckDBType.UnsignedTinyInt, _) => TryConvertTo(item, out byte result) ? NativeMethods.Value.DuckDBCreateUInt8(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), + (DuckDBType.UnsignedSmallInt, _) => TryConvertTo(item, out ushort result) ? NativeMethods.Value.DuckDBCreateUInt16(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), + (DuckDBType.UnsignedInteger, _) => TryConvertTo(item, out uint result) ? NativeMethods.Value.DuckDBCreateUInt32(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), + (DuckDBType.UnsignedBigInt, _) => TryConvertTo(item, out ulong result) ? NativeMethods.Value.DuckDBCreateUInt64(result) : NativeMethods.Value.DuckDBCreateVarchar(item.ToString()), (DuckDBType.Float, float value) => NativeMethods.Value.DuckDBCreateFloat(value), (DuckDBType.Double, double value) => NativeMethods.Value.DuckDBCreateDouble(value), @@ -97,25 +97,212 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log _ when ValueCreators.TryGetValue(dbType, out var converter) => converter(item), _ => NativeMethods.Value.DuckDBCreateVarchar(item.ToString()) }; + } - bool TryConvertTo(out T result) where T : struct + public static bool TryBindScalarValue(this object? item, DuckDBPreparedStatement statement, long index, DuckDBType duckDBType, DbType dbType, out DuckDBState result) + { + if (item.IsNull()) { - try - { - if (item is T parsable) - { - result = parsable; - return true; - } + result = NativeMethods.PreparedStatements.DuckDBBindNull(statement, index); + return true; + } - result = (T)Convert.ChangeType(item, typeof(T)); + switch (duckDBType, item) + { + case (DuckDBType.Boolean, bool booleanValue): + result = NativeMethods.PreparedStatements.DuckDBBindBoolean(statement, index, booleanValue); return true; - } - catch (Exception) - { + + case (DuckDBType.TinyInt, _): + result = TryConvertTo(item, out sbyte int8Value) + ? NativeMethods.PreparedStatements.DuckDBBindInt8(statement, index, int8Value) + : NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, item.ToString()!); + return true; + case (DuckDBType.SmallInt, _): + result = TryConvertTo(item, out short int16Value) + ? NativeMethods.PreparedStatements.DuckDBBindInt16(statement, index, int16Value) + : NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, item.ToString()!); + return true; + case (DuckDBType.Integer, _): + result = TryConvertTo(item, out int int32Value) + ? NativeMethods.PreparedStatements.DuckDBBindInt32(statement, index, int32Value) + : NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, item.ToString()!); + return true; + case (DuckDBType.BigInt, _): + result = TryConvertTo(item, out long int64Value) + ? NativeMethods.PreparedStatements.DuckDBBindInt64(statement, index, int64Value) + : NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, item.ToString()!); + return true; + + case (DuckDBType.UnsignedTinyInt, _): + result = TryConvertTo(item, out byte uint8Value) + ? NativeMethods.PreparedStatements.DuckDBBindUInt8(statement, index, uint8Value) + : NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, item.ToString()!); + return true; + case (DuckDBType.UnsignedSmallInt, _): + result = TryConvertTo(item, out ushort uint16Value) + ? NativeMethods.PreparedStatements.DuckDBBindUInt16(statement, index, uint16Value) + : NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, item.ToString()!); + return true; + case (DuckDBType.UnsignedInteger, _): + result = TryConvertTo(item, out uint uint32Value) + ? NativeMethods.PreparedStatements.DuckDBBindUInt32(statement, index, uint32Value) + : NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, item.ToString()!); + return true; + case (DuckDBType.UnsignedBigInt, _): + result = TryConvertTo(item, out ulong uint64Value) + ? NativeMethods.PreparedStatements.DuckDBBindUInt64(statement, index, uint64Value) + : NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, item.ToString()!); + return true; + + case (DuckDBType.Float, float floatValue): + result = NativeMethods.PreparedStatements.DuckDBBindFloat(statement, index, floatValue); + return true; + case (DuckDBType.Double, double doubleValue): + result = NativeMethods.PreparedStatements.DuckDBBindDouble(statement, index, doubleValue); + return true; + case (DuckDBType.Decimal, decimal decimalValue): + result = NativeMethods.PreparedStatements.DuckDBBindDecimal(statement, index, ToDuckDBDecimal(decimalValue)); + return true; + case (DuckDBType.HugeInt, BigInteger hugeIntValue): + result = NativeMethods.PreparedStatements.DuckDBBindHugeInt(statement, index, new DuckDBHugeInt(hugeIntValue)); + return true; + + case (DuckDBType.Varchar, string stringValue): + result = NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, stringValue); + return true; + case (DuckDBType.Timestamp, DateTime timestampValue): + result = NativeMethods.PreparedStatements.DuckDBBindTimestamp(statement, index, timestampValue.ToTimestampStruct(duckDBType)); + return true; + case (DuckDBType.TimestampTz, DateTime timestampTzValue): + result = NativeMethods.PreparedStatements.DuckDBBindTimestampTz(statement, index, timestampTzValue.ToTimestampStruct(duckDBType)); + return true; + case (DuckDBType.TimestampTz, DateTimeOffset timestampOffsetValue): + result = NativeMethods.PreparedStatements.DuckDBBindTimestampTz(statement, index, timestampOffsetValue.ToTimestampStruct()); + return true; + case (DuckDBType.Interval, TimeSpan intervalValue): + result = NativeMethods.PreparedStatements.DuckDBBindInterval(statement, index, intervalValue); + return true; + case (DuckDBType.Date, DateTime dateTimeValue): + result = NativeMethods.PreparedStatements.DuckDBBindDate(statement, index, ((DuckDBDateOnly)dateTimeValue).ToDuckDBDate()); + return true; + case (DuckDBType.Date, DuckDBDateOnly duckDBDateValue): + result = NativeMethods.PreparedStatements.DuckDBBindDate(statement, index, duckDBDateValue.ToDuckDBDate()); + return true; + case (DuckDBType.Date, DateOnly dateOnlyValue): + result = NativeMethods.PreparedStatements.DuckDBBindDate(statement, index, ((DuckDBDateOnly)dateOnlyValue).ToDuckDBDate()); + return true; + case (DuckDBType.Time, DateTime timeDateTimeValue): + result = NativeMethods.PreparedStatements.DuckDBBindTime(statement, index, NativeMethods.DateTimeHelpers.DuckDBToTime((DuckDBTimeOnly)timeDateTimeValue)); + return true; + case (DuckDBType.Time, DuckDBTimeOnly duckDBTimeValue): + result = NativeMethods.PreparedStatements.DuckDBBindTime(statement, index, NativeMethods.DateTimeHelpers.DuckDBToTime(duckDBTimeValue)); + return true; + case (DuckDBType.Time, TimeOnly timeOnlyValue): + result = NativeMethods.PreparedStatements.DuckDBBindTime(statement, index, NativeMethods.DateTimeHelpers.DuckDBToTime(timeOnlyValue)); + return true; + case (DuckDBType.Blob, byte[] blobValue): + result = NativeMethods.PreparedStatements.DuckDBBindBlob(statement, index, blobValue, blobValue.LongLength); + return true; + } + + // These logical types need the exact duckdb_value representation because the scalar C API + // has no equivalent binder, or because its timestamp unit differs from duckdb_bind_timestamp. + if (item is Guid || + item is DateTime && duckDBType is DuckDBType.TimestampS or DuckDBType.TimestampMs or DuckDBType.TimestampNs || + item is DateTimeOffset && duckDBType == DuckDBType.TimeTz || + item is ICollection && (item is not byte[] || duckDBType is DuckDBType.List or DuckDBType.Array)) + { + result = default; + return false; + } + + switch (dbType) + { + case DbType.Currency: + result = NativeMethods.PreparedStatements.DuckDBBindDecimal(statement, index, ToDuckDBDecimal((decimal)item)); + return true; + case DbType.Boolean: + result = NativeMethods.PreparedStatements.DuckDBBindBoolean(statement, index, (bool)item); + return true; + case DbType.SByte: + result = NativeMethods.PreparedStatements.DuckDBBindInt8(statement, index, (sbyte)item); + return true; + case DbType.Int16: + result = NativeMethods.PreparedStatements.DuckDBBindInt16(statement, index, (short)item); + return true; + case DbType.Int32: + result = NativeMethods.PreparedStatements.DuckDBBindInt32(statement, index, (int)item); + return true; + case DbType.Int64: + result = NativeMethods.PreparedStatements.DuckDBBindInt64(statement, index, (long)item); + return true; + case DbType.Byte: + result = NativeMethods.PreparedStatements.DuckDBBindUInt8(statement, index, (byte)item); + return true; + case DbType.UInt16: + result = NativeMethods.PreparedStatements.DuckDBBindUInt16(statement, index, (ushort)item); + return true; + case DbType.UInt32: + result = NativeMethods.PreparedStatements.DuckDBBindUInt32(statement, index, (uint)item); + return true; + case DbType.UInt64: + result = NativeMethods.PreparedStatements.DuckDBBindUInt64(statement, index, (ulong)item); + return true; + case DbType.Single: + result = NativeMethods.PreparedStatements.DuckDBBindFloat(statement, index, (float)item); + return true; + case DbType.Double: + result = NativeMethods.PreparedStatements.DuckDBBindDouble(statement, index, (double)item); + return true; + case DbType.String: + result = NativeMethods.PreparedStatements.DuckDBBindVarchar(statement, index, (string)item); + return true; + case DbType.VarNumeric: + result = NativeMethods.PreparedStatements.DuckDBBindHugeInt(statement, index, new DuckDBHugeInt((BigInteger)item)); + return true; + case DbType.Binary: + var bytes = (byte[])item; + result = NativeMethods.PreparedStatements.DuckDBBindBlob(statement, index, bytes, bytes.LongLength); + return true; + case DbType.Date: + var date = (item is DateOnly dateOnly ? (DuckDBDateOnly)dateOnly : (DuckDBDateOnly)item).ToDuckDBDate(); + result = NativeMethods.PreparedStatements.DuckDBBindDate(statement, index, date); + return true; + case DbType.Time: + var time = NativeMethods.DateTimeHelpers.DuckDBToTime(item is TimeOnly timeOnly ? (DuckDBTimeOnly)timeOnly : (DuckDBTimeOnly)item); + result = NativeMethods.PreparedStatements.DuckDBBindTime(statement, index, time); + return true; + case DbType.DateTime: + var timestamp = (item is DateTime dateTime ? (DuckDBTimestamp)dateTime : (DuckDBTimestamp)item).ToDuckDBTimestampStruct(); + result = NativeMethods.PreparedStatements.DuckDBBindTimestamp(statement, index, timestamp); + return true; + case DbType.DateTimeOffset: + result = NativeMethods.PreparedStatements.DuckDBBindTimestampTz(statement, index, ((DateTimeOffset)item).ToTimestampStruct()); + return true; + default: result = default; return false; + } + } + + private static bool TryConvertTo(object item, out T result) where T : struct + { + try + { + if (item is T parsable) + { + result = parsable; + return true; } + + result = (T)Convert.ChangeType(item, typeof(T)); + return true; + } + catch (Exception) + { + result = default; + return false; } } @@ -160,6 +347,9 @@ private static DuckDBValue[] BuildValues(DuckDBLogicalType childType, ICollectio } private static DuckDBValue DecimalToDuckDBValue(decimal value) + => NativeMethods.Value.DuckDBCreateDecimal(ToDuckDBDecimal(value)); + + private static DuckDBDecimal ToDuckDBDecimal(decimal value) { var mantissa = value.GetMantissa(); @@ -167,6 +357,6 @@ private static DuckDBValue DecimalToDuckDBValue(decimal value) ? value.Scale + 1 : Math.Max((int)BigInteger.Log10(BigInteger.Abs(mantissa)) + 1, value.Scale + 1); - return NativeMethods.Value.DuckDBCreateDecimal(new DuckDBDecimal((byte)width, value.Scale, new DuckDBHugeInt(mantissa))); + return new DuckDBDecimal((byte)width, value.Scale, new DuckDBHugeInt(mantissa)); } } diff --git a/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs b/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs index 4f7d559d..1b985af4 100644 --- a/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs +++ b/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs @@ -130,9 +130,12 @@ private static void BindParameter(DuckDBPreparedStatement preparedStatement, lon using var parameterLogicalType = NativeMethods.PreparedStatements.DuckDBParamLogicalType(preparedStatement, index); var duckDBType = NativeMethods.LogicalType.DuckDBGetTypeId(parameterLogicalType); - using var duckDBValue = parameter.Value.ToDuckDBValue(parameterLogicalType, duckDBType, parameter.DbType); - - var result = NativeMethods.PreparedStatements.DuckDBBindValue(preparedStatement, index, duckDBValue); + DuckDBState result; + if (!parameter.Value.TryBindScalarValue(preparedStatement, index, duckDBType, parameter.DbType, out result)) + { + using var duckDBValue = parameter.Value.ToDuckDBValue(parameterLogicalType, duckDBType, parameter.DbType); + result = NativeMethods.PreparedStatements.DuckDBBindValue(preparedStatement, index, duckDBValue); + } if (!result.IsSuccess()) { @@ -145,4 +148,4 @@ public void Dispose() { statement.Dispose(); } -} \ No newline at end of file +} diff --git a/DuckDB.NET.Test/Parameters/ListParameterTests.cs b/DuckDB.NET.Test/Parameters/ListParameterTests.cs index 4fa0f307..011ed0d3 100644 --- a/DuckDB.NET.Test/Parameters/ListParameterTests.cs +++ b/DuckDB.NET.Test/Parameters/ListParameterTests.cs @@ -78,6 +78,22 @@ public void CanBindByteList() TestInsertSelect("UTinyInt", faker => faker.Random.Byte().OrNull(faker)); } + [Theory] + [InlineData("UTINYINT[]")] + [InlineData("UTINYINT[3]")] + public void CanBindByteArrayAsCollection(string duckDbType) + { + byte[] expected = [1, 2, byte.MaxValue]; + + Command.CommandText = $"SELECT ?::{duckDbType};"; + Command.Parameters.Add(new DuckDBParameter(expected)); + + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue>(0).Should().Equal(expected); + } + [Fact] public void CanBindUShortList() { @@ -189,4 +205,4 @@ public void CanBindTimeOnlyList() return new TimeOnly(dateTime.TimeOfDay.Ticks - dateTime.TimeOfDay.Ticks % 10); }); } -} \ No newline at end of file +}