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
41 changes: 34 additions & 7 deletions DuckDB.NET.Data/DuckDBParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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("")]
Expand All @@ -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);
}
}
}
}
Expand All @@ -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)
{
}

Expand All @@ -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;
}
}
89 changes: 89 additions & 0 deletions DuckDB.NET.Test/Parameters/ParameterDbTypeTests.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading