Skip to content

ClickHouseCommand: await command.ExecuteReaderAsync() is typed DbDataReader, so the documented TryGetEnumOrdinal example does not compile #690

Open

Description

@polyglotAI-bot

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 a ClickHouseDataReader for iterating results"
  • "Using ClickHouseDataReader" (#ado-net-reader), line 1392, gets the reader with await using var reader = await command.ExecuteReaderAsync();
  • "Reading an enum's ordinal" (#ado-net-reader-enum-ordinal), line 1423, then calls reader.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

  1. Create a net10.0 console project that references ClickHouse.Driver.
  2. Paste the code example below. It is the #ado-net-reader block and the #ado-net-reader-enum-ordinal block from docs/http.mdx, without changes, after a minimal command setup.
  3. 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:

  1. API: add typed new methods on ClickHouseCommand that call ExecuteDbDataReaderAsync / ExecuteDbDataReader and 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 in ClickHouse.Driver/PublicAPI/PublicAPI.Unshipped.txt. It is a source change only for code that assigns the unawaited call to an explicit Task<DbDataReader>; var and await keep compiling. Calls through a variable typed as DbCommand must keep returning DbDataReader.

  2. Docs only: in docs/http.mdx, cast the reader in the #ado-net-reader snippet ((ClickHouseDataReader)await command.ExecuteReaderAsync(), as line 1681 does), and change line 1367 to say that the result is a DbDataReader whose run-time type is ClickHouseDataReader. This fixes the example but keeps the trap for users who copy the pattern.

Configuration

Environment

  • Client version: main at 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 TABLE statements for tables involved: none (literal SELECT)

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocumentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions