From b44b80e51c6686a0143e1c94f4d1c873568ab685 Mon Sep 17 00:00:00 2001 From: SMAH1 Date: Mon, 9 Jun 2025 10:30:30 +0330 Subject: [PATCH 1/5] test: Show no real cancellation and use duplication DbCommand --- .../ADO/ConnectionTests.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/ClickHouse.Client.Tests/ADO/ConnectionTests.cs b/ClickHouse.Client.Tests/ADO/ConnectionTests.cs index 77c44a61..cae99895 100644 --- a/ClickHouse.Client.Tests/ADO/ConnectionTests.cs +++ b/ClickHouse.Client.Tests/ADO/ConnectionTests.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Data; +using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; @@ -137,6 +139,89 @@ public async Task ReplaceRunningQuerySettingShouldReplace() await asyncResult2; } + // see: https://github.com/linq2db/linq2db/discussions/4966 + // see: https://github.com/DarkWanderer/ClickHouse.Client/issues/489 + [Test] + [Explicit("In linq2db you may call the same command multiple times in different SQLs")] + public async Task MultiTimeCallOneCommandAndFallExceptionWhenHasQueryId() + { + string queryId = "MyQueryId123456"; + var command = connection.CreateCommand(); + command.QueryId = queryId; + + try + { + List tasks = new List(2); + + command.CommandText = "SELECT sleep(2) FROM system.numbers LIMIT 100"; + tasks.Add(command.ExecuteScalarAsync()); + + command.CommandText = "SELECT sleep(3) FROM system.numbers LIMIT 200"; // this is another query with the same DbCommand/QueryId + tasks.Add(command.ExecuteScalarAsync()); + + await Task.WhenAll(tasks); + } + catch (ClickHouseServerException ex) when (ex.ErrorCode == 216) + { + Assert.Fail("The query id is running."); + } + catch (Exception) + { + Assert.Fail("Query throw another exception"); + } + } + +#if NET5_0_OR_GREATER + // see: https://github.com/DarkWanderer/ClickHouse.Client/discussions/482 + [Test] + [Explicit("Support Cancellation")] + public async Task SupportCancellation() + { + string queryId = "MyQueryId123456"; + var command = connection.CreateCommand(); + command.CommandText = "SELECT *\r\nFROM (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t1\r\nINNER JOIN (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t2 on t1.num = t2.num"; + command.QueryId = queryId; + + var commandRunning = connection.CreateCommand(); + commandRunning.CommandText = $"SELECT count(*) FROM system.processes where query_id like '{queryId}';"; + + CancellationTokenSource cts = new CancellationTokenSource(); + + async Task cancelAsync(CancellationTokenSource cancellationTokenSource) + { + await Task.Delay(1000); + cancellationTokenSource.Cancel(); + } + + Stopwatch sw = Stopwatch.StartNew(); + try + { + _ = Task.Run(async () => await cancelAsync(cts)); + + await command.ExecuteScalarAsync(cts.Token); + sw.Stop(); + + if(sw.ElapsedMilliseconds > 5000) + Assert.Fail("The query was not cancelled in time"); + + Assert.Fail("The query did not throw an exception"); + } + catch (OperationCanceledException) + { + // Expected exception as operation canceled + + ulong num = (ulong)commandRunning.ExecuteScalar(); + if(num > 0) + Assert.Fail("The query was not cancelled in time, it is still running on the server"); + } + catch (Exception) + { + Assert.Fail("Query throw another exception"); + } + sw.Stop(); + } +#endif + [Test] [Ignore("TODO")] public void ShouldFetchSchema() From 06ebd6fb206ea59160b3eaf46a543056d69ccf4d Mon Sep 17 00:00:00 2001 From: SMAH1 Date: Mon, 9 Jun 2025 12:41:28 +0330 Subject: [PATCH 2/5] Support cancellation and multi time use DbCommand --- .../ADO/ConnectionTests.cs | 6 +- .../AbstractConnectionTestFixture.cs | 7 + ClickHouse.Client.Tests/TestUtilities.cs | 27 ++- .../ADO/ClickHouseCancelableCommand.cs | 183 ++++++++++++++++++ .../ADO/ClickHouseCancelableConnection.cs | 26 +++ .../ClickHouseCancelableConnectionFactory.cs | 26 +++ ClickHouse.Client/ADO/ClickHouseCommand.cs | 4 +- .../ClickHouseCancelableCommandRunner.cs | 153 +++++++++++++++ 8 files changed, 425 insertions(+), 7 deletions(-) create mode 100644 ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs create mode 100644 ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs create mode 100644 ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs create mode 100644 ClickHouse.Client/ADO/Readers/ClickHouseCancelableCommandRunner.cs diff --git a/ClickHouse.Client.Tests/ADO/ConnectionTests.cs b/ClickHouse.Client.Tests/ADO/ConnectionTests.cs index cae99895..293b352f 100644 --- a/ClickHouse.Client.Tests/ADO/ConnectionTests.cs +++ b/ClickHouse.Client.Tests/ADO/ConnectionTests.cs @@ -139,6 +139,7 @@ public async Task ReplaceRunningQuerySettingShouldReplace() await asyncResult2; } +#if NET5_0_OR_GREATER // see: https://github.com/linq2db/linq2db/discussions/4966 // see: https://github.com/DarkWanderer/ClickHouse.Client/issues/489 [Test] @@ -146,7 +147,7 @@ public async Task ReplaceRunningQuerySettingShouldReplace() public async Task MultiTimeCallOneCommandAndFallExceptionWhenHasQueryId() { string queryId = "MyQueryId123456"; - var command = connection.CreateCommand(); + var command = cancelableConnection.CreateCommand(); command.QueryId = queryId; try @@ -171,14 +172,13 @@ public async Task MultiTimeCallOneCommandAndFallExceptionWhenHasQueryId() } } -#if NET5_0_OR_GREATER // see: https://github.com/DarkWanderer/ClickHouse.Client/discussions/482 [Test] [Explicit("Support Cancellation")] public async Task SupportCancellation() { string queryId = "MyQueryId123456"; - var command = connection.CreateCommand(); + var command = cancelableConnection.CreateCommand(); command.CommandText = "SELECT *\r\nFROM (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t1\r\nINNER JOIN (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t2 on t1.num = t2.num"; command.QueryId = queryId; diff --git a/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs b/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs index f1a5fe18..668a04b5 100644 --- a/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs +++ b/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs @@ -10,9 +10,16 @@ public class AbstractConnectionTestFixture : IDisposable { protected readonly ClickHouseConnection connection; +#if NET5_0_OR_GREATER + protected readonly ClickHouseCancelableConnection cancelableConnection; +#endif + protected AbstractConnectionTestFixture() { connection = TestUtilities.GetTestClickHouseConnection(); +#if NET5_0_OR_GREATER + cancelableConnection = TestUtilities.GetTestClickHouseCancelableConnection(); +#endif using var command = connection.CreateCommand(); command.CommandText = "CREATE DATABASE IF NOT EXISTS test;"; command.ExecuteScalar(); diff --git a/ClickHouse.Client.Tests/TestUtilities.cs b/ClickHouse.Client.Tests/TestUtilities.cs index ea2d6a32..848a58de 100644 --- a/ClickHouse.Client.Tests/TestUtilities.cs +++ b/ClickHouse.Client.Tests/TestUtilities.cs @@ -45,6 +45,28 @@ public static async Task ExpectedFeaturesShouldMatchActualFeatures() /// /// public static ClickHouseConnection GetTestClickHouseConnection(bool compression = true, bool session = false, bool customDecimals = true) + { + ClickHouseConnectionStringBuilder builder = SetupConnectionStringBuilder(compression, session, customDecimals); + var connection = new ClickHouseConnection(builder.ConnectionString); + connection.Open(); + return connection; + } + +#if NET5_0_OR_GREATER + /// + /// Utility method to allow to redirect ClickHouse connections to different machine, in case of Windows development environment + /// + /// + public static ClickHouseCancelableConnection GetTestClickHouseCancelableConnection(bool compression = true, bool session = false, bool customDecimals = true) + { + ClickHouseConnectionStringBuilder builder = SetupConnectionStringBuilder(compression, session, customDecimals); + var connection = new ClickHouseCancelableConnection(builder.ConnectionString); + connection.Open(); + return connection; + } +#endif + + private static ClickHouseConnectionStringBuilder SetupConnectionStringBuilder(bool compression, bool session, bool customDecimals) { var builder = GetConnectionStringBuilder(); builder.Compression = compression; @@ -70,9 +92,8 @@ public static ClickHouseConnection GetTestClickHouseConnection(bool compression { builder["set_allow_experimental_dynamic_type"] = 1; } - var connection = new ClickHouseConnection(builder.ConnectionString); - connection.Open(); - return connection; + + return builder; } public static ClickHouseConnectionStringBuilder GetConnectionStringBuilder() diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs b/ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs new file mode 100644 index 00000000..7418b8d0 --- /dev/null +++ b/ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs @@ -0,0 +1,183 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ClickHouse.Client.ADO.Parameters; +using ClickHouse.Client.ADO.Readers; +using ClickHouse.Client.Formats; + +namespace ClickHouse.Client.ADO; + +#if NET5_0_OR_GREATER +public class ClickHouseCancelableCommand : ClickHouseCommand +{ + public ClickHouseCancelableConnection ClickHouseConnection => (ClickHouseCancelableConnection)DbConnection; + + internal ClickHouseParameterCollection ClickHouseParameters => (ClickHouseParameterCollection)DbParameterCollection; + + public ClickHouseCancelableCommand() + : base() + { + } + + public ClickHouseCancelableCommand(ClickHouseConnection connection) + : base(connection) + { + } + + private async Task CancelQuery(string queryId) + { + if (string.IsNullOrEmpty(queryId)) return; + + System.Diagnostics.Trace.WriteLine($"QueryId '{queryId}' is canceld."); + + using ClickHouseCommand command = ClickHouseConnection.CreateCommand(); + command.CommandText = $"KILL QUERY WHERE query_id = '{queryId}'"; + int response = await command.ExecuteNonQueryAsync().ConfigureAwait(false); + } + +#pragma warning disable CA2215 // Dispose methods should call base class dispose + protected override void Dispose(bool disposing) +#pragma warning restore CA2215 // Dispose methods should call base class dispose + { + if (disposing) + { + // Dispose token source with delay + _ = Task.Run(async () => + { + await Task.Delay(1000).ConfigureAwait(false); + + base.Dispose(disposing); + }); + } + } + + public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) => ExecuteNonQueryAsync(new ClickHouseCancelableCommandRunner(), cancellationToken); + + public virtual async Task ExecuteNonQueryAsync(ClickHouseCancelableCommandRunner runner, CancellationToken cancellationToken) + { + if (ClickHouseConnection == null) + throw new InvalidOperationException("Connection is not set"); + + try + { + using var lcts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken, cancellationToken); + using var response = await runner.PostSqlQueryAsync(this, CommandText, lcts.Token).ConfigureAwait(false); + using var reader = new ExtendedBinaryReader(await response.Content.ReadAsStreamAsync(lcts.Token).ConfigureAwait(false)); + + return reader.PeekChar() != -1 ? reader.Read7BitEncodedInt() : 0; + } + catch (OperationCanceledException ex) + { + try + { + await CancelQuery(runner.QueryId).ConfigureAwait(false); + } + catch + { + } + + ExceptionDispatchInfo.Capture(ex).Throw(); + } + catch (Exception ex) + { + ExceptionDispatchInfo.Capture(ex).Throw(); + } + return -1; // no here + } + + /// + /// Allows to return raw result from a query (with custom FORMAT) + /// + /// Cancellation token + /// ClickHouseRawResult object containing response stream + public override Task ExecuteRawResultAsync(CancellationToken cancellationToken) => ExecuteRawResultAsync(new ClickHouseCancelableCommandRunner(), cancellationToken); + + public virtual async Task ExecuteRawResultAsync(ClickHouseCancelableCommandRunner runner, CancellationToken cancellationToken) + { + if (ClickHouseConnection == null) + throw new InvalidOperationException("Connection is not set"); + + try + { + using var lcts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken, cancellationToken); + var response = await runner.PostSqlQueryAsync(this, CommandText, lcts.Token).ConfigureAwait(false); + return new ClickHouseRawResult(response); + } + catch (OperationCanceledException ex) + { + try + { + await CancelQuery(runner.QueryId).ConfigureAwait(false); + } + catch + { + } + + ExceptionDispatchInfo.Capture(ex).Throw(); + } + catch (Exception ex) + { + ExceptionDispatchInfo.Capture(ex).Throw(); + } + return null; // no here + } + + public override Task ExecuteScalarAsync(CancellationToken cancellationToken) => ExecuteScalarAsync(new ClickHouseCancelableCommandRunner(), cancellationToken); + + public virtual async Task ExecuteScalarAsync(ClickHouseCancelableCommandRunner runner, CancellationToken cancellationToken) + { + using var lcts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken, cancellationToken); + using var reader = await ExecuteDbDataReaderAsync(runner, CommandBehavior.Default, lcts.Token).ConfigureAwait(false); + return reader.Read() ? reader.GetValue(0) : null; + } + + protected override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) => + ExecuteDbDataReaderAsync(new ClickHouseCancelableCommandRunner(), behavior, cancellationToken); + + protected virtual async Task ExecuteDbDataReaderAsync(ClickHouseCancelableCommandRunner runner, CommandBehavior behavior, CancellationToken cancellationToken) + { + if (ClickHouseConnection == null) + throw new InvalidOperationException("Connection is not set"); + + try + { + using var lcts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken, cancellationToken); + var sqlBuilder = new StringBuilder(CommandText); + switch (behavior) + { + case CommandBehavior.SingleRow: + sqlBuilder.Append(" LIMIT 1"); + break; + case CommandBehavior.SchemaOnly: + sqlBuilder.Append(" LIMIT 0"); + break; + default: + break; + } + var result = await runner.PostSqlQueryAsync(this, sqlBuilder.ToString(), lcts.Token).ConfigureAwait(false); + return ClickHouseDataReader.FromHttpResponse(result, ClickHouseConnection.TypeSettings); + } + catch (OperationCanceledException ex) + { + try + { + await CancelQuery(runner.QueryId).ConfigureAwait(false); + } + catch + { + } + + ExceptionDispatchInfo.Capture(ex).Throw(); + } + catch (Exception ex) + { + ExceptionDispatchInfo.Capture(ex).Throw(); + } + return null; // no here + } +} +#endif diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs b/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs new file mode 100644 index 00000000..66d97c33 --- /dev/null +++ b/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs @@ -0,0 +1,26 @@ +using System.Data.Common; +using System.Net.Http; + +namespace ClickHouse.Client.ADO; + +#if NET5_0_OR_GREATER +public class ClickHouseCancelableConnection : ClickHouseConnection +{ + public ClickHouseCancelableConnection() + : base() { } + + public ClickHouseCancelableConnection(string connectionString) + : base(connectionString) { } + + public ClickHouseCancelableConnection(string connectionString, HttpClient httpClient) + : base(connectionString, httpClient) { } + + public ClickHouseCancelableConnection(string connectionString, IHttpClientFactory httpClientFactory, string httpClientName = "") + : base(connectionString, httpClientFactory, httpClientName) { } + + + public new ClickHouseCancelableCommand CreateCommand() => new ClickHouseCancelableCommand(this); + + protected override DbCommand CreateDbCommand() => CreateCommand(); +} +#endif diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs b/ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs new file mode 100644 index 00000000..4451c8e6 --- /dev/null +++ b/ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs @@ -0,0 +1,26 @@ +using System.Data.Common; +using ClickHouse.Client.ADO.Adapters; +using ClickHouse.Client.ADO.Parameters; + +namespace ClickHouse.Client.ADO; + +#if NET5_0_OR_GREATER +public class ClickHouseCancelableConnectionFactory : DbProviderFactory +{ + public static ClickHouseCancelableConnectionFactory Instance => new(); + + public override DbConnection CreateConnection() => new ClickHouseCancelableConnection(); + + public override DbDataAdapter CreateDataAdapter() => new ClickHouseDataAdapter(); + + public override DbConnectionStringBuilder CreateConnectionStringBuilder() => new ClickHouseConnectionStringBuilder(); + + public override DbParameter CreateParameter() => new ClickHouseDbParameter(); + + public override DbCommand CreateCommand() => new ClickHouseCancelableCommand(); + +#if NET7_0_OR_GREATER + public override DbDataSource CreateDataSource(string connectionString) => new ClickHouseDataSource(connectionString); +#endif +} +#endif diff --git a/ClickHouse.Client/ADO/ClickHouseCommand.cs b/ClickHouse.Client/ADO/ClickHouseCommand.cs index 4f6181d2..18852b51 100644 --- a/ClickHouse.Client/ADO/ClickHouseCommand.cs +++ b/ClickHouse.Client/ADO/ClickHouseCommand.cs @@ -45,6 +45,8 @@ public ClickHouseCommand(ClickHouseConnection connection) public override UpdateRowSource UpdatedRowSource { get; set; } + public CancellationToken CancellationToken => cts.Token; + /// /// Gets or sets QueryId associated with command /// After query execution, will be set by value provided by server @@ -95,7 +97,7 @@ public override async Task ExecuteNonQueryAsync(CancellationToken cancellat /// /// Cancellation token /// ClickHouseRawResult object containing response stream - public async Task ExecuteRawResultAsync(CancellationToken cancellationToken) + public virtual async Task ExecuteRawResultAsync(CancellationToken cancellationToken) { if (connection == null) throw new InvalidOperationException("Connection is not set"); diff --git a/ClickHouse.Client/ADO/Readers/ClickHouseCancelableCommandRunner.cs b/ClickHouse.Client/ADO/Readers/ClickHouseCancelableCommandRunner.cs new file mode 100644 index 00000000..2b06f4bb --- /dev/null +++ b/ClickHouse.Client/ADO/Readers/ClickHouseCancelableCommandRunner.cs @@ -0,0 +1,153 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ClickHouse.Client.ADO.Parameters; +using ClickHouse.Client.Diagnostic; +using ClickHouse.Client.Formats; +using ClickHouse.Client.Json; +using ClickHouse.Client.Utility; + +namespace ClickHouse.Client.ADO; + +#if NET5_0_OR_GREATER +public class ClickHouseCancelableCommandRunner +{ + private string queryId; + + public ClickHouseCancelableCommandRunner() + { + queryId = Guid.NewGuid().ToString("N"); + } + + /// + /// Gets QueryId associated with command + /// + public string QueryId => queryId; + + public QueryStats QueryStats { get; private set; } + + public async Task PostSqlQueryAsync(ClickHouseCancelableCommand command, string sqlQuery, CancellationToken token) + { + if (command.ClickHouseConnection == null) + throw new InvalidOperationException("Connection not set"); + using var activity = command.ClickHouseConnection.StartActivity("PostSqlQueryAsync"); + + var uriBuilder = command.ClickHouseConnection.CreateUriBuilder(); + await command.ClickHouseConnection.EnsureOpenAsync().ConfigureAwait(false); // Preserve old behavior + + uriBuilder.QueryId = QueryId; + uriBuilder.CommandQueryStringParameters = command.CustomSettings; + + using var postMessage = command.ClickHouseConnection.UseFormDataParameters + ? BuildHttpRequestMessageWithFormData( + command: command, + sqlQuery: sqlQuery, + uriBuilder: uriBuilder) + : BuildHttpRequestMessageWithQueryParams( + command: command, + sqlQuery: sqlQuery, + uriBuilder: uriBuilder); + + activity.SetQuery(sqlQuery); + + var response = await command.ClickHouseConnection.HttpClient + .SendAsync(postMessage, HttpCompletionOption.ResponseHeadersRead, token) + .ConfigureAwait(false); + + QueryStats = ExtractQueryStats(response); + activity.SetQueryStats(QueryStats); + return await ClickHouseConnection.HandleError(response, sqlQuery, activity).ConfigureAwait(false); + } + + private static HttpRequestMessage BuildHttpRequestMessageWithQueryParams(ClickHouseCancelableCommand command, string sqlQuery, ClickHouseUriBuilder uriBuilder) + { + if (command.ClickHouseParameters != null) + { + sqlQuery = command.ClickHouseParameters.ReplacePlaceholders(sqlQuery); + foreach (ClickHouseDbParameter parameter in command.ClickHouseParameters) + { + uriBuilder.AddSqlQueryParameter( + parameter.ParameterName, + HttpParameterFormatter.Format(parameter, command.ClickHouseConnection.TypeSettings)); + } + } + + var uri = uriBuilder.ToString(); + + var postMessage = new HttpRequestMessage(HttpMethod.Post, uri); + + command.ClickHouseConnection.AddDefaultHttpHeaders(postMessage.Headers); + HttpContent content = new StringContent(sqlQuery); + content.Headers.ContentType = new MediaTypeHeaderValue("text/sql"); + if (command.ClickHouseConnection.UseCompression) + { + content = new CompressedContent(content, DecompressionMethods.GZip); + } + + postMessage.Content = content; + + return postMessage; + } + + private static HttpRequestMessage BuildHttpRequestMessageWithFormData(ClickHouseCancelableCommand command, string sqlQuery, ClickHouseUriBuilder uriBuilder) + { + var content = new MultipartFormDataContent(); + + if (command.ClickHouseParameters != null) + { + sqlQuery = command.ClickHouseParameters.ReplacePlaceholders(sqlQuery); + + foreach (ClickHouseDbParameter parameter in command.ClickHouseParameters) + { + content.Add( + content: new StringContent(HttpParameterFormatter.Format(parameter, command.ClickHouseConnection.TypeSettings)), + name: $"param_{parameter.ParameterName}"); + } + } + + content.Add( + content: new StringContent(sqlQuery), + name: "query"); + + var uri = uriBuilder.ToString(); + + var postMessage = new HttpRequestMessage(HttpMethod.Post, uri); + + command.ClickHouseConnection.AddDefaultHttpHeaders(postMessage.Headers); + + postMessage.Content = content; + + return postMessage; + } + + private static readonly JsonSerializerOptions SummarySerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = new SnakeCaseNamingPolicy(), + NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString, + }; + + private static QueryStats ExtractQueryStats(HttpResponseMessage response) + { + try + { + const string summaryHeader = "X-ClickHouse-Summary"; + if (response.Headers.Contains(summaryHeader)) + { + var value = response.Headers.GetValues(summaryHeader).FirstOrDefault(); + var jsonDoc = JsonDocument.Parse(value); + return JsonSerializer.Deserialize(value, SummarySerializerOptions); + } + } + catch + { + } + return null; + } +} +#endif From 32742f4535169ca99161b68fa1d203445c7db0ae Mon Sep 17 00:00:00 2001 From: SMAH1 Date: Mon, 9 Jun 2025 16:19:12 +0330 Subject: [PATCH 3/5] move file --- .../ADO/{Readers => }/ClickHouseCancelableCommandRunner.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ClickHouse.Client/ADO/{Readers => }/ClickHouseCancelableCommandRunner.cs (100%) diff --git a/ClickHouse.Client/ADO/Readers/ClickHouseCancelableCommandRunner.cs b/ClickHouse.Client/ADO/ClickHouseCancelableCommandRunner.cs similarity index 100% rename from ClickHouse.Client/ADO/Readers/ClickHouseCancelableCommandRunner.cs rename to ClickHouse.Client/ADO/ClickHouseCancelableCommandRunner.cs From a7bfb5a94219c0e2346dc55fda4511ad864fe482 Mon Sep 17 00:00:00 2001 From: SMAH1 Date: Tue, 10 Jun 2025 09:11:43 +0330 Subject: [PATCH 4/5] Update test code coverage --- .../ADO/ConnectionCancelableTests.cs | 150 ++++++++++++++++++ .../ADO/ConnectionTests.cs | 83 ---------- .../AbstractConnectionTestFixture.cs | 4 +- ClickHouse.Client.Tests/TestUtilities.cs | 2 +- .../ADO/ClickHouseCancelableCommand.cs | 3 +- .../ADO/ClickHouseCancelableCommandRunner.cs | 3 +- .../ADO/ClickHouseCancelableConnection.cs | 4 +- .../ClickHouseCancelableConnectionFactory.cs | 4 +- 8 files changed, 160 insertions(+), 93 deletions(-) create mode 100644 ClickHouse.Client.Tests/ADO/ConnectionCancelableTests.cs diff --git a/ClickHouse.Client.Tests/ADO/ConnectionCancelableTests.cs b/ClickHouse.Client.Tests/ADO/ConnectionCancelableTests.cs new file mode 100644 index 00000000..6b97f81a --- /dev/null +++ b/ClickHouse.Client.Tests/ADO/ConnectionCancelableTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ClickHouse.Client.ADO; +using ClickHouse.Client.ADO.Parameters; +using ClickHouse.Client.Utility; +using NUnit.Framework; + +#if NET7_0_OR_GREATER + +namespace ClickHouse.Client.Tests.ADO; + +public class ConnectionCancelableTests : AbstractConnectionTestFixture +{ + #region IHttpClientFactory + internal class TestException : Exception + { + public string Parameter { get; private set; } + public TestException(string parameter) : base() + { + Parameter = parameter; + } + } + + internal class HttpClientFactoryFake : IHttpClientFactory + { + public HttpClient CreateClient(string name) + { + throw new TestException($"HttpClientFactoryFake:CreateClient: {name}"); + } + } + #endregion + + [Test] + public async Task ShouldCreateCancelableConnectionWithProvidedHttpClient() + { + using var httpClientHandler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; + using var httpClient = new HttpClient(httpClientHandler); + using var connection = new ClickHouseCancelableConnection(TestUtilities.GetConnectionStringBuilder().ToString(), httpClient); + await connection.OpenAsync(); + ClassicAssert.IsNotEmpty(connection.ServerVersion); + } + + [Test] + public void ShouldCreateCancelableConnectionWithProvidedHttpClientName() + { + using var httpClientHandler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; + using var httpClient = new HttpClient(httpClientHandler); + using var connection = new ClickHouseCancelableConnection(TestUtilities.GetConnectionStringBuilder().ToString(), new HttpClientFactoryFake(), "TestMe"); + Assert.Catch(() => connection.Open(), "HttpClientFactoryFake:CreateClient: TestMe"); + } + + [Test] + public void ShouldCreateCommandCancelable() + { + using var connection = new ClickHouseCancelableConnection(); + var command1 = connection.CreateCommand(); + Assert.That(command1.GetType(), Is.EqualTo(typeof(ClickHouseCancelableCommand))); + Assert.That(command1.ClickHouseConnection, Is.EqualTo(connection)); + } + + // see: https://github.com/linq2db/linq2db/discussions/4966 + // see: https://github.com/DarkWanderer/ClickHouse.Client/issues/489 + [Test] + [Explicit("In linq2db you may call the same command multiple times in different SQLs")] + public async Task MultiTimeCallOneCommandAndFallExceptionWhenHasQueryId() + { + string queryId = "MyQueryId123456"; + var command = cancelableConnection.CreateCommand(); + command.QueryId = queryId; + + try + { + List tasks = new List(2); + + command.CommandText = "SELECT sleep(2) FROM system.numbers LIMIT 100"; + tasks.Add(command.ExecuteScalarAsync()); + + command.CommandText = "SELECT sleep(3) FROM system.numbers LIMIT 200"; // this is another query with the same DbCommand/QueryId + tasks.Add(command.ExecuteScalarAsync()); + + await Task.WhenAll(tasks); + } + catch (ClickHouseServerException ex) when (ex.ErrorCode == 216) + { + Assert.Fail("The query id is running."); + } + catch (Exception) + { + Assert.Fail("Query throw another exception"); + } + } + + // see: https://github.com/DarkWanderer/ClickHouse.Client/discussions/482 + [Test] + [Explicit("Support Cancellation")] + public async Task SupportCancellation() + { + string queryId = "MyQueryId123456"; + var command = cancelableConnection.CreateCommand(); + command.CommandText = "SELECT *\r\nFROM (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t1\r\nINNER JOIN (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t2 on t1.num = t2.num"; + command.QueryId = queryId; + + var commandRunning = connection.CreateCommand(); + commandRunning.CommandText = $"SELECT count(*) FROM system.processes where query_id like '{queryId}';"; + + CancellationTokenSource cts = new CancellationTokenSource(); + + async Task cancelAsync(CancellationTokenSource cancellationTokenSource) + { + await Task.Delay(1000); + cancellationTokenSource.Cancel(); + } + + Stopwatch sw = Stopwatch.StartNew(); + try + { + _ = Task.Run(async () => await cancelAsync(cts)); + + await command.ExecuteScalarAsync(cts.Token); + sw.Stop(); + + if (sw.ElapsedMilliseconds > 5000) + Assert.Fail("The query was not cancelled in time"); + + Assert.Fail("The query did not throw an exception"); + } + catch (OperationCanceledException) + { + // Expected exception as operation canceled + + ulong num = (ulong)commandRunning.ExecuteScalar(); + if (num > 0) + Assert.Fail("The query was not cancelled in time, it is still running on the server"); + } + catch (Exception) + { + Assert.Fail("Query throw another exception"); + } + sw.Stop(); + } +} +#endif diff --git a/ClickHouse.Client.Tests/ADO/ConnectionTests.cs b/ClickHouse.Client.Tests/ADO/ConnectionTests.cs index 293b352f..8aa47341 100644 --- a/ClickHouse.Client.Tests/ADO/ConnectionTests.cs +++ b/ClickHouse.Client.Tests/ADO/ConnectionTests.cs @@ -139,89 +139,6 @@ public async Task ReplaceRunningQuerySettingShouldReplace() await asyncResult2; } -#if NET5_0_OR_GREATER - // see: https://github.com/linq2db/linq2db/discussions/4966 - // see: https://github.com/DarkWanderer/ClickHouse.Client/issues/489 - [Test] - [Explicit("In linq2db you may call the same command multiple times in different SQLs")] - public async Task MultiTimeCallOneCommandAndFallExceptionWhenHasQueryId() - { - string queryId = "MyQueryId123456"; - var command = cancelableConnection.CreateCommand(); - command.QueryId = queryId; - - try - { - List tasks = new List(2); - - command.CommandText = "SELECT sleep(2) FROM system.numbers LIMIT 100"; - tasks.Add(command.ExecuteScalarAsync()); - - command.CommandText = "SELECT sleep(3) FROM system.numbers LIMIT 200"; // this is another query with the same DbCommand/QueryId - tasks.Add(command.ExecuteScalarAsync()); - - await Task.WhenAll(tasks); - } - catch (ClickHouseServerException ex) when (ex.ErrorCode == 216) - { - Assert.Fail("The query id is running."); - } - catch (Exception) - { - Assert.Fail("Query throw another exception"); - } - } - - // see: https://github.com/DarkWanderer/ClickHouse.Client/discussions/482 - [Test] - [Explicit("Support Cancellation")] - public async Task SupportCancellation() - { - string queryId = "MyQueryId123456"; - var command = cancelableConnection.CreateCommand(); - command.CommandText = "SELECT *\r\nFROM (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t1\r\nINNER JOIN (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t2 on t1.num = t2.num"; - command.QueryId = queryId; - - var commandRunning = connection.CreateCommand(); - commandRunning.CommandText = $"SELECT count(*) FROM system.processes where query_id like '{queryId}';"; - - CancellationTokenSource cts = new CancellationTokenSource(); - - async Task cancelAsync(CancellationTokenSource cancellationTokenSource) - { - await Task.Delay(1000); - cancellationTokenSource.Cancel(); - } - - Stopwatch sw = Stopwatch.StartNew(); - try - { - _ = Task.Run(async () => await cancelAsync(cts)); - - await command.ExecuteScalarAsync(cts.Token); - sw.Stop(); - - if(sw.ElapsedMilliseconds > 5000) - Assert.Fail("The query was not cancelled in time"); - - Assert.Fail("The query did not throw an exception"); - } - catch (OperationCanceledException) - { - // Expected exception as operation canceled - - ulong num = (ulong)commandRunning.ExecuteScalar(); - if(num > 0) - Assert.Fail("The query was not cancelled in time, it is still running on the server"); - } - catch (Exception) - { - Assert.Fail("Query throw another exception"); - } - sw.Stop(); - } -#endif - [Test] [Ignore("TODO")] public void ShouldFetchSchema() diff --git a/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs b/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs index 668a04b5..afb1029f 100644 --- a/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs +++ b/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs @@ -10,14 +10,14 @@ public class AbstractConnectionTestFixture : IDisposable { protected readonly ClickHouseConnection connection; -#if NET5_0_OR_GREATER +#if NET7_0_OR_GREATER protected readonly ClickHouseCancelableConnection cancelableConnection; #endif protected AbstractConnectionTestFixture() { connection = TestUtilities.GetTestClickHouseConnection(); -#if NET5_0_OR_GREATER +#if NET7_0_OR_GREATER cancelableConnection = TestUtilities.GetTestClickHouseCancelableConnection(); #endif using var command = connection.CreateCommand(); diff --git a/ClickHouse.Client.Tests/TestUtilities.cs b/ClickHouse.Client.Tests/TestUtilities.cs index 848a58de..cb5a3669 100644 --- a/ClickHouse.Client.Tests/TestUtilities.cs +++ b/ClickHouse.Client.Tests/TestUtilities.cs @@ -52,7 +52,7 @@ public static ClickHouseConnection GetTestClickHouseConnection(bool compression return connection; } -#if NET5_0_OR_GREATER +#if NET7_0_OR_GREATER /// /// Utility method to allow to redirect ClickHouse connections to different machine, in case of Windows development environment /// diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs b/ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs index 7418b8d0..1e12e18a 100644 --- a/ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs +++ b/ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs @@ -9,9 +9,10 @@ using ClickHouse.Client.ADO.Readers; using ClickHouse.Client.Formats; +#if NET7_0_OR_GREATER + namespace ClickHouse.Client.ADO; -#if NET5_0_OR_GREATER public class ClickHouseCancelableCommand : ClickHouseCommand { public ClickHouseCancelableConnection ClickHouseConnection => (ClickHouseCancelableConnection)DbConnection; diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableCommandRunner.cs b/ClickHouse.Client/ADO/ClickHouseCancelableCommandRunner.cs index 2b06f4bb..203fe4f9 100644 --- a/ClickHouse.Client/ADO/ClickHouseCancelableCommandRunner.cs +++ b/ClickHouse.Client/ADO/ClickHouseCancelableCommandRunner.cs @@ -13,9 +13,10 @@ using ClickHouse.Client.Json; using ClickHouse.Client.Utility; +#if NET7_0_OR_GREATER + namespace ClickHouse.Client.ADO; -#if NET5_0_OR_GREATER public class ClickHouseCancelableCommandRunner { private string queryId; diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs b/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs index 66d97c33..462f5ed6 100644 --- a/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs +++ b/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs @@ -1,9 +1,10 @@ using System.Data.Common; using System.Net.Http; +#if NET7_0_OR_GREATER + namespace ClickHouse.Client.ADO; -#if NET5_0_OR_GREATER public class ClickHouseCancelableConnection : ClickHouseConnection { public ClickHouseCancelableConnection() @@ -18,7 +19,6 @@ public ClickHouseCancelableConnection(string connectionString, HttpClient httpCl public ClickHouseCancelableConnection(string connectionString, IHttpClientFactory httpClientFactory, string httpClientName = "") : base(connectionString, httpClientFactory, httpClientName) { } - public new ClickHouseCancelableCommand CreateCommand() => new ClickHouseCancelableCommand(this); protected override DbCommand CreateDbCommand() => CreateCommand(); diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs b/ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs index 4451c8e6..798ea4ee 100644 --- a/ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs +++ b/ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs @@ -4,7 +4,7 @@ namespace ClickHouse.Client.ADO; -#if NET5_0_OR_GREATER +#if NET7_0_OR_GREATER public class ClickHouseCancelableConnectionFactory : DbProviderFactory { public static ClickHouseCancelableConnectionFactory Instance => new(); @@ -19,8 +19,6 @@ public class ClickHouseCancelableConnectionFactory : DbProviderFactory public override DbCommand CreateCommand() => new ClickHouseCancelableCommand(); -#if NET7_0_OR_GREATER public override DbDataSource CreateDataSource(string connectionString) => new ClickHouseDataSource(connectionString); -#endif } #endif From 97885f34780cc9fbffb08b14b26be49053657834 Mon Sep 17 00:00:00 2001 From: SMAH1 Date: Tue, 10 Jun 2025 09:19:53 +0330 Subject: [PATCH 5/5] Fix Typo --- ...Tests.cs => ConnectionCancellableTests.cs} | 20 +++++++------- .../AbstractConnectionTestFixture.cs | 4 +-- ClickHouse.Client.Tests/TestUtilities.cs | 4 +-- .../ADO/ClickHouseCancelableConnection.cs | 26 ------------------- ...and.cs => ClickHouseCancellableCommand.cs} | 24 ++++++++--------- ... => ClickHouseCancellableCommandRunner.cs} | 10 +++---- .../ADO/ClickHouseCancellableConnection.cs | 26 +++++++++++++++++++ ...ClickHouseCancellableConnectionFactory.cs} | 8 +++--- 8 files changed, 61 insertions(+), 61 deletions(-) rename ClickHouse.Client.Tests/ADO/{ConnectionCancelableTests.cs => ConnectionCancellableTests.cs} (85%) delete mode 100644 ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs rename ClickHouse.Client/ADO/{ClickHouseCancelableCommand.cs => ClickHouseCancellableCommand.cs} (87%) rename ClickHouse.Client/ADO/{ClickHouseCancelableCommandRunner.cs => ClickHouseCancellableCommandRunner.cs} (93%) create mode 100644 ClickHouse.Client/ADO/ClickHouseCancellableConnection.cs rename ClickHouse.Client/ADO/{ClickHouseCancelableConnectionFactory.cs => ClickHouseCancellableConnectionFactory.cs} (78%) diff --git a/ClickHouse.Client.Tests/ADO/ConnectionCancelableTests.cs b/ClickHouse.Client.Tests/ADO/ConnectionCancellableTests.cs similarity index 85% rename from ClickHouse.Client.Tests/ADO/ConnectionCancelableTests.cs rename to ClickHouse.Client.Tests/ADO/ConnectionCancellableTests.cs index 6b97f81a..9ddbc7a3 100644 --- a/ClickHouse.Client.Tests/ADO/ConnectionCancelableTests.cs +++ b/ClickHouse.Client.Tests/ADO/ConnectionCancellableTests.cs @@ -17,7 +17,7 @@ namespace ClickHouse.Client.Tests.ADO; -public class ConnectionCancelableTests : AbstractConnectionTestFixture +public class ConnectionCancellableTests : AbstractConnectionTestFixture { #region IHttpClientFactory internal class TestException : Exception @@ -39,30 +39,30 @@ public HttpClient CreateClient(string name) #endregion [Test] - public async Task ShouldCreateCancelableConnectionWithProvidedHttpClient() + public async Task ShouldCreateCancellableConnectionWithProvidedHttpClient() { using var httpClientHandler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; using var httpClient = new HttpClient(httpClientHandler); - using var connection = new ClickHouseCancelableConnection(TestUtilities.GetConnectionStringBuilder().ToString(), httpClient); + using var connection = new ClickHouseCancellableConnection(TestUtilities.GetConnectionStringBuilder().ToString(), httpClient); await connection.OpenAsync(); ClassicAssert.IsNotEmpty(connection.ServerVersion); } [Test] - public void ShouldCreateCancelableConnectionWithProvidedHttpClientName() + public void ShouldCreateCancellableConnectionWithProvidedHttpClientName() { using var httpClientHandler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; using var httpClient = new HttpClient(httpClientHandler); - using var connection = new ClickHouseCancelableConnection(TestUtilities.GetConnectionStringBuilder().ToString(), new HttpClientFactoryFake(), "TestMe"); + using var connection = new ClickHouseCancellableConnection(TestUtilities.GetConnectionStringBuilder().ToString(), new HttpClientFactoryFake(), "TestMe"); Assert.Catch(() => connection.Open(), "HttpClientFactoryFake:CreateClient: TestMe"); } [Test] - public void ShouldCreateCommandCancelable() + public void ShouldCreateCommandCancellable() { - using var connection = new ClickHouseCancelableConnection(); + using var connection = new ClickHouseCancellableConnection(); var command1 = connection.CreateCommand(); - Assert.That(command1.GetType(), Is.EqualTo(typeof(ClickHouseCancelableCommand))); + Assert.That(command1.GetType(), Is.EqualTo(typeof(ClickHouseCancellableCommand))); Assert.That(command1.ClickHouseConnection, Is.EqualTo(connection)); } @@ -73,7 +73,7 @@ public void ShouldCreateCommandCancelable() public async Task MultiTimeCallOneCommandAndFallExceptionWhenHasQueryId() { string queryId = "MyQueryId123456"; - var command = cancelableConnection.CreateCommand(); + var command = cancellableConnection.CreateCommand(); command.QueryId = queryId; try @@ -104,7 +104,7 @@ public async Task MultiTimeCallOneCommandAndFallExceptionWhenHasQueryId() public async Task SupportCancellation() { string queryId = "MyQueryId123456"; - var command = cancelableConnection.CreateCommand(); + var command = cancellableConnection.CreateCommand(); command.CommandText = "SELECT *\r\nFROM (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t1\r\nINNER JOIN (SELECT sleep(3), '0' as num FROM system.numbers LIMIT 100) t2 on t1.num = t2.num"; command.QueryId = queryId; diff --git a/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs b/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs index afb1029f..aa632f4c 100644 --- a/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs +++ b/ClickHouse.Client.Tests/AbstractConnectionTestFixture.cs @@ -11,14 +11,14 @@ public class AbstractConnectionTestFixture : IDisposable protected readonly ClickHouseConnection connection; #if NET7_0_OR_GREATER - protected readonly ClickHouseCancelableConnection cancelableConnection; + protected readonly ClickHouseCancellableConnection cancellableConnection; #endif protected AbstractConnectionTestFixture() { connection = TestUtilities.GetTestClickHouseConnection(); #if NET7_0_OR_GREATER - cancelableConnection = TestUtilities.GetTestClickHouseCancelableConnection(); + cancellableConnection = TestUtilities.GetTestClickHouseCancellableConnection(); #endif using var command = connection.CreateCommand(); command.CommandText = "CREATE DATABASE IF NOT EXISTS test;"; diff --git a/ClickHouse.Client.Tests/TestUtilities.cs b/ClickHouse.Client.Tests/TestUtilities.cs index cb5a3669..d4c54cfe 100644 --- a/ClickHouse.Client.Tests/TestUtilities.cs +++ b/ClickHouse.Client.Tests/TestUtilities.cs @@ -57,10 +57,10 @@ public static ClickHouseConnection GetTestClickHouseConnection(bool compression /// Utility method to allow to redirect ClickHouse connections to different machine, in case of Windows development environment /// /// - public static ClickHouseCancelableConnection GetTestClickHouseCancelableConnection(bool compression = true, bool session = false, bool customDecimals = true) + public static ClickHouseCancellableConnection GetTestClickHouseCancellableConnection(bool compression = true, bool session = false, bool customDecimals = true) { ClickHouseConnectionStringBuilder builder = SetupConnectionStringBuilder(compression, session, customDecimals); - var connection = new ClickHouseCancelableConnection(builder.ConnectionString); + var connection = new ClickHouseCancellableConnection(builder.ConnectionString); connection.Open(); return connection; } diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs b/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs deleted file mode 100644 index 462f5ed6..00000000 --- a/ClickHouse.Client/ADO/ClickHouseCancelableConnection.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Data.Common; -using System.Net.Http; - -#if NET7_0_OR_GREATER - -namespace ClickHouse.Client.ADO; - -public class ClickHouseCancelableConnection : ClickHouseConnection -{ - public ClickHouseCancelableConnection() - : base() { } - - public ClickHouseCancelableConnection(string connectionString) - : base(connectionString) { } - - public ClickHouseCancelableConnection(string connectionString, HttpClient httpClient) - : base(connectionString, httpClient) { } - - public ClickHouseCancelableConnection(string connectionString, IHttpClientFactory httpClientFactory, string httpClientName = "") - : base(connectionString, httpClientFactory, httpClientName) { } - - public new ClickHouseCancelableCommand CreateCommand() => new ClickHouseCancelableCommand(this); - - protected override DbCommand CreateDbCommand() => CreateCommand(); -} -#endif diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs b/ClickHouse.Client/ADO/ClickHouseCancellableCommand.cs similarity index 87% rename from ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs rename to ClickHouse.Client/ADO/ClickHouseCancellableCommand.cs index 1e12e18a..53f9bb75 100644 --- a/ClickHouse.Client/ADO/ClickHouseCancelableCommand.cs +++ b/ClickHouse.Client/ADO/ClickHouseCancellableCommand.cs @@ -13,18 +13,18 @@ namespace ClickHouse.Client.ADO; -public class ClickHouseCancelableCommand : ClickHouseCommand +public class ClickHouseCancellableCommand : ClickHouseCommand { - public ClickHouseCancelableConnection ClickHouseConnection => (ClickHouseCancelableConnection)DbConnection; + public ClickHouseCancellableConnection ClickHouseConnection => (ClickHouseCancellableConnection)DbConnection; internal ClickHouseParameterCollection ClickHouseParameters => (ClickHouseParameterCollection)DbParameterCollection; - public ClickHouseCancelableCommand() + public ClickHouseCancellableCommand() : base() { } - public ClickHouseCancelableCommand(ClickHouseConnection connection) + public ClickHouseCancellableCommand(ClickHouseConnection connection) : base(connection) { } @@ -56,9 +56,9 @@ protected override void Dispose(bool disposing) } } - public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) => ExecuteNonQueryAsync(new ClickHouseCancelableCommandRunner(), cancellationToken); + public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken) => ExecuteNonQueryAsync(new ClickHouseCancellableCommandRunner(), cancellationToken); - public virtual async Task ExecuteNonQueryAsync(ClickHouseCancelableCommandRunner runner, CancellationToken cancellationToken) + public virtual async Task ExecuteNonQueryAsync(ClickHouseCancellableCommandRunner runner, CancellationToken cancellationToken) { if (ClickHouseConnection == null) throw new InvalidOperationException("Connection is not set"); @@ -95,9 +95,9 @@ public virtual async Task ExecuteNonQueryAsync(ClickHouseCancelableCommandR /// /// Cancellation token /// ClickHouseRawResult object containing response stream - public override Task ExecuteRawResultAsync(CancellationToken cancellationToken) => ExecuteRawResultAsync(new ClickHouseCancelableCommandRunner(), cancellationToken); + public override Task ExecuteRawResultAsync(CancellationToken cancellationToken) => ExecuteRawResultAsync(new ClickHouseCancellableCommandRunner(), cancellationToken); - public virtual async Task ExecuteRawResultAsync(ClickHouseCancelableCommandRunner runner, CancellationToken cancellationToken) + public virtual async Task ExecuteRawResultAsync(ClickHouseCancellableCommandRunner runner, CancellationToken cancellationToken) { if (ClickHouseConnection == null) throw new InvalidOperationException("Connection is not set"); @@ -127,9 +127,9 @@ public virtual async Task ExecuteRawResultAsync(ClickHouseC return null; // no here } - public override Task ExecuteScalarAsync(CancellationToken cancellationToken) => ExecuteScalarAsync(new ClickHouseCancelableCommandRunner(), cancellationToken); + public override Task ExecuteScalarAsync(CancellationToken cancellationToken) => ExecuteScalarAsync(new ClickHouseCancellableCommandRunner(), cancellationToken); - public virtual async Task ExecuteScalarAsync(ClickHouseCancelableCommandRunner runner, CancellationToken cancellationToken) + public virtual async Task ExecuteScalarAsync(ClickHouseCancellableCommandRunner runner, CancellationToken cancellationToken) { using var lcts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken, cancellationToken); using var reader = await ExecuteDbDataReaderAsync(runner, CommandBehavior.Default, lcts.Token).ConfigureAwait(false); @@ -137,9 +137,9 @@ public virtual async Task ExecuteScalarAsync(ClickHouseCancelableCommand } protected override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) => - ExecuteDbDataReaderAsync(new ClickHouseCancelableCommandRunner(), behavior, cancellationToken); + ExecuteDbDataReaderAsync(new ClickHouseCancellableCommandRunner(), behavior, cancellationToken); - protected virtual async Task ExecuteDbDataReaderAsync(ClickHouseCancelableCommandRunner runner, CommandBehavior behavior, CancellationToken cancellationToken) + protected virtual async Task ExecuteDbDataReaderAsync(ClickHouseCancellableCommandRunner runner, CommandBehavior behavior, CancellationToken cancellationToken) { if (ClickHouseConnection == null) throw new InvalidOperationException("Connection is not set"); diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableCommandRunner.cs b/ClickHouse.Client/ADO/ClickHouseCancellableCommandRunner.cs similarity index 93% rename from ClickHouse.Client/ADO/ClickHouseCancelableCommandRunner.cs rename to ClickHouse.Client/ADO/ClickHouseCancellableCommandRunner.cs index 203fe4f9..544a3d51 100644 --- a/ClickHouse.Client/ADO/ClickHouseCancelableCommandRunner.cs +++ b/ClickHouse.Client/ADO/ClickHouseCancellableCommandRunner.cs @@ -17,11 +17,11 @@ namespace ClickHouse.Client.ADO; -public class ClickHouseCancelableCommandRunner +public class ClickHouseCancellableCommandRunner { private string queryId; - public ClickHouseCancelableCommandRunner() + public ClickHouseCancellableCommandRunner() { queryId = Guid.NewGuid().ToString("N"); } @@ -33,7 +33,7 @@ public ClickHouseCancelableCommandRunner() public QueryStats QueryStats { get; private set; } - public async Task PostSqlQueryAsync(ClickHouseCancelableCommand command, string sqlQuery, CancellationToken token) + public async Task PostSqlQueryAsync(ClickHouseCancellableCommand command, string sqlQuery, CancellationToken token) { if (command.ClickHouseConnection == null) throw new InvalidOperationException("Connection not set"); @@ -66,7 +66,7 @@ public async Task PostSqlQueryAsync(ClickHouseCancelableCom return await ClickHouseConnection.HandleError(response, sqlQuery, activity).ConfigureAwait(false); } - private static HttpRequestMessage BuildHttpRequestMessageWithQueryParams(ClickHouseCancelableCommand command, string sqlQuery, ClickHouseUriBuilder uriBuilder) + private static HttpRequestMessage BuildHttpRequestMessageWithQueryParams(ClickHouseCancellableCommand command, string sqlQuery, ClickHouseUriBuilder uriBuilder) { if (command.ClickHouseParameters != null) { @@ -96,7 +96,7 @@ private static HttpRequestMessage BuildHttpRequestMessageWithQueryParams(ClickHo return postMessage; } - private static HttpRequestMessage BuildHttpRequestMessageWithFormData(ClickHouseCancelableCommand command, string sqlQuery, ClickHouseUriBuilder uriBuilder) + private static HttpRequestMessage BuildHttpRequestMessageWithFormData(ClickHouseCancellableCommand command, string sqlQuery, ClickHouseUriBuilder uriBuilder) { var content = new MultipartFormDataContent(); diff --git a/ClickHouse.Client/ADO/ClickHouseCancellableConnection.cs b/ClickHouse.Client/ADO/ClickHouseCancellableConnection.cs new file mode 100644 index 00000000..acca3877 --- /dev/null +++ b/ClickHouse.Client/ADO/ClickHouseCancellableConnection.cs @@ -0,0 +1,26 @@ +using System.Data.Common; +using System.Net.Http; + +#if NET7_0_OR_GREATER + +namespace ClickHouse.Client.ADO; + +public class ClickHouseCancellableConnection : ClickHouseConnection +{ + public ClickHouseCancellableConnection() + : base() { } + + public ClickHouseCancellableConnection(string connectionString) + : base(connectionString) { } + + public ClickHouseCancellableConnection(string connectionString, HttpClient httpClient) + : base(connectionString, httpClient) { } + + public ClickHouseCancellableConnection(string connectionString, IHttpClientFactory httpClientFactory, string httpClientName = "") + : base(connectionString, httpClientFactory, httpClientName) { } + + public new ClickHouseCancellableCommand CreateCommand() => new ClickHouseCancellableCommand(this); + + protected override DbCommand CreateDbCommand() => CreateCommand(); +} +#endif diff --git a/ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs b/ClickHouse.Client/ADO/ClickHouseCancellableConnectionFactory.cs similarity index 78% rename from ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs rename to ClickHouse.Client/ADO/ClickHouseCancellableConnectionFactory.cs index 798ea4ee..570e8b12 100644 --- a/ClickHouse.Client/ADO/ClickHouseCancelableConnectionFactory.cs +++ b/ClickHouse.Client/ADO/ClickHouseCancellableConnectionFactory.cs @@ -5,11 +5,11 @@ namespace ClickHouse.Client.ADO; #if NET7_0_OR_GREATER -public class ClickHouseCancelableConnectionFactory : DbProviderFactory +public class ClickHouseCancellableConnectionFactory : DbProviderFactory { - public static ClickHouseCancelableConnectionFactory Instance => new(); + public static ClickHouseCancellableConnectionFactory Instance => new(); - public override DbConnection CreateConnection() => new ClickHouseCancelableConnection(); + public override DbConnection CreateConnection() => new ClickHouseCancellableConnection(); public override DbDataAdapter CreateDataAdapter() => new ClickHouseDataAdapter(); @@ -17,7 +17,7 @@ public class ClickHouseCancelableConnectionFactory : DbProviderFactory public override DbParameter CreateParameter() => new ClickHouseDbParameter(); - public override DbCommand CreateCommand() => new ClickHouseCancelableCommand(); + public override DbCommand CreateCommand() => new ClickHouseCancellableCommand(); public override DbDataSource CreateDataSource(string connectionString) => new ClickHouseDataSource(connectionString); }