From c500c1ad7b5f7684216f9605c14e513aff25a5e1 Mon Sep 17 00:00:00 2001 From: Giorgi Date: Tue, 26 Sep 2023 21:29:32 +0400 Subject: [PATCH] Support named parameters --- .../NativeMethods.PreparedStatements.cs | 3 + DuckDB.NET.Data/Internal/PreparedStatement.cs | 25 +++++- DuckDB.NET.Test/Helpers/Defer.cs | 4 +- .../Parameters/ParameterCollectionTests.cs | 86 ++++++++++++++----- 4 files changed, 89 insertions(+), 29 deletions(-) diff --git a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.PreparedStatements.cs b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.PreparedStatements.cs index 5ac11c1..6befabb 100644 --- a/DuckDB.NET.Bindings/NativeMethods/NativeMethods.PreparedStatements.cs +++ b/DuckDB.NET.Bindings/NativeMethods/NativeMethods.PreparedStatements.cs @@ -22,6 +22,9 @@ public static class PreparedStatements [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_nparams")] public static extern long DuckDBParams(DuckDBPreparedStatement preparedStatement); + [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_bind_parameter_index")] + public static extern DuckDBState DuckDBBindParameterIndex(DuckDBPreparedStatement preparedStatement, out int index, string name); + [DllImport(DuckDbLibrary, CallingConvention = CallingConvention.Cdecl, EntryPoint = "duckdb_bind_boolean")] public static extern DuckDBState DuckDBBindBoolean(DuckDBPreparedStatement preparedStatement, long index, bool val); diff --git a/DuckDB.NET.Data/Internal/PreparedStatement.cs b/DuckDB.NET.Data/Internal/PreparedStatement.cs index e4cd8fa..bfde50b 100644 --- a/DuckDB.NET.Data/Internal/PreparedStatement.cs +++ b/DuckDB.NET.Data/Internal/PreparedStatement.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Globalization; +using System.Linq; using System.Numerics; using System.Runtime.ExceptionServices; using DuckDB.NET.Data.Extensions; @@ -146,10 +147,28 @@ private static void BindParameters(DuckDBPreparedStatement preparedStatement, Du throw new InvalidOperationException($"Invalid number of parameters. Expected {expectedParameters}, got {parameterCollection.Count}"); } - for (var i = 0; i < parameterCollection.Count; ++i) + if (parameterCollection.OfType().Any(p => !string.IsNullOrEmpty(p.ParameterName))) { - var param = parameterCollection[i]; - BindParameter(preparedStatement, i + 1, param); + foreach (DuckDBParameter param in parameterCollection) + { + var state = NativeMethods.PreparedStatements.DuckDBBindParameterIndex(preparedStatement, out var index, param.ParameterName); + if (state.IsSuccess()) + { + BindParameter(preparedStatement, index, param); + } + else + { + throw new InvalidOperationException($"Cannot get parameter '{param.ParameterName}' index."); + } + } + } + else + { + for (var i = 0; i < parameterCollection.Count; ++i) + { + var param = parameterCollection[i]; + BindParameter(preparedStatement, i + 1, param); + } } } diff --git a/DuckDB.NET.Test/Helpers/Defer.cs b/DuckDB.NET.Test/Helpers/Defer.cs index 6cdca11..5d9c764 100644 --- a/DuckDB.NET.Test/Helpers/Defer.cs +++ b/DuckDB.NET.Test/Helpers/Defer.cs @@ -11,10 +11,8 @@ public Defer() {} public Defer(Action action) { - AddAction(action); + actions.Push(action); } - - public void AddAction(Action action) => actions.Push(action); public void Dispose() { diff --git a/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs b/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs index fd35fd5..02a7e44 100644 --- a/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs +++ b/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs @@ -22,7 +22,7 @@ public void BindSingleValueTest(string query) var command = connection.CreateCommand(); - command.Parameters.Add(new DuckDBParameter("test", 42)); + command.Parameters.Add(new DuckDBParameter("1", 42)); command.CommandText = query; var scalar = command.ExecuteScalar(); scalar.Should().Be(42); @@ -39,7 +39,7 @@ public void BindSingleValueNullTest(string query) var command = connection.CreateCommand(); - command.Parameters.Add(new DuckDBParameter("test", null)); + command.Parameters.Add(new DuckDBParameter("1", null)); command.CommandText = query; var scalar = command.ExecuteScalar(); scalar.Should().Be(DBNull.Value); @@ -160,8 +160,30 @@ public void BindMultipleValuesTest(string queryStatement) command.ExecuteNonQuery(); command.CommandText = queryStatement; - command.Parameters.Add(new DuckDBParameter("param1", 42)); - command.Parameters.Add(new DuckDBParameter("param2", "hello")); + command.Parameters.Add(new DuckDBParameter("1", 42)); + command.Parameters.Add(new DuckDBParameter("2", "hello")); + var affectedRows = command.ExecuteNonQuery(); + affectedRows.Should().NotBe(0); + } + + [Theory] + [InlineData("INSERT INTO ParametersTestKeyValue (KEY, VALUE) VALUES ($key, $value)")] + [InlineData("UPDATE ParametersTestKeyValue SET KEY = $key, VALUE = $value;")] + public void BindMultipleValuesTestNamedParameters(string queryStatement) + { + using var connection = new DuckDBConnection("DataSource=:memory:"); + using var defer = new Defer(() => connection.Execute("DROP TABLE ParametersTestKeyValue;")); + connection.Open(); + + var command = connection.CreateCommand(); + command.CommandText = "CREATE TABLE ParametersTestKeyValue (KEY INTEGER, VALUE TEXT)"; + command.ExecuteNonQuery(); + command.CommandText = "INSERT INTO ParametersTestKeyValue (KEY, VALUE) VALUES (42, 'test string');"; + command.ExecuteNonQuery(); + + command.CommandText = queryStatement; + command.Parameters.Add(new DuckDBParameter("key", 42)); + command.Parameters.Add(new DuckDBParameter("value", "hello")); var affectedRows = command.ExecuteNonQuery(); affectedRows.Should().NotBe(0); } @@ -192,25 +214,43 @@ public void BindMultipleValuesInvalidOrderTest(string queryStatement) [Theory] // Dapper supports ? placeholders when using both DynamicParameters and an object - [InlineData("INSERT INTO DapperParatemersObjectBindingTest VALUES (?, ?);")] - [InlineData("UPDATE DapperParatemersObjectBindingTest SET a = ?, b = ?;")] + [InlineData("INSERT INTO DapperParametersObjectBindingTest VALUES (?, ?);")] + [InlineData("UPDATE DapperParametersObjectBindingTest SET a = ?, b = ?;")] public void BindDapperWithObjectTest(string queryStatement) { using var connection = new DuckDBConnection("DataSource=:memory:"); - using var defer = new Defer(() => connection.Execute("DROP TABLE DapperParatemersObjectBindingTest;")); + using var defer = new Defer(() => connection.Execute("DROP TABLE DapperParametersObjectBindingTest;")); connection.Open(); - connection.Execute("CREATE TABLE DapperParatemersObjectBindingTest (a INTEGER, b TEXT);"); + connection.Execute("CREATE TABLE DapperParametersObjectBindingTest (a INTEGER, b TEXT);"); + connection.Execute("INSERT INTO DapperParametersObjectBindingTest (a, b) VALUES (42, 'test string');"); var dp = new DynamicParameters(); - dp.Add("param2", 1); - dp.Add("param1", "test"); + dp.Add("?1", 1); + dp.Add("?2", "test"); - connection.Execute(queryStatement, dp).Should().BeLessOrEqualTo(1); - connection.Execute(queryStatement, new { A = 1, B = "test" }); + connection.Execute(queryStatement, dp).Should().BeGreaterOrEqualTo(1); + } - connection.Execute(queryStatement, dp).Should().BeLessOrEqualTo(1); - connection.Execute(queryStatement, new { A = 1, B = "test" }); + [Theory] + // Dapper supports ? placeholders when using both DynamicParameters and an object + [InlineData("INSERT INTO DapperParametersObjectBindingTest VALUES ($foo, $bar);")] + [InlineData("UPDATE DapperParametersObjectBindingTest SET a = $foo, b = $bar;")] + public void BindDapperWithObjectTestNamesParameters(string queryStatement) + { + using var connection = new DuckDBConnection("DataSource=:memory:"); + using var defer = new Defer(() => connection.Execute("DROP TABLE DapperParametersObjectBindingTest;")); + connection.Open(); + + connection.Execute("CREATE TABLE DapperParametersObjectBindingTest (a INTEGER, b TEXT);"); + connection.Execute("INSERT INTO DapperParametersObjectBindingTest (a, b) VALUES (42, 'test string');"); + + var dp = new DynamicParameters(); + dp.Add("foo", 1); + dp.Add("bar", "test"); + + connection.Execute(queryStatement, dp).Should().BeGreaterOrEqualTo(1); + //connection.Execute(queryStatement, new { foo = 1, bar = "test" }).Should().BeGreaterOrEqualTo(1, "Passing parameters as object should work"); } [Theory] @@ -228,8 +268,8 @@ public void BindDapperDynamicParamsOnlyTest(string queryStatement) connection.Execute("CREATE TABLE DapperParametersDynamicParamsBindingTest (a INTEGER, b TEXT);"); var dp = new DynamicParameters(); - dp.Add("param2", 1); - dp.Add("param1", "test"); + dp.Add("1", 1); + dp.Add("2", "test"); connection.Execute(queryStatement, dp).Should().BeLessOrEqualTo(1); } @@ -244,24 +284,24 @@ public void BindSingleValueDapperNullTest(string query) connection.Open(); var parameters = new DynamicParameters(); - parameters.Add("test", null); + parameters.Add("1", null); var scalar = connection.QuerySingle(query, parameters); scalar.Should().BeNull(); } [Theory] // Dapper does not support such placeholders when using an object :( - [InlineData("INSERT INTO DapperParametersObjectBindingFaileTest VALUES (?1, ?2);")] - [InlineData("INSERT INTO DapperParametersObjectBindingFaileTest VALUES ($1, $2);")] - [InlineData("UPDATE DapperParametersObjectBindingFaileTest SET a = ?1, b = ?2;")] - [InlineData("UPDATE DapperParametersObjectBindingFaileTest SET a = $1, b = $2;")] + [InlineData("INSERT INTO DapperParametersObjectBindingFailTest VALUES (?1, ?2);")] + [InlineData("INSERT INTO DapperParametersObjectBindingFailTest VALUES ($1, $2);")] + [InlineData("UPDATE DapperParametersObjectBindingFailTest SET a = ?1, b = ?2;")] + [InlineData("UPDATE DapperParametersObjectBindingFailTest SET a = $1, b = $2;")] public void BindDapperObjectFailuresTest(string queryStatement) { using var connection = new DuckDBConnection("DataSource=:memory:"); - using var defer = new Defer(() => connection.Execute("DROP TABLE DapperParametersObjectBindingFaileTest;")); + using var defer = new Defer(() => connection.Execute("DROP TABLE DapperParametersObjectBindingFailTest;")); connection.Open(); - connection.Execute("CREATE TABLE DapperParametersObjectBindingFaileTest (a INTEGER, b TEXT);"); + connection.Execute("CREATE TABLE DapperParametersObjectBindingFailTest (a INTEGER, b TEXT);"); connection.Invoking(con => con.Execute(queryStatement, new { param1 = 1, param2 = "hello" })) .Should().ThrowExactly();