Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ internal static class ClrToDuckDBConverter
{
{ DbType.Guid, value => NativeMethods.Value.DuckDBCreateUuid(((Guid)value).ToHugeInt(false)) },
{ DbType.Currency, value => DecimalToDuckDBValue((decimal)value) },
// Decimal and DateTime2 had no converter: any other value keeps the fallback below, as before.
{ DbType.Decimal, value => value is decimal number ? DecimalToDuckDBValue(number) : NativeMethods.Value.DuckDBCreateVarchar(value.ToString()) },
{ DbType.Boolean, value => NativeMethods.Value.DuckDBCreateBool((bool)value) },
{ DbType.SByte, value => NativeMethods.Value.DuckDBCreateInt8((sbyte)value) },
{ DbType.Int16, value => NativeMethods.Value.DuckDBCreateInt16((short)value) },
Expand Down Expand Up @@ -43,12 +45,8 @@ internal static class ClrToDuckDBConverter
return NativeMethods.Value.DuckDBCreateTime(time);
}
},
{ DbType.DateTime, value =>
{
var dateTime = value is DateTime dt ? dt.ToTimestampStruct(DuckDBType.Timestamp) : ((DuckDBTimestamp)value).ToDuckDBTimestampStruct();
return NativeMethods.Value.DuckDBCreateTimestamp(dateTime);
}
},
{ DbType.DateTime, TimestampToDuckDBValue },
{ DbType.DateTime2, value => value is DateTime or DuckDBTimestamp ? TimestampToDuckDBValue(value) : NativeMethods.Value.DuckDBCreateVarchar(value.ToString()) },
{ DbType.DateTimeOffset, value => NativeMethods.Value.DuckDBCreateTimestampTz(((DateTimeOffset)value).ToTimestampStruct()) },
};

Expand Down Expand Up @@ -227,6 +225,12 @@ private static unsafe DuckDBValue BigIntegerToDuckDBValue(BigInteger value)
}
}

private static DuckDBValue TimestampToDuckDBValue(object value)
{
var dateTime = value is DateTime dt ? dt.ToTimestampStruct(DuckDBType.Timestamp) : ((DuckDBTimestamp)value).ToDuckDBTimestampStruct();
return NativeMethods.Value.DuckDBCreateTimestamp(dateTime);
}

private static DuckDBValue DecimalToDuckDBValue(decimal value)
{
// The value goes to DuckDB as it is in the decimal: the integer, with the scale of the decimal. The writer of
Expand Down
2 changes: 1 addition & 1 deletion DuckDB.NET.Data/PreparedStatement/DuckDBTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal static class DuckDBTypeMap
{typeof(double), DbType.Double},
{typeof(string), DbType.String},
{typeof(Guid), DbType.Guid},
{typeof(decimal), DbType.Currency},
{typeof(decimal), DbType.Decimal},
{typeof(byte), DbType.Byte},
{typeof(ushort), DbType.UInt16},
{typeof(uint), DbType.UInt32},
Expand Down
54 changes: 54 additions & 0 deletions DuckDB.NET.Test/Parameters/DecimalParameterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -456,4 +456,58 @@ public void ParameterWithMoreDigitsThanItsColumnIsRefused()
Command.CommandText = "SELECT list(value ORDER BY rowid)::VARCHAR FROM DecimalParameterRefused";
Command.ExecuteScalar().Should().Be("[999, -999, 999]");
}

// ADO.NET infers DbType.Decimal for a decimal value, and GetSchema("DataTypes") maps DECIMAL to System.Decimal.
[Fact]
public void DecimalValueInfersDbTypeDecimal()
{
new DuckDBParameter(1.5m).DbType.Should().Be(DbType.Decimal);
}

// DbType.Decimal had no converter, so a parameter that set it was sent as decimal.ToString() in the current
// culture: "1234,5" under fr-FR, stored as such in a VARCHAR column and refused by a DOUBLE one.
[Theory]
[InlineData(DbType.Decimal)]
[InlineData(DbType.Currency)]
public void ExplicitDecimalDbTypeIsSentAsADecimalInAnyCulture(DbType dbType)
{
var defaultCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
try
{
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");

Command.Parameters.Clear();
Command.CommandText = "SELECT ?";
Command.Parameters.Add(new DuckDBParameter(1234.5m) { DbType = dbType });
Command.ExecuteScalar().Should().BeOfType<decimal>().Subject.Should().Be(1234.5m);

Command.CommandText = "CREATE OR REPLACE TABLE DecimalDbTypeTests (text VARCHAR, number DOUBLE)";
Command.Parameters.Clear();
Command.ExecuteNonQuery();

Command.CommandText = "INSERT INTO DecimalDbTypeTests VALUES (?, ?)";
Command.Parameters.Add(new DuckDBParameter(1234.5m) { DbType = dbType });
Command.Parameters.Add(new DuckDBParameter(1234.5m) { DbType = dbType });
Command.ExecuteNonQuery();

Command.Parameters.Clear();
Command.CommandText = "SELECT text || ' ' || number::VARCHAR FROM DecimalDbTypeTests";
Command.ExecuteScalar().Should().Be("1234.5 1234.5");
}
finally
{
System.Threading.Thread.CurrentThread.CurrentCulture = defaultCulture;
}
}

// Other values with DbType.Decimal are still sent as before.
[Fact]
public void ExplicitDecimalDbTypeWithAnIntegerValueIsStillSent()
{
Command.CommandText = "SELECT ?::DECIMAL(4,1)";
Command.Parameters.Clear();
Command.Parameters.Add(new DuckDBParameter(5) { DbType = DbType.Decimal });

Command.ExecuteScalar().Should().Be(5.0m);
}
}
32 changes: 32 additions & 0 deletions DuckDB.NET.Test/Parameters/TimestampTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,38 @@ public void BindParameterWithoutTable_DateTimeOffset()
result.Should().Be(value);
}

// DbType.DateTime2 had no converter, so the value was sent as DateTime.ToString() in the current culture:
// text a statement could misread or refuse, and a VARCHAR column stored as such.
[Fact]
public void ExplicitDateTime2DbTypeIsSentAsATimestampInAnyCulture()
{
var value = new DateTime(2026, 10, 18, 13, 45, 30, 123);
var defaultCulture = System.Threading.Thread.CurrentThread.CurrentCulture;
try
{
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("de-DE");

Command.CommandText = "SELECT ?;";
Command.Parameters.Add(new DuckDBParameter(value) { DbType = DbType.DateTime2 });

Command.ExecuteScalar().Should().BeOfType<DateTime>().Subject.Should().Be(value);
}
finally
{
System.Threading.Thread.CurrentThread.CurrentCulture = defaultCulture;
}
}

// Other values with DbType.DateTime2 are still sent as before.
[Fact]
public void ExplicitDateTime2DbTypeWithATextValueIsStillSent()
{
Command.CommandText = "SELECT ?::TIMESTAMP;";
Command.Parameters.Add(new DuckDBParameter("2026-10-18 13:45:30") { DbType = DbType.DateTime2 });

Command.ExecuteScalar().Should().Be(new DateTime(2026, 10, 18, 13, 45, 30));
}

public static TimeOnly Trim(TimeOnly date, long ticks) => new(date.Ticks - date.Ticks % ticks);

public static DateTime Trim(DateTime date, long ticks) => new(date.Ticks - date.Ticks % ticks, date.Kind);
Expand Down
Loading