Skip to content

Commit

Permalink
Option to send query as MultipartFormData (#499)
Browse files Browse the repository at this point in the history
  • Loading branch information
ArtemAstashkin authored Jul 19, 2024
1 parent 2a84287 commit cf5c2b7
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
namespace ClickHouse.Client.Tests.SQL;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ClickHouse.Client.ADO;
using ClickHouse.Client.Tests.Attributes;
using ClickHouse.Client.Utility;
using NUnit.Framework;

[Parallelizable]
[TestFixture(true)]
[TestFixture(false)]
public class SqlParameterizedSelectWithFormDataTests
{
private readonly ClickHouseConnection connection;

public SqlParameterizedSelectWithFormDataTests(bool useCompression)
{
connection = TestUtilities.GetTestClickHouseConnection(useCompression);
connection.SetFormDataParameters(true);
connection.Open();
}

public static IEnumerable<TestCaseData> TypedQueryParameters => TestUtilities.GetDataTypeSamples()
// DB::Exception: There are no UInt128 literals in SQL
.Where(sample => !sample.ClickHouseType.Contains("UUID") || TestUtilities.SupportedFeatures.HasFlag(Feature.UUIDParameters))
// DB::Exception: Serialization is not implemented
.Where(sample => sample.ClickHouseType != "Nothing")
.Select(sample => new TestCaseData(sample.ExampleExpression, sample.ClickHouseType, sample.ExampleValue));

[Test]
[Parallelizable]
[RequiredFeature(Feature.ParamsInMultipartFormData)]
[TestCaseSource(typeof(SqlParameterizedSelectTests), nameof(TypedQueryParameters))]
public async Task ShouldExecuteParameterizedCompareWithTypeDetection(string exampleExpression, string clickHouseType, object value)
{
// https://github.com/ClickHouse/ClickHouse/issues/33928
// TODO: remove
if (connection.ServerVersion.StartsWith("22.1.") && clickHouseType == "IPv6")
Assert.Ignore("IPv6 is broken in ClickHouse 22.1.2.2");

if (clickHouseType.StartsWith("DateTime64") || clickHouseType == "Date" || clickHouseType == "Date32")
Assert.Pass("Automatic type detection does not work for " + clickHouseType);
if (clickHouseType.StartsWith("Enum"))
clickHouseType = "String";

using var command = connection.CreateCommand();
command.CommandText = $"SELECT {exampleExpression} as expected, {{var:{clickHouseType}}} as actual, expected = actual as equals";
command.AddParameter("var", value);

var result = (await command.ExecuteReaderAsync()).GetEnsureSingleRow();
Assert.AreEqual(result[0], result[1]);

if (value is null || value is DBNull)
{
Assert.IsInstanceOf<DBNull>(result[2]);
}
}

public void Dispose() => connection?.Dispose();
}
68 changes: 56 additions & 12 deletions ClickHouse.Client/ADO/ClickHouseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,28 @@ private async Task<HttpResponseMessage> PostSqlQueryAsync(string sqlQuery, Cance
var uriBuilder = connection.CreateUriBuilder();
await connection.EnsureOpenAsync().ConfigureAwait(false); // Preserve old behavior

if (!string.IsNullOrEmpty(QueryId))
uriBuilder.CustomParameters.Add("query_id", QueryId);

using var postMessage = connection.UseFormDataParameters
? BuildHttpRequestMessageWithFormData(
sqlQuery: sqlQuery,
uriBuilder: uriBuilder)
: BuildHttpRequestMessageWithQueryParams(
sqlQuery: sqlQuery,
uriBuilder: uriBuilder);

activity.SetQuery(sqlQuery);

var response = await connection.HttpClient.SendAsync(postMessage, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
QueryId = ExtractQueryId(response);
QueryStats = ExtractQueryStats(response);
activity.SetQueryStats(QueryStats);
return await ClickHouseConnection.HandleError(response, sqlQuery, activity).ConfigureAwait(false);
}

private HttpRequestMessage BuildHttpRequestMessageWithQueryParams(string sqlQuery, ClickHouseUriBuilder uriBuilder)
{
if (commandParameters != null)
{
sqlQuery = commandParameters.ReplacePlaceholders(sqlQuery);
Expand All @@ -165,14 +187,9 @@ private async Task<HttpResponseMessage> PostSqlQueryAsync(string sqlQuery, Cance
}
}

activity.SetQuery(sqlQuery);
var uri = uriBuilder.ToString();

if (!string.IsNullOrEmpty(QueryId))
uriBuilder.CustomParameters.Add("query_id", QueryId);

string uri = uriBuilder.ToString();

using var postMessage = new HttpRequestMessage(HttpMethod.Post, uri);
var postMessage = new HttpRequestMessage(HttpMethod.Post, uri);

connection.AddDefaultHttpHeaders(postMessage.Headers);
HttpContent content = new StringContent(sqlQuery);
Expand All @@ -184,11 +201,38 @@ private async Task<HttpResponseMessage> PostSqlQueryAsync(string sqlQuery, Cance

postMessage.Content = content;

var response = await connection.HttpClient.SendAsync(postMessage, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
QueryId = ExtractQueryId(response);
QueryStats = ExtractQueryStats(response);
activity.SetQueryStats(QueryStats);
return await ClickHouseConnection.HandleError(response, sqlQuery, activity).ConfigureAwait(false);
return postMessage;
}

private HttpRequestMessage BuildHttpRequestMessageWithFormData(string sqlQuery, ClickHouseUriBuilder uriBuilder)
{
var content = new MultipartFormDataContent();

if (commandParameters != null)
{
sqlQuery = commandParameters.ReplacePlaceholders(sqlQuery);

foreach (ClickHouseDbParameter parameter in commandParameters)
{
content.Add(
content: new StringContent(HttpParameterFormatter.Format(parameter, connection.TypeSettings)),
name: $"param_{parameter.ParameterName}");
}
}

content.Add(
content: new StringContent(sqlQuery),
name: "query");

var uri = uriBuilder.ToString();

var postMessage = new HttpRequestMessage(HttpMethod.Post, uri);

connection.AddDefaultHttpHeaders(postMessage.Headers);

postMessage.Content = content;

return postMessage;
}

private static readonly JsonSerializerOptions SummarySerializerOptions = new JsonSerializerOptions
Expand Down
8 changes: 8 additions & 0 deletions ClickHouse.Client/ADO/ClickHouseConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ internal string RedactedConnectionString

public bool UseCompression { get; private set; }

public bool UseFormDataParameters { get; private set; }

public void SetFormDataParameters(
bool sendParametersAsFormData)
{
this.UseFormDataParameters = sendParametersAsFormData;
}

/// <summary>
/// Gets enum describing which ClickHouse features are available on this particular server version
/// Requires connection to be in Open state
Expand Down
3 changes: 3 additions & 0 deletions ClickHouse.Client/ADO/Feature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,8 @@ public enum Feature
[SinceVersion("24.1")]
Variant = 16384,

[SinceVersion("22.3")]
ParamsInMultipartFormData = 32768,

All = ~None, // Special value
}

0 comments on commit cf5c2b7

Please sign in to comment.