From 224a0caf269a79bd49c648bff8afa1dc01481e1c Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:19:48 +0000 Subject: [PATCH 1/2] Fix ClickHouseConnection: PostStreamAsync and ClickHouseBulkCopy ignore ChangeDatabase ChangeDatabase stores the database on the connection only. ClickHouseClient.CreateUriBuilder falls back to Settings.Database when the request options carry no database. ClickHouseCommand passes connection.Database, but PostStreamAsync passed only the query id (or no options at all), and ClickHouseBulkCopy built its InsertOptions without a database. Both paths therefore ran in the configured database, so an unqualified table name could resolve to a table in the wrong database. PostStreamAsync now always sends the connection's Database, and ClickHouseBulkCopy reads connection.Database at write time for the schema probe and the INSERT. Fixes: https://github.com/ClickHouse/clickhouse-cs/issues/683 Co-Authored-By: Claude Opus 5.5 --- .../ADO/ConnectionTests.cs | 15 +++++++++++++++ .../BulkCopy/BulkCopyTests.cs | 18 ++++++++++++++++++ ClickHouse.Driver/ADO/ClickHouseConnection.cs | 19 ++++++------------- ClickHouse.Driver/Copy/ClickHouseBulkCopy.cs | 1 + ...ge-database-post-stream-bulk-copy.fixes.md | 3 +++ 5 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 changelog.d/683-change-database-post-stream-bulk-copy.fixes.md diff --git a/ClickHouse.Driver.Tests/ADO/ConnectionTests.cs b/ClickHouse.Driver.Tests/ADO/ConnectionTests.cs index 42e891088..8d22e22e2 100644 --- a/ClickHouse.Driver.Tests/ADO/ConnectionTests.cs +++ b/ClickHouse.Driver.Tests/ADO/ConnectionTests.cs @@ -523,6 +523,21 @@ public async Task ShouldUseQueryIdForRawStream() Assert.That(queryResult.QueryId, Is.EqualTo(queryId)); } + [Test] + public async Task PostStreamAsync_AfterChangeDatabase_RunsInSelectedDatabase([Values] bool useCallback, [Values] bool withQueryId) + { + using var conn = client.CreateConnection(); + conn.ChangeDatabase("system"); + const string sql = "SELECT currentDatabase() FORMAT TabSeparated"; + var queryId = withQueryId ? Guid.NewGuid().ToString() : null; + + using var response = useCallback + ? await conn.PostStreamAsync(sql, (_, _) => Task.CompletedTask, false, CancellationToken.None, queryId) + : await conn.PostStreamAsync(sql, new MemoryStream(), false, CancellationToken.None, queryId); + + Assert.That(await response.Content.ReadAsStringAsync(), Is.EqualTo("system\n")); + } + private static string[] GetColumnNames(DataTable table) => table.Columns.Cast().Select(dc => dc.ColumnName).ToArray(); [Test] diff --git a/ClickHouse.Driver.Tests/BulkCopy/BulkCopyTests.cs b/ClickHouse.Driver.Tests/BulkCopy/BulkCopyTests.cs index 6fee13bc1..c7b23d227 100644 --- a/ClickHouse.Driver.Tests/BulkCopy/BulkCopyTests.cs +++ b/ClickHouse.Driver.Tests/BulkCopy/BulkCopyTests.cs @@ -157,6 +157,24 @@ public async Task ShouldInsertStringOrFixedString(string columnType, string tabl Assert.That(data, Is.EqualTo(testData)); } + [Test] + [TestCase(RowBinaryFormat.RowBinary)] + [TestCase(RowBinaryFormat.RowBinaryWithDefaults)] + public async Task WriteToServerAsync_AfterChangeDatabase_InsertsIntoSelectedDatabase(RowBinaryFormat format) + { + await client.ExecuteNonQueryAsync("CREATE DATABASE IF NOT EXISTS test_secondary"); + var targetTable = CreateTableName($"bulk_{format}", database: "test_secondary"); + await connection.ExecuteStatementAsync($"CREATE TABLE {targetTable} (value UInt32) ENGINE Memory"); + + using var conn = client.CreateConnection(); + using var bulkCopy = new ClickHouseBulkCopy(conn, format) { DestinationTableName = TestUtilities.BareTableName(targetTable) }; + conn.ChangeDatabase("test_secondary"); + + await bulkCopy.WriteToServerAsync(new[] { new object[] { 1u }, new object[] { 2u } }); + + Assert.That(await client.ExecuteScalarAsync($"SELECT count() FROM {targetTable}"), Is.EqualTo(2UL)); + } + [Test] public async Task WriteToServerAsync_FixedStringWithNonSeekableStreamTooLong_ThrowsArgumentException() { diff --git a/ClickHouse.Driver/ADO/ClickHouseConnection.cs b/ClickHouse.Driver/ADO/ClickHouseConnection.cs index d526fef52..6d97b15a2 100644 --- a/ClickHouse.Driver/ADO/ClickHouseConnection.cs +++ b/ClickHouse.Driver/ADO/ClickHouseConnection.cs @@ -231,7 +231,7 @@ public override Task OpenAsync(CancellationToken cancellationToken) /// Task-wrapped HttpResponseMessage object public async Task PostStreamAsync(string sql, Stream data, bool isCompressed, CancellationToken token, string queryId = null) { - var options = GetQueryOptionsWithQueryId(queryId); + var options = GetPostStreamOptions(queryId); return await ClickHouseClient.PostStreamAsync(sql, data, isCompressed, token, options).ConfigureAwait(false); } @@ -247,22 +247,15 @@ public async Task PostStreamAsync(string sql, Stream data, /// Task-wrapped HttpResponseMessage object public async Task PostStreamAsync(string sql, Func callback, bool isCompressed, CancellationToken token, string queryId = null) { - var options = GetQueryOptionsWithQueryId(queryId); + var options = GetPostStreamOptions(queryId); return await ClickHouseClient.PostStreamAsync(sql, callback, isCompressed, token, options).ConfigureAwait(false); } - private static QueryOptions GetQueryOptionsWithQueryId(string queryId) + private QueryOptions GetPostStreamOptions(string queryId) => new() { - QueryOptions options = null; - if (queryId != null) - { - options = new QueryOptions - { - QueryId = queryId, - }; - } - return options; - } + QueryId = queryId, + Database = Database, + }; #pragma warning disable CS0109 // Member does not hide an inherited member; new keyword is not required public new ClickHouseCommand CreateCommand(string commandText = null) => new (this) { CommandText = commandText }; diff --git a/ClickHouse.Driver/Copy/ClickHouseBulkCopy.cs b/ClickHouse.Driver/Copy/ClickHouseBulkCopy.cs index 6ab75751c..fca211252 100644 --- a/ClickHouse.Driver/Copy/ClickHouseBulkCopy.cs +++ b/ClickHouse.Driver/Copy/ClickHouseBulkCopy.cs @@ -129,6 +129,7 @@ public async Task WriteToServerAsync(IEnumerable rows, CancellationTok { var options = new InsertOptions { + Database = connection.Database, BatchSize = BatchSize, MaxDegreeOfParallelism = MaxDegreeOfParallelism, Format = rowBinaryFormat, diff --git a/changelog.d/683-change-database-post-stream-bulk-copy.fixes.md b/changelog.d/683-change-database-post-stream-bulk-copy.fixes.md new file mode 100644 index 000000000..525b537e2 --- /dev/null +++ b/changelog.d/683-change-database-post-stream-bulk-copy.fixes.md @@ -0,0 +1,3 @@ +* Fixed `ClickHouseConnection.PostStreamAsync` and `ClickHouseBulkCopy` ignoring the database + selected with `ChangeDatabase` ([#683](https://github.com/ClickHouse/clickhouse-cs/issues/683)). + An unqualified table name now resolves in that database instead of the configured one. From bf8e057cad05fa87052b40885015100f87c22a45 Mon Sep 17 00:00:00 2001 From: Polyglot AI <293096396+polyglotAI-bot@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:35:53 +0000 Subject: [PATCH 2/2] docs: describe ChangeDatabase in the ADO.NET section of http.mdx Document that commands, both connection PostStreamAsync overloads, and ClickHouseBulkCopy use the database selected with ChangeDatabase, and that the bulk copy reads it when WriteToServerAsync runs. Co-Authored-By: Claude Opus 5.5 --- docs/http.mdx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/http.mdx b/docs/http.mdx index 1e1462472..c4bc8d46d 100644 --- a/docs/http.mdx +++ b/docs/http.mdx @@ -1430,6 +1430,29 @@ any column that is not an enum. The ordinal is the signed value from the wire, so it can be negative, and an `Enum16` ordinal can be larger than a byte. +--- + +### Changing the database {#ado-net-change-database} + +`ChangeDatabase` selects the database for one connection. The connection sends this database with +each request, so an unqualified table name resolves in it: + +```csharp +// connection: a ClickHouseConnection opened from a ClickHouseDataSource +connection.ChangeDatabase("analytics"); + +await using var command = connection.CreateCommand("SELECT count() FROM events"); // analytics.events +var count = await command.ExecuteScalarAsync(); +``` + +- Commands that run on the connection and both `PostStreamAsync` overloads of the connection use + the selected database. +- `ClickHouseBulkCopy` reads the database when `WriteToServerAsync` runs, so a `ChangeDatabase` call + made after you create the bulk copy applies to the next write. +- `connection.Database` returns the selected database. Before the first `ChangeDatabase` call, it + returns the `Database` setting. +- `ChangeDatabase` does not change the settings of the `ClickHouseClient` or of other connections. + ## Best practices {#best-practices} ### Connection lifetime and pooling {#best-practices-connection-lifetime}