From cc0c936533f5692f438abcc0040662e11cfff932 Mon Sep 17 00:00:00 2001 From: Pavel Kravtsov Date: Sun, 17 Aug 2025 03:51:45 +0700 Subject: [PATCH 1/2] Support ValueTuple query parameters --- ClickHouse.Driver/Types/TypeConverter.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ClickHouse.Driver/Types/TypeConverter.cs b/ClickHouse.Driver/Types/TypeConverter.cs index 62e2df74..7e175a1b 100644 --- a/ClickHouse.Driver/Types/TypeConverter.cs +++ b/ClickHouse.Driver/Types/TypeConverter.cs @@ -278,6 +278,11 @@ public static ClickHouseType ToClickHouseType(Type type) return new TupleType { UnderlyingTypes = type.GetGenericArguments().Select(ToClickHouseType).ToArray() }; } + if (type.IsGenericType && type.GetGenericTypeDefinition().FullName.StartsWith("System.ValueTuple", StringComparison.InvariantCulture)) + { + return new TupleType { UnderlyingTypes = type.GetGenericArguments().Select(ToClickHouseType).ToArray() }; + } + if (type.IsGenericType && type.GetGenericTypeDefinition().FullName.StartsWith("System.Collections.Generic.Dictionary", StringComparison.InvariantCulture)) { var types = type.GetGenericArguments().Select(ToClickHouseType).ToArray(); From 92f7125e1b56ead95af180234ce5f4abdbae5c08 Mon Sep 17 00:00:00 2001 From: Pavel Kravtsov Date: Sun, 17 Aug 2025 03:57:12 +0700 Subject: [PATCH 2/2] Add ValueTuple sql parameter tests --- .../SQL/SqlParameterizedSelectTests.cs | 40 ++++++++++++++++++- .../Types/TypeMappingTests.cs | 1 + 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/ClickHouse.Driver.Tests/SQL/SqlParameterizedSelectTests.cs b/ClickHouse.Driver.Tests/SQL/SqlParameterizedSelectTests.cs index 3f4334e4..f68d42d2 100644 --- a/ClickHouse.Driver.Tests/SQL/SqlParameterizedSelectTests.cs +++ b/ClickHouse.Driver.Tests/SQL/SqlParameterizedSelectTests.cs @@ -115,7 +115,7 @@ SELECT 1 FROM (SELECT tuple(1, 'a', NULL) AS res) WHERE res.1 = tupleElement({var:Tuple(Int32, String, Nullable(Int32))}, 1) AND res.2 = tupleElement({var:Tuple(Int32, String, Nullable(Int32))}, 2) - AND res.3 is NULL + AND res.3 is NULL AND tupleElement({var:Tuple(Int32, String, Nullable(Int32))}, 3) is NULL"; using var command = connection.CreateCommand(); command.CommandText = sql; @@ -145,5 +145,43 @@ SELECT 1 result.GetEnsureSingleRow(); } + [Test] + public async Task ShouldExecuteSelectWithValueTupleParameter() + { + var sql = @" + SELECT 1 + FROM (SELECT tuple(1, 'a', NULL) AS res) + WHERE res.1 = tupleElement({var:Tuple(Int32, String, Nullable(Int32))}, 1) + AND res.2 = tupleElement({var:Tuple(Int32, String, Nullable(Int32))}, 2) + AND res.3 is NULL + AND tupleElement({var:Tuple(Int32, String, Nullable(Int32))}, 3) is NULL"; + using var command = connection.CreateCommand(); + command.CommandText = sql; + + command.AddParameter("var", ValueTuple.Create(1, "a", null)); + + var result = await command.ExecuteReaderAsync(); + result.GetEnsureSingleRow(); + } + + [Test] + public async Task ShouldExecuteSelectWithUnderlyingValueTupleParameter() + { + var sql = @" + SELECT 1 + FROM (SELECT tuple(123, tuple(5, 'a', 7)) AS res) + WHERE res.1 = tupleElement({var:Tuple(Int32, Tuple(UInt8, String, Nullable(Int32)))}, 1) + AND res.2.1 = tupleElement(tupleElement({var:Tuple(Int32, Tuple(UInt8, String, Nullable(Int32)))}, 2), 1) + AND res.2.2 = tupleElement(tupleElement({var:Tuple(Int32, Tuple(UInt8, String, Nullable(Int32)))}, 2), 2) + AND res.2.3 = tupleElement(tupleElement({var:Tuple(Int32, Tuple(UInt8, String, Nullable(Int32)))}, 2), 3)"; + using var command = connection.CreateCommand(); + command.CommandText = sql; + + command.AddParameter("var", ValueTuple.Create(123, ValueTuple.Create((byte)5, "a", 7))); + + var result = await command.ExecuteReaderAsync(); + result.GetEnsureSingleRow(); + } + public void Dispose() => connection?.Dispose(); } diff --git a/ClickHouse.Driver.Tests/Types/TypeMappingTests.cs b/ClickHouse.Driver.Tests/Types/TypeMappingTests.cs index 171bc1f6..fdb0521f 100644 --- a/ClickHouse.Driver.Tests/Types/TypeMappingTests.cs +++ b/ClickHouse.Driver.Tests/Types/TypeMappingTests.cs @@ -83,6 +83,7 @@ public class TypeMappingTests [TestCase(typeof(DateOnly), ExpectedResult = "Date")] #endif [TestCase(typeof(Tuple), ExpectedResult = "Tuple(Int32,UInt8,Nullable(Float32),Array(String))")] + [TestCase(typeof(ValueTuple), ExpectedResult = "Tuple(Int32,UInt8,Nullable(Float32),Array(String))")] public string ShouldConvertToClickHouseType(Type type) => TypeConverter.ToClickHouseType(type).ToString(); [Test, Explicit]