Repository navigation
Description
Describe the bug
ClickHouseCommand does not declare its own ExecuteReaderAsync overloads. A call to command.ExecuteReaderAsync() binds to the base DbCommand.ExecuteReaderAsync(), which returns Task<DbDataReader>. The awaited reader has the static type DbDataReader, not ClickHouseDataReader. The run-time object is a ClickHouseDataReader.
docs/http.mdx says the opposite:
- line 1367: "
ExecuteReaderAsync()- Returns aClickHouseDataReaderfor iterating results" - "Using ClickHouseDataReader" (
#ado-net-reader), line 1392, gets the reader withawait using var reader = await command.ExecuteReaderAsync(); - "Reading an enum's ordinal" (
#ado-net-reader-enum-ordinal), line 1423, then callsreader.TryGetEnumOrdinal(ordinal, out int value)
TryGetEnumOrdinal is declared only on ClickHouseDataReader, so this example does not compile. The same applies to the other ClickHouseDataReader-only members (GetDateTimeOffset, GetUInt64, GetIPAddress, GetBigInteger, GetTuple, MapTo<T>, ...) when the reader comes from a ClickHouseCommand.
This is not the same defect as #686 (OpenConnectionAsync() returns DbConnection), and the open fix for #686 (#688) does not change it. The repro below types the command explicitly as ClickHouseCommand, so it does not depend on #686.
Other places in the docs already work around it with a cast, for example line 1681: var reader = (ClickHouseDataReader)await connection.ExecuteReaderAsync(...). The tests do the same (ClickHouse.Driver.Tests/ADO/DataReaderTests.cs:223).
Steps to reproduce
- Create a
net10.0console project that referencesClickHouse.Driver. - Paste the code example below. It is the
#ado-net-readerblock and the#ado-net-reader-enum-ordinalblock fromdocs/http.mdx, without changes, after a minimal command setup. - Build.
Expected behaviour
The documented example compiles, and await command.ExecuteReaderAsync() gives a ClickHouseDataReader, as line 1367 says.
Code example
using System.Data; // by-name accessor extensions (GetString("email"), ...)
using ClickHouse.Driver.ADO;
using ClickHouse.Driver.ADO.Readers;
using var connection = new ClickHouseConnection("Host=clickhouse;Port=8123");
await connection.OpenAsync();
ClickHouseCommand command = connection.CreateCommand();
command.CommandText = "SELECT toInt64(1) AS id, 'n' AS name, 'e@x' AS email, toDateTime('2024-01-01 00:00:00') AS created_at, CAST(NULL, 'Nullable(String)') AS optional_field, CAST('Active', 'Enum8(''Active'' = 1, ''Inactive'' = 2)') AS status";
int ordinal = 5;
// docs/http.mdx lines 1392-1412 and 1423-1424, unchanged
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
var id = reader.GetInt64(0);
var name = reader.GetString(1);
var email = reader.GetString("email");
var timestamp = reader.GetFieldValue<DateTime>("created_at");
if (!reader.IsDBNull("optional_field"))
{
var value = reader.GetString("optional_field");
}
if (reader.TryGetEnumOrdinal(ordinal, out int value2)) // `value` renamed only to avoid a clash in one scope
Console.WriteLine(value2);
}Contrast: with (ClickHouseDataReader)await command.ExecuteReaderAsync() the same program compiles and runs. It prints 1. The server gives the same ordinal:
$ curl -s http://clickhouse:8123/ --data-binary "SELECT CAST(CAST('Active', 'Enum8(''Active'' = 1, ''Inactive'' = 2)'), 'Int8')"
1
Static and run-time type of the reader in that run: ClickHouseDataReader / ClickHouseDataReader. Without the cast, the static type is DbDataReader.
Error log
Program.cs(32,16): error CS1061: 'DbDataReader' does not contain a definition for 'TryGetEnumOrdinal' and no accessible extension method 'TryGetEnumOrdinal' accepting a first argument of type 'DbDataReader' could be found (are you missing a using directive or an assembly reference?)
This is the only error. All the other lines of the snippet compile.
Root cause
ClickHouse.Driver/ADO/ClickHouseCommand.cs:205-207 overrides only the protected ExecuteDbDataReader / ExecuteDbDataReaderAsync, which return DbDataReader. The public ExecuteReader / ExecuteReaderAsync methods are the non-virtual DbCommand methods, so their return type stays DbDataReader / Task<DbDataReader>. ClickHouseCommand already uses new to give a typed CreateParameter() (line 190), but it does not do this for the reader methods.
Suggested fix
Two options. The choice is for a maintainer, as in #686:
-
API: add typed
newmethods onClickHouseCommandthat callExecuteDbDataReaderAsync/ExecuteDbDataReaderand cast the result:public new async Task<ClickHouseDataReader> ExecuteReaderAsync(CancellationToken cancellationToken = default)public new async Task<ClickHouseDataReader> ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken = default)- optionally
public new ClickHouseDataReader ExecuteReader(CommandBehavior behavior = CommandBehavior.Default)for the sync path.
Npgsql (
NpgsqlCommand) and Microsoft.Data.SqlClient (SqlCommand) use the same pattern. This needs entries inClickHouse.Driver/PublicAPI/PublicAPI.Unshipped.txt. It is a source change only for code that assigns the unawaited call to an explicitTask<DbDataReader>;varandawaitkeep compiling. Calls through a variable typed asDbCommandmust keep returningDbDataReader. -
Docs only: in
docs/http.mdx, cast the reader in the#ado-net-readersnippet ((ClickHouseDataReader)await command.ExecuteReaderAsync(), as line 1681 does), and change line 1367 to say that the result is aDbDataReaderwhose run-time type isClickHouseDataReader. This fixes the example but keeps the trap for users who copy the pattern.
Configuration
Environment
- Client version:
mainat 860fe1a (1.5.0 + unreleased), referenced as a project - Language version: C# latest
- .NET version: .NET SDK 10.0.402,
net10.0 - OS: Linux (Debian, in Docker)
ClickHouse server
- ClickHouse Server version: 26.9.1.1629
- ClickHouse Server non-default settings, if any: none
CREATE TABLEstatements for tables involved: none (literalSELECT)
Found by automated analysis while working on #686 (Polyglot AI). The compile error and the cast contrast were reproduced with a real build against a live server, not only by reading the code.
Activity