From 5fa76e5c02e0cf6c98de13f46101d4ecc3f490f2 Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Sun, 18 Oct 2026 09:31:32 -0500 Subject: [PATCH] Keep a DbType that was set explicitly when Value is assigned afterwards DuckDBParameter.Value replaced DbType with the type inferred from the new value every time it was set, so "parameter.DbType = DbType.Time; parameter.Value = timeSpan" lost the Time. The converter then saw a TimeSpan with no DbType and bound it as an INTERVAL (the behavior added for a bare "?"), which DuckDB cannot cast to TIME where it has no target column to infer from, such as a MERGE source: MERGE INTO t USING (SELECT $1 AS id, $2 AS v) s ON t.id = s.id ... INSERT (id, v) VALUES (s.id, s.v) -- Conversion Error: Unimplemented type for cast (INTERVAL -> TIME) when casting from source column v The same applied to every explicit DbType followed by a Value assignment, which is the usual order for a provider-neutral caller (CreateParameter, set DbType, set Value). A DbType that was assigned now stays; until one is assigned, and after ResetDbType, the type still follows the value, as it does for SqlParameter and NpgsqlParameter. The constructors that take a value without a type still infer it, and the ones that take a DbType keep it. Found while testing for a release of https://github.com/pengdows/pengdows.crud Co-Authored-By: Claude Sonnet 5.5 --- DuckDB.NET.Data/DuckDBParameter.cs | 41 +++++++-- .../Parameters/ParameterDbTypeTests.cs | 89 +++++++++++++++++++ 2 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 DuckDB.NET.Test/Parameters/ParameterDbTypeTests.cs diff --git a/DuckDB.NET.Data/DuckDBParameter.cs b/DuckDB.NET.Data/DuckDBParameter.cs index 7ace2d50..b23bd864 100644 --- a/DuckDB.NET.Data/DuckDBParameter.cs +++ b/DuckDB.NET.Data/DuckDBParameter.cs @@ -9,8 +9,21 @@ public class DuckDBParameter : DbParameter private const DbType DefaultDbType = DbType.String; private object? value; + private DbType dbType = DefaultDbType; - public override DbType DbType { get; set; } + // Once a DbType is assigned it stays: assigning Value afterwards must not replace it with the type inferred from the value. + // Until then (and after ResetDbType) the type follows the value, as it does for SqlParameter and NpgsqlParameter. + private bool inferDbType = true; + + public override DbType DbType + { + get => dbType; + set + { + dbType = value; + inferDbType = false; + } + } [AllowNull] [DefaultValue("")] @@ -24,7 +37,11 @@ public override object? Value if (this.value != value) { this.value = value; - DbType = DuckDBTypeMap.GetDbTypeForValue(value); + + if (inferDbType) + { + dbType = DuckDBTypeMap.GetDbTypeForValue(value); + } } } } @@ -39,11 +56,11 @@ public override object? Value public override int Size { get; set; } public DuckDBParameter() - : this (string.Empty, DefaultDbType, null) + : this(string.Empty, DefaultDbType, null, inferDbType: true) { } public DuckDBParameter(object value) - : this(string.Empty, DuckDBTypeMap.GetDbTypeForValue(value), value) + : this(string.Empty, DuckDBTypeMap.GetDbTypeForValue(value), value, inferDbType: true) { } @@ -53,17 +70,27 @@ public DuckDBParameter(DbType type, object? value) } public DuckDBParameter(string name, object? value) - : this(name, DuckDBTypeMap.GetDbTypeForValue(value), value) + : this(name, DuckDBTypeMap.GetDbTypeForValue(value), value, inferDbType: true) { } public DuckDBParameter(string name, DbType type, object? value) + : this(name, type, value, inferDbType: false) + { + } + + private DuckDBParameter(string name, DbType type, object? value, bool inferDbType) { - DbType = type; + dbType = type; + this.inferDbType = inferDbType; ParameterName = name; this.value = value; SourceColumn = string.Empty; } - public override void ResetDbType() => DbType = DefaultDbType; + public override void ResetDbType() + { + dbType = DefaultDbType; + inferDbType = true; + } } \ No newline at end of file diff --git a/DuckDB.NET.Test/Parameters/ParameterDbTypeTests.cs b/DuckDB.NET.Test/Parameters/ParameterDbTypeTests.cs new file mode 100644 index 00000000..8582db68 --- /dev/null +++ b/DuckDB.NET.Test/Parameters/ParameterDbTypeTests.cs @@ -0,0 +1,89 @@ +namespace DuckDB.NET.Test.Parameters; + +public class ParameterDbTypeTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) +{ + [Fact] + public void DbTypeSetBeforeTheValueIsKept() + { + var parameter = new DuckDBParameter { DbType = DbType.Time }; + + parameter.Value = new TimeSpan(13, 45, 30); + + parameter.DbType.Should().Be(DbType.Time); + } + + [Fact] + public void DbTypeSetAfterTheValueIsKept() + { + var parameter = new DuckDBParameter { Value = new TimeSpan(13, 45, 30) }; + + parameter.DbType = DbType.Time; + parameter.Value = new TimeSpan(1, 2, 3); + + parameter.DbType.Should().Be(DbType.Time); + } + + [Fact] + public void DbTypeIsInferredFromTheValueWhenNoneWasSet() + { + var parameter = new DuckDBParameter(); + + parameter.Value = 5; + parameter.DbType.Should().Be(DbType.Int32); + + parameter.Value = "text"; + parameter.DbType.Should().Be(DbType.String); + + parameter.Value = 5L; + parameter.DbType.Should().Be(DbType.Int64); + } + + [Fact] + public void ResetDbTypeReturnsToInference() + { + var parameter = new DuckDBParameter { DbType = DbType.Time }; + + parameter.ResetDbType(); + parameter.Value = 5; + + parameter.DbType.Should().Be(DbType.Int32); + } + + [Fact] + public void ConstructorsThatTakeAValueStillInferItsType() + { + new DuckDBParameter(5).DbType.Should().Be(DbType.Int32); + new DuckDBParameter("name", "text").DbType.Should().Be(DbType.String); + + var typed = new DuckDBParameter(DbType.Int64, 5); + typed.Value = "text"; + typed.DbType.Should().Be(DbType.Int64); + } + + [Fact] + public void ATimeSpanWithTheTimeDbTypeIsBoundAsATimeInAMergeSource() + { + Command.CommandText = "CREATE OR REPLACE TABLE parameter_dbtype_time (id INTEGER PRIMARY KEY, v TIME)"; + Command.ExecuteNonQuery(); + + // A MERGE source's SELECT gives DuckDB no target column to infer the parameter's type from. + Command.CommandText = "MERGE INTO parameter_dbtype_time t USING (SELECT $1 AS id, $2 AS v) s ON t.id = s.id " + + "WHEN MATCHED THEN UPDATE SET v = s.v WHEN NOT MATCHED THEN INSERT (id, v) VALUES (s.id, s.v)"; + Command.Parameters.Add(new DuckDBParameter { Value = 1 }); + Command.Parameters.Add(new DuckDBParameter { DbType = DbType.Time, Value = new TimeSpan(13, 45, 30) }); + Command.ExecuteNonQuery(); + + Command.Parameters.Clear(); + Command.CommandText = "SELECT v FROM parameter_dbtype_time WHERE id = 1"; + Command.ExecuteScalar().Should().Be(new TimeOnly(13, 45, 30)); + } + + [Fact] + public void ATimeSpanWithoutADbTypeIsStillBoundAsAnInterval() + { + Command.CommandText = "SELECT ?"; + Command.Parameters.Add(new DuckDBParameter { Value = new TimeSpan(3, 4, 5, 6) }); + + Command.ExecuteScalar().Should().Be(new TimeSpan(3, 4, 5, 6)); + } +}