> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-vortex-format.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> Официальный клиент для подключения к ClickHouse из C# по собственному TCP-протоколу.

# Клиент ClickHouse для C# с поддержкой нативного TCP

<h2 id="experimental-status">
  Экспериментальный статус
</h2>

<Warning>
  API нативного TCP-клиента является экспериментальным. Его публичный интерфейс может измениться в одном из будущих релизов.
</Warning>

Основной клиент, источник данных, сеанс, интерфейсы операций и расширения для внедрения зависимостей
вызывают предупреждение компилятора `CHTCP0001`. Чтобы подавить это предупреждение на время ознакомления с API, добавьте его
в `NoWarn` в файле проекта:

```xml theme={null}
<PropertyGroup>
  <NoWarn>$(NoWarn);CHTCP0001</NoWarn>
</PropertyGroup>
```

Отключайте это предупреждение, только если вы готовы к тому, что в одном из будущих релизов код, использующий этот API, может потребовать
изменений.

<h2 id="supported-net-versions">
  Поддерживаемые версии .NET
</h2>

Нативный TCP-клиент поддерживает следующие версии .NET:

* .NET 8.0
* .NET 9.0
* .NET 10.0

<h2 id="supported-clickhouse-versions">
  Поддерживаемые версии ClickHouse
</h2>

TCP-клиент официально поддерживает две последние LTS-версии и три последних релиза сервера ClickHouse.

<h2 id="installation">
  Установка
</h2>

Нативный TCP-клиент входит в пакет `ClickHouse.Driver`. Установите его из NuGet:

```bash theme={null}
dotnet add package ClickHouse.Driver
```

Или с помощью NuGet Package Manager:

```bash theme={null}
Install-Package ClickHouse.Driver
```

<h2 id="quick-start">
  Быстрый старт
</h2>

```csharp theme={null}
using ClickHouse.Driver.Tcp;

// Create and reuse a client. It is thread-safe and owns a connection pool.
await using var client = new ClickHouseTcpClient(
    "Host=my.clickhouse;Port=9000;Username=user");

// Execute a query
object version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);
```

<h2 id="migrating-from-http">
  Переход с HTTP-клиента
</h2>

TCP API не связан с `IClickHouseClient`: TCP нельзя выбрать в качестве транспортного протокола для
`ClickHouseClient` или `ClickHouseConnection`. Большинство SQL-запросов можно использовать без изменений, но потребуется создать
`ClickHouseTcpClient` и заменить каждую операцию её аналогом для TCP.

<h3 id="migrating-methods">
  Соответствия методов
</h3>

| HTTP-клиент | TCP-клиент | Важное отличие |
| - | - | - |
| `new ClickHouseClient(...)` | `new ClickHouseTcpClient(...)` | Используйте `ClickHouseTcpClientOptions` или строку TCP-соединения и порт нативного протокола — обычно 9000 без TLS. Оба клиента можно повторно использовать для разных операций. |
| `ExecuteNonQueryAsync(...)` | `ExecuteAsync(...)` | TCP-метод завершается после того, как ClickHouse подтверждает выполнение оператора, и не возвращает количество затронутых строк. |
| `ExecuteScalarAsync(...)` | `ExecuteScalarAsync(...)` | Оба метода возвращают первое значение или `null`, если результат пуст. TCP-метод полностью считывает ответ перед возвратом результата, чтобы соединение можно было использовать повторно. |
| `ExecuteReaderAsync(...)` | `QueryAsync(...)`, `QueryAsync<T>(...)` или `StreamAsync(...)` | В TCP-клиенте нет `DbDataReader`. Можно получать строки `object[]` с выделением памяти, объекты POCO с сопоставленными полями или заимствованные столбцовые блоки. |
| `QueryAsync<T>(...)` | `QueryAsync<T>(...)` | TCP-клиент автоматически создаёт и кэширует сопоставление с POCO; регистрировать их отдельно не нужно. Вместо атрибутов сопоставления HTTP-клиента используйте `ClickHouseTcpColumn` и `ClickHouseTcpNotMapped`. |
| `InsertBinaryAsync(table, columns, rows)` | `InsertRowsAsync(sql, rows)` | Передайте материализованный `IReadOnlyList<object[]>`; SQL-оператор должен заканчиваться на `VALUES`. TCP-метод не возвращает количество вставленных строк. |
| `InsertBinaryAsync<T>(table, rows)` | `InsertRowsAsync<T>(sql, rows)` | TCP-клиенту не требуется регистрация POCO, но каждому столбцу, указанному в операторе, должно соответствовать совместимое общедоступное свойство. |
| — | `InsertAsync(sql, columns)` | Используйте типизированные значения `IColumn`, чтобы при вставке строк избежать преобразования строк в столбцы и упаковки значимых типов. |
| `PingAsync(...)` | `PingAsync(...)` | При неудачной проверке доступности HTTP-клиент возвращает `false`; вызов TCP-клиента успешно завершается при получении `Pong`, а в остальных случаях выбрасывает исключение. |
| `ExecuteRawResultAsync(...)`, `InsertRawStreamAsync(...)`, `PostStreamAsync(...)` | Нет аналога | Для сырых потоков и форматов, таких как CSV, JSONEachRow и Parquet, продолжайте использовать HTTP-клиент. |
| `CreateConnection()` или типы ADO.NET | Нет аналога | Для ADO.NET и ORM продолжайте использовать HTTP-клиент. |

Например, цикл чтения через HTTP-клиент:

```csharp theme={null}
using var reader = await http.ExecuteReaderAsync(
    "SELECT id, name FROM events ORDER BY id");

while (reader.Read())
{
    Console.WriteLine($"{reader.GetFieldValue<ulong>(0)}: {reader.GetString(1)}");
}
```

превращается в асинхронный поток строк по TCP:

```csharp theme={null}
await foreach (object[] row in tcp.QueryAsync(
    "SELECT id, name FROM events ORDER BY id"))
{
    Console.WriteLine($"{row[0]}: {row[1]}");
}
```

<h3 id="migrating-options-parameters">
  Параметры и аргументы
</h3>

HTTP-версии `QueryOptions` и `ClickHouseParameterCollection` не взаимозаменяемы с их аналогами
для TCP. Перенесите значения в `ClickHouseTcpQueryOptions`: в нём настройки задаются строками, а
аргументы входят в объект параметров:

```csharp theme={null}
var options = new ClickHouseTcpQueryOptions
{
    QueryId = "events-by-score",
    Settings = new Dictionary<string, string>
    {
        ["max_execution_time"] = "30",
    },
    Parameters = new ClickHouseTcpParameterCollection
    {
        { "minimum", 18.0 },
    },
};

await foreach (object[] row in client.QueryAsync(
    "SELECT id, score FROM events WHERE score >= {minimum:Float64}",
    options))
{
    Console.WriteLine($"{row[0]}: {row[1]}");
}
```

TCP-клиент поддерживает плейсхолдеры `{name:Type}`, встроенные в ClickHouse, но не преобразует
плейсхолдеры `@name` в стиле ADO.NET. Кроме того, для отдельных запросов нельзя задать базу данных, роль, Bearer-токен или пользовательский
заголовок; также не поддерживаются средство разрешения или форматтер параметров и преобразователь данных при чтении. Подробнее о
возможностях, доступных только через HTTP, см. в разделе [«Ограничения»](#limitations).

<h3 id="migrating-lifetime">
  Время жизни соединения и результата
</h3>

Оба высокоуровневых клиента потокобезопасны, поэтому их следует использовать повторно. TCP-клиент управляет постоянными соединениями по нативному протоколу,
а не HTTP-соединениями, реализует `IAsyncDisposable` и обычно должен
асинхронно освобождаться при завершении работы приложения.

Строки, возвращаемые `QueryAsync`, принадлежат вызывающему коду. Блоки `StreamAsync` и их столбцы не принадлежат вызывающему коду и доступны
только во время текущей итерации; значения, которые понадобятся позже, необходимо скопировать. Всегда завершайте перебор или освобождайте
его ресурсы, чтобы соединение вернулось в пул. Используйте `OpenSessionAsync`, если несколько операций
должны выполняться через одно соединение с сохранением состояния сеанса.

Полный
[пример миграции с HTTP на TCP](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Tcp/Core/Tcp_004_MigratingFromHttp.cs)
показывает, как оба клиента работают с одной таблицей.

<h2 id="configuration">
  Конфигурация
</h2>

Нативный TCP-клиент можно настроить двумя способами:

* **Строка подключения:** пары «ключ — значение», разделённые точкой с запятой и задающие конечную точку сервера,
  учётные данные для аутентификации и другие параметры соединения.
* **Объект `ClickHouseTcpClientOptions`:** строго типизированный неизменяемый объект конфигурации, задаваемый в
  коде.

Передайте строку подключения или объект конфигурации конструктору `ClickHouseTcpClient`. Используйте
`ClickHouseTcpConnectionStringBuilder`, чтобы разобрать или создать строку подключения и преобразовать её в
`ClickHouseTcpClientOptions`.

```csharp theme={null}
var builder = new ClickHouseTcpConnectionStringBuilder
{
    Host = "my.clickhouse",
    Username = "user",
    UseTls = true,
    Compression = "zstd",
};

await using var client = new ClickHouseTcpClient(builder.ToOptions());
```

В следующих разделах перечислены доступные настройки, их значения по умолчанию и описано, на что они влияют.

<h3 id="connection-settings">
  Настройки соединения
</h3>

Значения свойств `TimeSpan` в строке подключения задаются в секундах.

| Свойство | Тип | Значение по умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| Хост | `string` | `"localhost"` | `Host` | Имя хоста или IP-адрес сервера ClickHouse |
| Порт | `int?` | `null` | `Port` | Порт нативного протокола; если порт не указан, используется 9000 без TLS или 9440 с TLS |
| Имя пользователя | `string` | `"default"` | `Username` | Имя пользователя для аутентификации |
| Пароль | `string` | `""` | `Password` | Пароль для аутентификации |
| База данных | `string` | `"default"` | `Database` | База данных, используемая по умолчанию для выполнения операций |
| QuotaKey | `string` | `""` | `QuotaKey` | Ключ для квоты ClickHouse, привязанной к ключу |
| DialTimeout | `TimeSpan` | 30 секунд | `DialTimeout` | Тайм-аут установки соединения через сокет, согласования TLS и рукопожатия по нативному протоколу |
| ReadTimeout | `TimeSpan` | 5 минут | `ReadTimeout` | Максимальное время каждого чтения из транспортного соединения при выполнении операции; нулевое значение отключает тайм-аут |

<h3 id="data-format-serialization">
  Формат данных и сериализация
</h3>

| Свойство | Тип | Значение по умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| SendJsonAndDynamicSerializationSettings | `bool` | `true` | `SendJsonAndDynamicSerializationSettings` | Запрашивает форматы передачи данных `JSON` и `Dynamic`, поддерживаемые клиентом. Для пользователя с доступом только для чтения, который не может изменять настройки, установите `false`; в этом случае чтение данных этих типов может завершиться ошибкой. |
| MaxSendBufferBytes | `int` | 1 MiB | `MaxSendBufferBytes` | Мягкое ограничение на объём буферизованных данных для вставки. Клиент проверяет объём после каждого столбца, поэтому один столбец может привести к превышению ограничения. |
| Compressor | `IClickHouseCompressor` | `Lz4Compressor.Default` | `Compression` | Сжимает блоки, записываемые клиентом, и запрашивает сжатые результаты. В строках подключения допустимы значения `lz4`, `zstd` и `none`; пользовательский компрессор можно задать только через параметры. |

<h3 id="connection-pooling">
  Пул соединений
</h3>

Одно соединение может выполнять только одну операцию за раз. Пул позволяет выполнять операции параллельно и повторно использовать соединения для разных операций.

| Свойство | Тип | Значение по умолчанию | Параметр строки подключения | Описание |
| - | - | - | - | - |
| MinPoolSize | `int` | `0` | `MinPoolSize` | Количество соединений, которые пул по возможности поддерживает открытыми, включая используемые в данный момент |
| MaxPoolSize | `int` | `20` | `MaxPoolSize` | Жёсткое ограничение числа открытых соединений и параллельных операций; остальные операции ожидают свободного соединения |
| PoolTimeout | `TimeSpan` | 30 секунд | `PoolTimeout` | Максимальное время ожидания соединения, когда пул заполнен; время установления нового соединения отдельно ограничивается параметром `DialTimeout` |
| MaxConnectionLifetime | `TimeSpan` | 30 минут | `MaxConnectionLifetime` | Максимальный возраст соединения, при котором его можно использовать повторно; нулевое значение отключает ограничение; выполняемые операции не прерываются |
| IdleTimeout | `TimeSpan` | 5 минут | `IdleTimeout` | Максимальное время, в течение которого неиспользуемое соединение остаётся доступным для повторного использования; нулевое значение отключает ограничение |
| SweepInterval | `TimeSpan?` | `null` (вычисляется) | `SweepInterval` | Как часто закрываются соединения, превысившие ограничения по времени, и восстанавливается минимальное число соединений, заданное `MinPoolSize`; значение по умолчанию вычисляется на основе ограничений возраста соединения и времени простоя |
| PoolReusePolicy | `ClickHouseTcpPoolReusePolicy` | `Lifo` | `PoolReusePolicy` | Выбирает бездействующее соединение, которое вернулось в пул последним (`Lifo`) или раньше всех (`Fifo`) |

<h3 id="security">
  Безопасность
</h3>

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| UseTls | `bool` | `false` | `UseTls` | Включает TLS до рукопожатия по собственному протоколу. Если `Port` не указан, порт по умолчанию меняется с 9000 на 9440. |
| TlsServerName | `string` | `null` | `TlsServerName` | Имя хоста, передаваемое в SNI и сверяемое с сертификатом сервера; если указано `null`, используется `Host` |
| TlsAllowInvalidCertificates | `bool` | `false` | `TlsAllowInvalidCertificates` | Разрешает сертификаты, не прошедшие проверку; используйте только при локальной разработке |
| TlsCaCertificatePath | `string` | `null` | `TlsCaCertificatePath` | Путь к PEM-файлу с хотя бы одним доверенным корневым сертификатом; заменяет хранилище доверенных сертификатов хоста и несовместим с `TlsAllowInvalidCertificates` |
| ConfigureTls | `Action<SslClientAuthenticationOptions>` | `null` | — | Позволяет изменить параметры TLS перед каждым рукопожатием, после применения остальных настроек TLS; доступно только через `ClickHouseTcpClientOptions` |

<Warning>
  Без TLS пароль и все данные запросов передаются по сети без шифрования. Вне локальной разработки
  оставляйте для `TlsAllowInvalidCertificates` значение `false`.
</Warning>

Для защищённого собственного протокола используется отдельная конечная точка сервера: переключиться
на TLS в рамках уже установленного соединения нельзя. Клиент с TLS должен подключаться к защищённому
порту собственного протокола; перехода на соединение без шифрования не происходит. `ConfigureTls`
применяется последним и может переопределить проверку сертификата и имени хоста, поэтому тщательно
проверяйте пользовательские функции обратного вызова.

<h3 id="logging-debugging">
  Логирование и отладка
</h3>

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | Предоставляет логгеры для событий жизненного цикла соединений, пулов и операций в категориях `ClickHouse.Driver.Tcp.*` |
| IncludeSqlInActivityTags | `bool` | `false` | — | Добавляет текст оператора в атрибут OpenTelemetry `db.query.text`, ограничивая его длину значением `StatementMaxLength` |
| StatementMaxLength | `int` | `5` | — | Максимальное число символов оператора в отладочных журналах и тегах активности; если значение не превышает нуля, текст не включается |

Логгер клиента записывает события жизненного цикла и операций на стороне клиента. Пакеты серверного журнала вместо этого передаются
в `ClickHouseTcpQueryCallbacks.OnLog`. Текст оператора может содержать конфиденциальные данные, поэтому по умолчанию он
не включается в теги активности, а в отладочных журналах существенно обрезается.

<h3 id="custom-settings">
  Пользовательские настройки
</h3>

`ClickHouseTcpClientOptions.CustomSettings` применяет настройки сервера ClickHouse к каждому запросу и
каждой вставке. Значения `ClickHouseTcpQueryOptions.Settings`, заданные для конкретной операции, переопределяют одноимённые настройки на уровне клиента.

```csharp theme={null}
var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    CustomSettings = new Dictionary<string, string>
    {
        ["max_threads"] = "4",
    },
};
```

В строке подключения добавляйте к имени каждой настройки сервера префикс `set_`:

```text theme={null}
Host=my.clickhouse;set_max_threads=4;set_max_memory_usage=10000000000
```

Не указывайте префикс `set_` при добавлении настройки в `CustomSettings` или в `Settings` отдельной
операции.

<h3 id="query-options">
  ClickHouseTcpQueryOptions
</h3>

`ClickHouseTcpQueryOptions` задаёт настройки для отдельной операции. Все свойства необязательны.

| Свойство | Тип | По умолчанию | Описание |
| - | - | - | - |
| QueryId | `string` | `null` | Идентификатор, используемый в `system.query_log`, журналах и спанах трассировки; если указано `null` или пустая строка, для каждой операции генерируется новый GUID |
| Settings | `IReadOnlyDictionary<string, string>` | `null` | Настройки сервера для этой операции; значения переопределяют совпадающие параметры `CustomSettings`, заданные на уровне клиента |
| Parameters | `ClickHouseTcpParameterCollection` | `null` | Значения, подставляемые в плейсхолдеры `{name:Type}` в SQL-операторе |
| Callbacks | `ClickHouseTcpQueryCallbacks` | `null` | Синхронные обратные вызовы для получения данных о прогрессе, информации профилирования, серверных журналов, событий профилирования, итоговых и экстремальных значений |

```csharp theme={null}
var queryOptions = new ClickHouseTcpQueryOptions
{
    QueryId = $"report-{Guid.NewGuid():N}",
    Settings = new Dictionary<string, string>
    {
        ["max_threads"] = "4",
    },
};

object count = await client.ExecuteScalarAsync(
    "SELECT count() FROM numbers(1000)",
    queryOptions);
```

<h3 id="insert-options">
  ClickHouseTcpInsertOptions
</h3>

`ClickHouseTcpInsertOptions` расширяет `ClickHouseTcpQueryOptions` настройками для вставки
нативных блоков. Все свойства параметров запроса также доступны.

| Свойство | Тип | По умолчанию | Описание |
| - | - | - | - |
| MaxRowsPerBlock | `int?` | `50,000` | Максимальное число строк в одном передаваемом блоке; при значении `null` вся вставка отправляется одним блоком. Меньшие значения также сокращают объём временной памяти, расходуемой при вставке строк и POCO-объектов. |
| DeduplicationToken | `string` | `null` | Идентифицирует батч для дедупликации вставок на стороне сервера. Используйте один и тот же токен при всех повторных попытках отправки этого батча и новый токен для каждого нового батча. |

```csharp theme={null}
var insertOptions = new ClickHouseTcpInsertOptions
{
    MaxRowsPerBlock = 10_000,
    DeduplicationToken = batchContentHash,
};

await client.InsertRowsAsync(
    "INSERT INTO events (id, name) VALUES",
    rows,
    insertOptions);
```

<h2 id="clickhouse-tcp-client">
  ClickHouseTcpClient
</h2>

`ClickHouseTcpClient` — высокоуровневый API для собственного TCP-протокола. Он выполняет команды и
скалярные запросы, передаёт результаты потоком в виде столбцовых блоков, строк или объектов POCO, вставляет данные по столбцам или строкам
и открывает сеансы, закреплённые за соединением.

Клиент потокобезопасен и управляет пулом соединений. Создайте по одному клиенту для каждой конечной точки, используйте его
во всём приложении и освобождайте его ресурсы при остановке. Одновременно может выполняться до
`MaxPoolSize` операций; остальные ожидают свободного соединения не дольше `PoolTimeout`.

<h3 id="creating-a-client">
  Создание клиента
</h3>

Создайте клиент, используя строку подключения или `ClickHouseTcpClientOptions`. Доступные параметры см. в разделе
[Конфигурация](#configuration).

Создание клиента с помощью строки подключения:

```csharp theme={null}
using ClickHouse.Driver.Tcp;

await using var client = new ClickHouseTcpClient(
    "Host=localhost;Port=9000;Username=default");
```

С использованием строго типизированных параметров:

```csharp theme={null}
var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    Username = "user",
    UseTls = true,
    MaxPoolSize = 20,
};

await using var client = new ClickHouseTcpClient(options);
```

Рекомендуется использовать асинхронное освобождение ресурсов. При освобождении клиент закрывает бездействующие соединения и ожидает завершения активных операций не дольше
`PoolTimeout`, после чего прерывает их.

<h3 id="dependency-injection">
  Внедрение зависимостей
</h3>

`AddClickHouseTcpDataSource` регистрирует один источник данных типа singleton и предоставляет доступ к его общему клиенту через интерфейсы
`IClickHouseTcpClient` и `IClickHouseTcpOperations`:

```csharp theme={null}
using ClickHouse.Driver.Tcp;
using Microsoft.Extensions.DependencyInjection;

services.AddClickHouseTcpDataSource(
    "Host=my.clickhouse;UseTls=true;Username=user");

public sealed class ReportService(IClickHouseTcpClient client)
{
    public async Task<object> CountAsync(CancellationToken cancellationToken) =>
        await client.ExecuteScalarAsync(
            "SELECT count() FROM events",
            cancellationToken: cancellationToken);
}
```

Провайдер сервисов владеет источником данных и пулом соединений. Не освобождайте ресурсы клиента, полученного через внедрение зависимостей;
освобождайте ресурсы провайдера при остановке приложения. Если в параметрах не указан `LoggerFactory`,
при регистрации используется `ILoggerFactory` контейнера. `serviceKey` позволяет зарегистрировать несколько
независимых источников данных.

<h3 id="executing-queries">
  Выполнение запросов
</h3>

Для команд, которые не возвращают строк, например DDL-команд и мутаций, используйте `ExecuteAsync`:

```csharp theme={null}
await client.ExecuteAsync("TRUNCATE TABLE events");
```

Метод завершается после того, как сервер подтвердит получение оператора. Если оператор возвращает строки,
`ExecuteAsync` считывает и отбрасывает их. Для
`INSERT INTO ... VALUES` используйте `InsertAsync` или `InsertRowsAsync`, а SQL-оператор завершайте ключевым словом
`VALUES`, не указывая значения непосредственно в запросе.

<h3 id="reading-data">
  Чтение данных
</h3>

Выберите API для чтения в зависимости от того, в каком виде приложению нужны данные:

| API | Результат | Особенности |
| - | - | - |
| `ExecuteScalarAsync` | Первый столбец первой строки | Проще всего для запросов, возвращающих одно значение; при этом весь результат всё равно считывается |
| `QueryAsync` | Отдельный массив `object[]` для каждой строки | Модель не требуется; для каждой строки выделяется массив, а значимые типы упаковываются |
| `QueryAsync<T>` | Отдельный объект POCO для каждой строки | Строго типизированное сопоставление; для каждой строки создаётся и заполняется объект |
| `StreamAsync` | Заимствованные значения из столбцового блока `Block` | Самый быстрый вариант: без выделения памяти, копирования и построчной материализации |

<h4 id="scalar-reads">
  Чтение скалярных значений
</h4>

`ExecuteScalarAsync` возвращает значение из первого столбца первой строки как `object`:

```csharp theme={null}
object value = await client.ExecuteScalarAsync("SELECT count() FROM events");
ulong count = (ulong)value;
```

Метод считывает ответ целиком и отбрасывает все значения, кроме первого. Пишите запрос, который
возвращает одну строку, а не рассчитывайте, что этот метод досрочно прервёт чтение большого результата. Если первое значение равно `NULL` или запрос
не возвращает ни одной строки, в обоих случаях результатом будет `null`.

<h4 id="row-reads">
  Чтение строк
</h4>

`QueryAsync` потоково выдаёт каждую строку как отдельный массив `object[]`, значения в котором расположены в порядке столбцов результата:

```csharp theme={null}
await foreach (object[] row in client.QueryAsync(
    "SELECT id, name, score FROM events ORDER BY id"))
{
    Console.WriteLine($"{row[0]}: {row[1]} ({row[2]})");
}
```

Массив строки остаётся пригодным для использования после перехода перечислителя к следующему элементу. Этот вариант удобен, когда нет модели
результата, но для каждой строки создаётся отдельный массив, а значения значимых типов упаковываются в объекты.

<h4 id="poco-reads">
  Чтение данных в объекты POCO
</h4>

`QueryAsync<T>` сопоставляет столбцы с общедоступными свойствами, доступными для записи. При сопоставлении имён не учитываются регистр и
символы подчёркивания. Чтобы явно задать имя, используйте `ClickHouseTcpColumn`, а чтобы исключить свойство — `ClickHouseTcpNotMapped`.

```csharp theme={null}
public sealed class Event
{
    public ulong Id { get; set; }

    [ClickHouseTcpColumn(Name = "event_name")]
    public string Name { get; set; } = string.Empty;

    public DateTime RecordedAt { get; set; }
}

await foreach (Event row in client.QueryAsync<Event>(
    "SELECT id, event_name, recorded_at FROM events ORDER BY id"))
{
    Console.WriteLine($"{row.Id}: {row.Name} at {row.RecordedAt:O}");
}
```

Сопоставление с POCO требует преобразований и выделения памяти под отдельный объект для каждой строки.

<h4 id="columnar-block-reads">
  Чтение данных столбцовыми блоками
</h4>

`StreamAsync` возвращает результат в нативном столбцовом формате как последовательность блоков `Block`. Это самый быстрый способ чтения данных.

```csharp theme={null}
await foreach (Block block in client.StreamAsync(
    "SELECT id, score FROM events ORDER BY id"))
{
    ReadOnlySpan<ulong> ids = block.Column<ulong>("id").Values;
    ReadOnlySpan<double> scores = block.Column<double>("score").Values;

    for (int row = 0; row < block.RowCount; row++)
    {
        Console.WriteLine($"{ids[row]}: {scores[row]}");
    }
}
```

Блоки передаются во временное пользование. Блок, его столбцы и диапазоны значений в них доступны только в текущей
итерации. Не освобождайте и не сохраняйте их; данные, которые должны оставаться доступными после завершения итерации, необходимо явно скопировать в выделенную вами память.

<Note>
  Обязательно завершите перебор или освободите перечислитель, чтобы связанное с ним соединение вернулось в пул.
</Note>

`Block.Column<T>` предоставляет доступ к значениям в типе CLR, сформированном декодером. Если требуется преобразование, используйте `Block.ReadAs<T>` или
специализированный интерфейс, например `IDateTimeColumn`.

<h3 id="inserting-data">
  Вставка данных
</h3>

Нативные вставки передают данные в виде столбцовых блоков в двоичном формате. SQL должен содержать оператор `INSERT INTO ... VALUES`
без списка значений в самом запросе. Чтобы свести к минимуму обработку на стороне клиента, используйте типизированные столбцы; если данные в приложении уже представлены построчно,
используйте перегрузки для строк и объектов POCO.

<h4 id="columnar-inserts">
  Столбцовая вставка
</h4>

`InsertAsync` принимает данные, уже сгруппированные по столбцам с заданными типами. Это самый быстрый способ вставки данных.

```csharp theme={null}
var columns = new IColumn[]
{
    ClickHouseTcpColumn.Create("id", new ulong[] { 1, 2, 3 }),
    ClickHouseTcpColumn.Create("name", new[] { "Ada", "Grace", "Alan" }),
    ClickHouseTcpColumn.Create("score", new[] { 99.5, 97.25, 91.0 }),
};

await client.InsertAsync(
    "INSERT INTO events (id, name, score) VALUES",
    columns);
```

Столбцы сопоставляются со столбцами из списка в SQL-операторе по именам, а не по порядку аргументов; число строк
во всех столбцах должно совпадать. Для столбцов таблицы, не указанных в операторе, сервер использует значения по умолчанию. Не изменяйте
массивы, принадлежащие вызывающему коду, до завершения вставки.

Этот API позволяет обойтись без преобразования строк в столбцы и упаковки значений в объектные типы, которые требуются при построчной вставке. Столбец,
полученный из заимствованного блока результатов, также можно передать напрямую в `InsertAsync`, если
вставка завершится до освобождения этого блока.

<h4 id="row-inserts">
  Вставка строк
</h4>

`InsertRowsAsync` принимает строки в виде массивов `object[]`. Значения сопоставляются со столбцами из списка в SQL-запросе в порядке их следования:

```csharp theme={null}
var rows = new[]
{
    new object[] { 1UL, "Ada", 99.5 },
    new object[] { 2UL, "Grace", 97.25 },
};

await client.InsertRowsAsync(
    "INSERT INTO events (id, name, score) VALUES",
    rows);
```

Строки преобразуются в столбцы блок за блоком. Каждая строка должна содержать по одному значению для каждого столбца,
указанного в операторе. Значения в пределах одного столбца должны иметь одинаковый тип CLR, если только
целевой тип не `Variant` или `Dynamic`. Не изменяйте строки до завершения операции.

<h4 id="poco-inserts">
  Вставка данных из POCO
</h4>

Обобщённая перегрузка `InsertRowsAsync<T>` сопоставляет столбцы целевой таблицы с общедоступными свойствами, доступными для чтения, по
тем же правилам именования и использования атрибутов, что и при чтении данных в POCO:

```csharp theme={null}
public sealed class EventToInsert
{
    public ulong Id { get; set; }

    [ClickHouseTcpColumn(Name = "event_name")]
    public string Name { get; set; } = string.Empty;

    public DateTime RecordedAt { get; set; }

    [ClickHouseTcpNotMapped]
    public string? ImportBatch { get; set; }
}

var rows = new[]
{
    new EventToInsert
    {
        Id = 1,
        Name = "Ada",
        RecordedAt = DateTime.UtcNow,
        ImportBatch = "batch-42",
    },
    new EventToInsert
    {
        Id = 2,
        Name = "Grace",
        RecordedAt = DateTime.UtcNow,
        ImportBatch = "batch-42",
    },
};

await client.InsertRowsAsync(
    "INSERT INTO events (id, event_name, recorded_at) VALUES",
    rows);
```

Каждому столбцу, указанному в операторе, должен соответствовать совместимый общедоступный геттер. Строки POCO преобразуются
по одному блоку в формате передачи за раз, поэтому недопустимое значение в одном из последующих блоков может обнаружиться уже после отправки предыдущих блоков.
`ClickHouseTcpNotMapped` исключает из сопоставления свойства, используемые только приложением, например
`ImportBatch`.

<h3 id="sql-parameters">
  Параметры SQL
</h3>

Нативный клиент подставляет значения в плейсхолдеры `{name:Type}` нативного синтаксиса ClickHouse. Плейсхолдеры
`@name` в стиле ADO.NET он не преобразует.

```csharp theme={null}
var parameters = new ClickHouseTcpParameterCollection
{
    { "minimum", 18.0 },
    { "ids", new ulong[] { 1, 2 } },
};

var options = new ClickHouseTcpQueryOptions { Parameters = parameters };

await foreach (object[] row in client.QueryAsync(
    "SELECT id, score FROM events " +
    "WHERE score >= {minimum:Float64} AND id IN {ids:Array(UInt64)}",
    options))
{
    Console.WriteLine($"{row[0]}: {row[1]}");
}
```

Тип каждого параметра должен быть указан либо в плейсхолдере SQL, либо в свойстве
`ClickHouseTcpParameter.ClickHouseType`. Для имён таблиц и столбцов используйте тип `Identifier`.
Если значение `DateTime` или `DateTimeOffset` обозначает конкретный момент времени, укажите часовой пояс, например
`{timestamp:DateTime('UTC')}`; в противном случае часовой пояс сеанса сервера может повлиять на интерпретацию этого значения.

<h3 id="query-id">
  Идентификатор запроса
</h3>

Каждой операции соответствует идентификатор запроса, который используется в системных таблицах ClickHouse, журналах клиента и спанах трассировки. Если
`QueryId` имеет значение null или пуст, клиент генерирует новый GUID. Укажите его, если операцию необходимо
коррелировать с идентификатором приложения:

```csharp theme={null}
var options = new ClickHouseTcpQueryOptions
{
    QueryId = $"report-{Guid.NewGuid():N}",
};

await client.ExecuteAsync("OPTIMIZE TABLE events FINAL", options);
```

<h3 id="query-progress-metadata">
  Ход выполнения запроса и метаданные
</h3>

Настройте `ClickHouseTcpQueryOptions.Callbacks`, чтобы получать метаданные вперемежку с данными ответа:

```csharp theme={null}
ClickHouseTcpProgress total = default;

var options = new ClickHouseTcpQueryOptions
{
    Callbacks = new ClickHouseTcpQueryCallbacks
    {
        OnProgress = progress => total += progress,
        OnProfileInfo = profile =>
            Console.WriteLine($"{profile.Rows} rows in {profile.Blocks} blocks"),
    },
};

await foreach (object[] _ in client.QueryAsync(
    "SELECT number FROM numbers(1000000)",
    options))
{
}
```

Значения прогресса отражают приращения, а не накопленные итоги. Обратные вызовы выполняются синхронно в
потоке, который читает ответ, в порядке поступления пакетов; обработчики должны работать быстро и не генерировать исключений. Блоки, переданные в
`OnLog`, `OnProfileEvents`, `OnTotals` и `OnExtremes`, доступны только на время вызова и не должны использоваться после завершения
обратного вызова.

ClickHouse не отправляет пакеты о ходе выполнения запроса для строк, загружаемых при вставке. Используйте
`OnBlockWritten`, чтобы отслеживать блоки, отправленные клиентом; этот обратный вызов сообщает о ходе передачи, но не о том, что
сервер закоммитил вставку.

<h3 id="cancellation">
  Отмена
</h3>

Каждая операция принимает `CancellationToken`:

```csharp theme={null}
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(2));

try
{
    await foreach (object[] row in client.QueryAsync(
        "SELECT number, sleepEachRow(0.1) FROM numbers(100)",
        cancellationToken: cancellation.Token))
    {
        Console.WriteLine(row[0]);
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("Query cancelled.");
}
```

Отмена или досрочное прекращение чтения потокового результата не позволяет безопасно использовать соединение повторно, поэтому клиент
закрывает его, а не возвращает в пул. При этом клиентом по-прежнему можно пользоваться: для последующих операций он открывает
новое соединение или использует другое существующее. Если сеанс закреплён за соединением, отмена может
завершить этот сеанс, и его временные таблицы и настройки будут потеряны.

<h3 id="server-information-health-checks">
  Информация о сервере и проверки его работоспособности
</h3>

`GetServerInfoAsync` возвращает идентификатор сервера, его версию, часовой пояс и сведения о протоколе, согласованном
при рукопожатии по нативному протоколу:

```csharp theme={null}
ClickHouseTcpServerInfo server = await client.GetServerInfoAsync();

Console.WriteLine($"ClickHouse {server.Version}");
Console.WriteLine($"Protocol revision: {server.ProtocolRevision}");
Console.WriteLine($"Server timezone: {server.Timezone}");
```

Поддержку возможностей на уровне двоичного представления данных определяйте по `ProtocolRevision` — ревизии, согласованной клиентом и
сервером. Поддержку возможностей SQL, таких как типы данных и функции, определяйте по `Version`. Метод использует существующее
соединение, если оно доступно, и открывает новое, если пул пуст.

Используйте `PingAsync` для проверки готовности по собственному протоколу:

```csharp theme={null}
await client.PingAsync(cancellationToken);
```

Успешный Ping подтверждает подключение по нативному протоколу и прохождение аутентификации. При этом он не проверяет доступ к
таблице приложения.

<h3 id="more-examples">
  Дополнительные примеры
</h3>

Полные примеры, которые можно запустить и которые охватывают чтение данных, вставку, типы данных, сеансы, TLS, обсервабилити и
обработку ошибок, см. в
[примерах использования нативного TCP](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples/Tcp) в
репозитории GitHub.

<h2 id="sessions">
  Сеансы
</h2>

Обычные операции клиента используют любое свободное соединение из пула, поэтому состояние, связанное с соединением,
не изолировано и может не сохраняться между вызовами. Соединения из одного пула могут вести к разным серверам.
Откройте сеанс, если для работы с временными таблицами, выполнения операторов `SET` или последовательности других операций требуется одно и то же соединение.

```csharp theme={null}
await using IClickHouseTcpSession session = await client.OpenSessionAsync();

await session.ExecuteAsync("SET max_threads = 2");
await session.ExecuteAsync("""
    CREATE TEMPORARY TABLE session_events
    (
        id UInt64,
        name String
    )
    ENGINE = Memory
    """);

await session.InsertRowsAsync(
    "INSERT INTO session_events (id, name) VALUES",
    new[]
    {
        new object[] { 1UL, "Ada" },
        new object[] { 2UL, "Grace" },
    });

object count = await session.ExecuteScalarAsync(
    "SELECT count() FROM session_events");
```

`IClickHouseTcpSession` предоставляет те же API для выполнения запросов и команд, вставки данных, проверки доступности и получения информации о сервере,
что и клиент. Сеанс не является потокобезопасным. Операции в нём выполняются последовательно: попытка начать новую операцию до завершения текущей
будет отклонена. При потоковой обработке результата сеанс остаётся занятым, пока результат не будет полностью перебран или
не будет освобождён его перечислитель.

Каждый сеанс занимает один слот `MaxPoolSize` на протяжении всего времени своего существования. Клиент может продолжать использовать другие
соединения из пула, но если в пуле всего одно соединение, другие операции клиента будут ожидать,
пока сеанс не будет освобождён или не истечёт время ожидания `PoolTimeout`.

Освободите сеанс до того, как освободите создавший его клиент. При освобождении сеанса закреплённое за ним
соединение закрывается, а не возвращается в пул. Благодаря этому последующие операции не унаследуют
временные таблицы или настройки сеанса. Сбой соединения или протокола, отмена операции либо не полностью обработанный
поток результатов могут сделать сеанс непригодным для использования; если `IsOpen` принимает значение `false`, откройте новый сеанс.

<h2 id="best-practices">
  Рекомендации
</h2>

<h3 id="best-practices-client-lifetime">
  Время жизни клиента и пул соединений
</h3>

Создайте по одному `ClickHouseTcpClient` или `ClickHouseTcpDataSource` для каждой уникальной комбинации конечной точки и
конфигурации и используйте его в течение всего времени работы приложения. Клиент безопасен для использования из нескольких потоков и управляет
собственным пулом соединений. Если создавать клиент для каждой операции, каждый раз будет создаваться новый пул, а повторное использование
соединений станет невозможным.

В приложении с внедрением зависимостей один раз зарегистрируйте `AddClickHouseTcpDataSource` и внедряйте
`IClickHouseTcpClient`. Жизненным циклом клиента управляет контейнер зависимостей, поэтому компоненты, получающие клиент, не должны освобождать
внедрённый экземпляр. Если внедрение зависимостей не используется, освобождайте клиент при остановке приложения с помощью `await using`.

Каждое соединение по нативному протоколу выполняет только одну операцию за раз. Поэтому `MaxPoolSize` ограничивает и число открытых
соединений, и число параллельных операций; дополнительные вызовы ожидают свободного соединения не дольше `PoolTimeout`.
Выбирайте значение с учётом фактического параллелизма приложения, не превышая установленных на сервере
ограничений на соединения и запросы. Увеличение этого значения не ускорит отдельный запрос.

Учитывайте, что соединения из пула также используются в следующих случаях:

* При потоковой обработке результата соединение остаётся занятым, пока не будет освобождён перечислитель.
* Сеанс удерживает одно соединение на протяжении всего времени своего существования и выполняет только одну операцию за
  раз.
* Если обработка результата прервана, операция отменена во время выполнения или произошёл сбой протокола либо соединения,
  соединение закрывается, а не используется повторно.

Своевременно освобождайте сеансы и всегда перебирайте асинхронные результаты до конца либо освобождайте их
перечислитель. Используйте категорию логирования `ClickHouse.Driver.Tcp.Pool` для диагностики исчерпания пула,
неожиданного закрытия соединений или частого повторного установления соединений.

<h3 id="best-practices-timeouts">
  Тайм-ауты и отмена
</h3>

У TCP-клиента для разных этапов ожидания предусмотрены отдельные ограничения:

| Механизм | Что он ограничивает |
| - | - |
| `DialTimeout` | Установление соединения через сокет, согласование TLS и рукопожатие по нативному протоколу при создании нового соединения |
| `PoolTimeout` | Ожидание свободного слота в пуле, когда все соединения заняты |
| `ReadTimeout` | Один период, в течение которого сервер не передаёт данные при чтении ответа, а не общую длительность запроса |
| `CancellationToken` | Всю операцию под управлением вызывающего кода, включая ожидание свободного слота в пуле и перебор результатов |
| `max_execution_time` | Время выполнения запроса на стороне сервера |

Получение соединения, при котором сначала нужно дождаться свободного слота в пуле, а затем установить новое соединение, может занять до
`PoolTimeout + DialTimeout`. Отсчёт `ReadTimeout` начинается заново при каждом чтении из транспорта, поэтому длительный запрос, в ходе которого
продолжают поступать сведения о ходе выполнения или данные, может выполняться дольше этого времени. Установите значение, достаточное для допустимых
периодов, когда сервер не передаёт данные; задавайте `TimeSpan.Zero` только в том случае, если длительность операции надёжно ограничена
другим тайм-аутом.

Если нужно ограничить время выполнения всей операции, используйте токен отмены:

```csharp theme={null}
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));

var queryOptions = new ClickHouseTcpQueryOptions
{
    Settings = new Dictionary<string, string>
    {
        ["max_execution_time"] = "25",
    },
};

await foreach (Block block in client.StreamAsync(
    "SELECT * FROM large_report",
    queryOptions,
    timeout.Token))
{
    // Process each block before advancing.
}
```

Клиент пытается отправить пакет Cancel нативного протокола, если это безопасно, а затем закрывает
соединение, ответ из которого не был полностью считан. Клиент из пула остаётся
пригодным для последующих операций, но после отмены закреплённый сеанс может стать непригодным для использования. Результат отменённой вставки или вставки, завершившейся по тайм-ауту, может остаться
неизвестным; руководствуйтесь [рекомендациями по повторным попыткам вставки](#error-handling-retries), а не
повторяйте её вслепую.

<h3 id="best-practices-datetime">
  Обработка даты и времени
</h3>

Для временных меток предпочтительны `DateTime('UTC')` или `DateTime64(S, 'UTC')`, а в прикладном коде — `DateTimeOffset` или
`DateTime` со значением `Utc` для `Kind`. Так момент времени задаётся однозначно
и не зависит от часового пояса сервера или сеанса.

При записи значений `DateTime` TCP-клиент применяет следующие правила:

| `DateTime.Kind` | Интерпретация |
| - | - |
| `Utc` | Момент времени в UTC сохраняется. |
| `Local` | Значение преобразуется в соответствующий момент времени в UTC. |
| `Unspecified` | Время по местным часам интерпретируется в часовом поясе столбца, а если он не указан — в часовом поясе сеанса. |

Значение `Unspecified`, попадающее в пропущенный при переходе на летнее время интервал, отклоняется: ему не соответствует
ни один момент времени. Если при возврате на зимнее время показание часов повторяется, выбирается первый из двух моментов. Используйте `DateTimeOffset`, когда
выбор необходимо задать явно.

Для параметров SQL, обозначающих момент времени, указывайте часовой пояс в плейсхолдере:

```csharp theme={null}
var parameters = new ClickHouseTcpParameterCollection
{
    new ClickHouseTcpParameter(
        "captured_at",
        DateTimeOffset.UtcNow,
        "DateTime64(3, 'UTC')"),
};

var queryOptions = new ClickHouseTcpQueryOptions
{
    Parameters = parameters,
};

await client.ExecuteAsync(
    "INSERT INTO events (captured_at) VALUES ({captured_at:DateTime64(3, 'UTC')})",
    queryOptions);
```

При столбцовом чтении `DateTime` и `DateTime64` по умолчанию возвращают исходные числовые значения в типах `uint` или `long`.
Используйте `Block.ReadAs<DateTimeOffset>`, `IDateTimeColumn.GetDateTimeOffset` или соответствующее свойство POCO,
если удобнее работать с календарной датой и временем. Если важна каждая цифра, сохраняйте исходное числовое значение:
разрешение `DateTimeOffset` составляет 100 наносекунд, поэтому при чтении через него значений `DateTime64` с масштабом 8 или 9
теряется точность на уровне долей тика.

Используйте `DateOnly` для `Date` и `Date32`. Для значений `Time` и `Time64`, которые могут быть
отрицательными или превышать 24 часа, используйте `TimeSpan`; `TimeOnly` подходит только тогда, когда значения столбца ограничены
временем суток.

<h3 id="best-practices-streaming">
  Потоковая обработка и время жизни результатов
</h3>

Для обработки с высокой пропускной способностью используйте `StreamAsync` и типизированные столбцы. Это соответствует
столбцовой структуре ClickHouse и позволяет избежать создания массивов для каждой строки, упаковки значений и выделения памяти под POCO. Используйте `QueryAsync<T>`, когда
важнее получить модель приложения, владеющую своими данными, чем свести выделение памяти к минимуму, а нетипизированный
`QueryAsync` — только если схема неизвестна на этапе компиляции.

Блоки, возвращаемые `StreamAsync`, предоставляются без передачи владения. Блок, его столбцы, дочерние столбцы составных типов и
все спаны доступны только в течение текущей итерации цикла. Обрабатывайте их на месте или копируйте значения, которые
понадобятся после завершения итерации:

```csharp theme={null}
ulong[] retained;

await foreach (Block block in client.StreamAsync("SELECT id FROM events"))
{
    ReadOnlySpan<ulong> ids = block.Column<ulong>("id").Values;
    retained = ids.ToArray();
}
```

Не освобождайте возвращённый блок вручную. `await foreach` сам освободит перечислитель. Если нужна только часть
результата, ограничьте выборку в SQL с помощью `LIMIT` и выбирайте только необходимые столбцы. Досрочное прекращение перебора
отменяет получение оставшейся части ответа и не позволяет повторно использовать соединение.

<h3 id="best-practices-inserts">
  Структура вставки и формирование батчей
</h3>

Если данные уже организованы по столбцам, используйте `InsertAsync` с типизированными столбцами: этот метод сводит к минимуму преобразования на стороне клиента. Для объектов POCO лучше подходит `InsertRowsAsync<T>`. Строки в виде нетипизированных `object[]` дают наибольшую гибкость, но требуют выделения памяти под массивы и упаковки значимых типов.

Передавайте достаточное количество строк за один вызов вставки вместо множества небольших вставок. В пределах одного вызова `MaxRowsPerBlock` разбивает данные на блоки нативного формата передачи. Значение по умолчанию — 50 000 строк — также ограничивает размер временных буферов столбцов при преобразовании строк и объектов POCO. Увеличение этого значения может повысить пропускную способность для строк с небольшим числом столбцов, но увеличит пиковое потребление памяти. Если задать `null`, вся вставка будет записана одним блоком, а все входные данные, организованные по строкам, будут преобразованы за один раз.

`MaxSendBufferBytes` отдельно ограничивает объём закодированных данных, которые накапливаются в буфере перед отправкой через сокет. Это мягкое ограничение проверяется между столбцами, поэтому один широкий столбец может его превысить.

Строки и объекты POCO преобразуются по одному блоку за раз. Поэтому недопустимое значение в одном из последующих блоков может обнаружиться уже после отправки предыдущих блоков. Если качество данных вызывает сомнения, перед вставкой проверьте допустимость NULL и типы CLR. Если возможна повторная отправка логического батча, задайте `DeduplicationToken` на основе данных.

<h2 id="supported-data-types">
  Поддерживаемые типы данных
</h2>

В таблицах ниже отдельно указаны тип, возвращаемый при декодировании `IColumn<T>`, и дополнительные типы,
доступные при преобразовании через `Block.ReadAs<T>`, сопоставлении с POCO через `QueryAsync<T>` или вставке. Проверить, поддерживается ли конкретное
сопоставление, можно с помощью того же механизма разрешения типов, который используется при этих операциях:

```csharp theme={null}
bool canRead = ClickHouseTcpTypes.CanRead("Array(Nullable(DateTime))", typeof(DateTime?[]));
bool canWrite = ClickHouseTcpTypes.CanWrite("FixedString(8)", typeof(byte[]));
```

Правила сопоставления для `Nullable`, `Array`, `Tuple`, `Map` и `LowCardinality` рекурсивно применяются к вложенным
типам.

<h3 id="type-map-reading">
  Сопоставление типов: чтение из ClickHouse
</h3>

`Default .NET type` — это тип `T`, доступный через `Block.Column<T>` и `IColumn<T>`. Типы, указанные в столбце
`Also readable as`, можно читать через `Block.ReadAs<T>` и свойства POCO.

<h4 id="type-map-reading-integer">
  Целочисленные типы
</h4>

| Тип ClickHouse | Тип .NET по умолчанию |
| - | - |
| `Int8` | `sbyte` |
| `UInt8` | `byte` |
| `Int16` | `short` |
| `UInt16` | `ushort` |
| `Int32` | `int` |
| `UInt32` | `uint` |
| `Int64` | `long` |
| `UInt64` | `ulong` |
| `Int128` | `Int128` |
| `UInt128` | `UInt128` |
| `Int256` | `Int256` |
| `UInt256` | `UInt256` |

<h4 id="type-map-reading-floating-point">
  Типы с плавающей запятой
</h4>

| Тип ClickHouse | Тип .NET по умолчанию |
| - | - |
| `Float32` | `float` |
| `Float64` | `double` |
| `BFloat16` | `float` |

<h4 id="type-map-reading-decimal">
  Десятичные типы
</h4>

| Тип ClickHouse | Тип .NET по умолчанию |
| - | - |
| `Decimal(P, S)`, где `P <= 18` | `decimal` |
| `Decimal(P, S)`, где `P > 18` | `ClickHouseTcpDecimal` |
| `Decimal32(S)` | `decimal` |
| `Decimal64(S)` | `decimal` |
| `Decimal128(S)` | `ClickHouseTcpDecimal` |
| `Decimal256(S)` | `ClickHouseTcpDecimal` |

`ClickHouseTcpDecimal` хранит мантиссу как целое число произвольной разрядности и масштаб, что позволяет сохранять значения,
точность которых превышает возможности типа `decimal`.

<h4 id="type-map-reading-boolean">
  Булевый тип
</h4>

| Тип ClickHouse | Тип .NET по умолчанию |
| - | - |
| `Bool` | `bool` |

<h4 id="type-map-reading-strings">
  Строковые типы
</h4>

| Тип ClickHouse | Тип .NET по умолчанию | Также можно прочитать как |
| - | - | - |
| `String` | `string` | `byte[]` |
| `FixedString(N)` | `byte[]` | `string` |

Если данные могут не соответствовать кодировке UTF-8, используйте `IStringColumn.GetBytes(row)` или считывайте их как `byte[]`.
`FixedString(N)` сохраняет все `N` байтов, включая нулевые байты в конце.

<h4 id="type-map-reading-datetime">
  Типы даты и времени
</h4>

| Тип ClickHouse | Тип .NET по умолчанию | Также можно прочитать как |
| - | - | - |
| `Date` | `DateOnly` | — |
| `Date32` | `DateOnly` | — |
| `DateTime`, `DateTime('timezone')` | `uint` — секунды с начала эпохи Unix | `DateTimeOffset`, `DateTime` |
| `DateTime64(S)`, `DateTime64(S, 'timezone')` | `long` — число единиц с масштабом `S` | `DateTimeOffset`, `DateTime` |
| `Time` | `int` — секунды | `TimeSpan`, `TimeOnly` |
| `Time64(S)` | `long` — число единиц с масштабом `S` | `TimeSpan`, `TimeOnly` |
| От `IntervalNanosecond` до `IntervalYear` | `long` | — |

По умолчанию значения читаются как целые числа, чтобы при чтении по столбцам сохранялось их точное представление в передаваемых данных. При
преобразовании в календарные значения используется часовой пояс, указанный в типе, а если он не указан — часовой пояс сеанса. При чтении
`Time` или `Time64` как `TimeOnly` генерируется исключение, если значение отрицательное или составляет не менее 24 часов. Для
работы с полным диапазоном значений ClickHouse используйте `TimeSpan`.

<h4 id="type-map-reading-enum">
  Типы Enum
</h4>

| Тип ClickHouse | Тип .NET по умолчанию | Также можно прочитать как |
| - | - | - |
| `Enum8(...)` | `sbyte` | строковую метку (`string`) |
| `Enum16(...)` | `short` | строковую метку (`string`) |

`IEnumColumn` также позволяет получить объявленное соответствие меток их числовым значениям.

<h4 id="type-map-reading-other-scalar">
  Другие скалярные типы
</h4>

| Тип ClickHouse | Тип .NET по умолчанию |
| - | - |
| `UUID` | `Guid` |
| `IPv4` | `IPAddress` |
| `IPv6` | `IPAddress` |
| `Nothing` | `object` (`null` в каждой строке) |

<h4 id="type-map-reading-composite">
  Составные типы
</h4>

| Тип ClickHouse | Тип .NET по умолчанию | Столбцовое представление |
| - | - | - |
| `Nullable(T)` | `T?` | `INullableColumn<T>` |
| `Array(T)` | `T[]` | `IArrayColumn<T>` |
| `Tuple(T1, ..., Tn)` | `(T1, ..., Tn)` | `ITupleColumn` |
| `Map(K, V)` | `KeyValuePair<K, V>[]` | `IMapColumn<K, V>` |
| `LowCardinality(T)` | Такой же, как `T` | `ILowCardinalityColumn<T>` |
| `Nested(...)` | `object[][]` | `INestedColumn` |

Кортежи могут содержать до семи элементов. Столбец `Nested` не зависит от числа полей; его удобнее всего читать
через `INestedColumn`, который предоставляет доступ к отдельным столбцам с именами полей и общим смещениям строк без
выделения памяти под записи, упакованные в объекты. При заданном в ClickHouse по умолчанию параметре `flatten_nested = 1` вложенные поля вместо этого поступают как
отдельные столбцы `Array(T)` с точкой в имени.

<h4 id="type-map-reading-variant-dynamic-json">
  Типы Variant, Dynamic и JSON
</h4>

| Тип ClickHouse | Тип .NET по умолчанию | Столбцовое представление |
| - | - | - |
| `Variant(T1, ..., Tn)` | `object` | `IVariantColumn` |
| `Dynamic` | `object` | `IDynamicColumn` |
| `JSON` | `string` | `IColumn<string>` |

`Variant` и `Dynamic` возвращают для каждой строки значение CLR выбранного типа или `null`. Их
столбцовые представления предоставляют поток дискриминаторов и по одному дочернему столбцу с заданным типом для каждого типа, встречающегося во время выполнения. Это позволяет избежать
упаковки значений при обработке данных по столбцам.

TCP-клиент считывает `JSON` с помощью сериализации ClickHouse в формате String. По умолчанию клиент включает
`output_format_native_write_json_as_string = 1`; если отключить этот параметр, чтение JSON
не поддерживается. ClickHouse разбирает и нормализует JSON, поэтому возвращаемый текст может отличаться от вставленного
порядком ключей, пробелами и форматированием чисел.

<h4 id="type-map-reading-geometry">
  Геометрические типы
</h4>

| Тип ClickHouse | Тип .NET по умолчанию |
| - | - |
| `Point` | `(double, double)` |
| `Ring` | `(double, double)[]` |
| `LineString` | `(double, double)[]` |
| `Polygon` | `(double, double)[][]` |
| `MultiLineString` | `(double, double)[][]` |
| `MultiPolygon` | `(double, double)[][][]` |
| `Geometry` | `object` |

`Geometry` представляется интерфейсом `IVariantColumn`, объединяющим шесть геометрических типов.

<h4 id="type-map-reading-qbit">
  Тип QBit
</h4>

| Тип ClickHouse | Тип .NET по умолчанию |
| - | - |
| `QBit(Int8, N)` | `sbyte[]` |
| `QBit(BFloat16, N)` | `float[]` |
| `QBit(Float32, N)` | `float[]` |
| `QBit(Float64, N)` | `double[]` |

TCP-клиент поддерживает форму `QBit(T, N)` с двумя аргументами, без параметра stride. Структура с тремя аргументами
и параметром stride не поддерживается.

<h4 id="type-map-reading-aggregate-function">
  Типы AggregateFunction
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| `SimpleAggregateFunction(function, T)` | Тот же, что и `T` |
| `AggregateFunction(function, ...)` | Не поддерживается |

Столбец типа `AggregateFunction` содержит промежуточное состояние, зависящее от функции. Получите окончательный результат на
сервере, например с помощью `sumMerge(column)`, и считайте полученное значение.

<h3 id="type-map-writing">
  Сопоставление типов: запись в ClickHouse
</h3>

Каждая запись указывает тип CLR одной строки в `IColumn`, переданном в `InsertAsync`. Соответствие типов
точное: TCP-клиент не выполняет неявных преобразований по аналогии с `Convert`.

<h4 id="type-map-writing-integer">
  Целочисленные типы
</h4>

| Тип ClickHouse | Допустимый тип .NET |
| - | - |
| `Int8` | `sbyte` |
| `UInt8` | `byte` |
| `Int16` | `short` |
| `UInt16` | `ushort` |
| `Int32` | `int` |
| `UInt32` | `uint` |
| `Int64` | `long` |
| `UInt64` | `ulong` |
| `Int128` | `Int128` |
| `UInt128` | `UInt128` |
| `Int256` | `Int256` |
| `UInt256` | `UInt256` |

<h4 id="type-map-writing-floating-point">
  Типы с плавающей запятой
</h4>

| Тип ClickHouse | Допустимый тип .NET |
| - | - |
| `Float32` | `float` |
| `Float64` | `double` |
| `BFloat16` | `float` |

При записи в формате `BFloat16` каждое значение `float` усекается до 16-битного представления с плавающей запятой (brain floating point).

<h4 id="type-map-writing-decimal">
  Десятичные типы
</h4>

| Тип ClickHouse | Допустимый тип .NET |
| - | - |
| `Decimal(P, S)`, где `P <= 18` | `decimal` |
| `Decimal(P, S)`, где `P > 18` | `ClickHouseTcpDecimal` |
| `Decimal32(S)` | `decimal` |
| `Decimal64(S)` | `decimal` |
| `Decimal128(S)` | `ClickHouseTcpDecimal` |
| `Decimal256(S)` | `ClickHouseTcpDecimal` |

При записи генерируется исключение `OverflowException`, если количество цифр мантиссы после масштабирования превышает заданную точность.

<h4 id="type-map-writing-boolean">
  Булевый тип
</h4>

| Тип ClickHouse | Допустимый тип .NET |
| - | - |
| `Bool` | `bool` |

<h4 id="type-map-writing-strings">
  Строковые типы
</h4>

| Тип ClickHouse | Допустимый тип .NET | Примечания |
| - | - | - |
| `String` | `string`, `byte[]` | Строки кодируются в UTF-8; массивы байтов записываются без изменений. |
| `FixedString(N)` | `byte[]` | Каждое значение должно содержать ровно `N` байт. |

`FixedString(N)` намеренно не принимает значения типа `string`: их необходимо явно закодировать и дополнить до нужной длины, чтобы
двоичное представление каждого значения было однозначным.

<h4 id="type-map-writing-datetime">
  Типы даты и времени
</h4>

| Тип ClickHouse | Допустимые типы .NET |
| - | - |
| `Date`, `Date32` | `DateOnly` |
| `DateTime`, `DateTime('timezone')` | `uint`, `DateTimeOffset`, `DateTime` |
| `DateTime64(S)`, `DateTime64(S, 'timezone')` | `long`, `DateTimeOffset`, `DateTime` |
| `Time` | `int`, `TimeSpan`, `TimeOnly` |
| `Time64(S)` | `long`, `TimeSpan`, `TimeOnly` |
| От `IntervalNanosecond` до `IntervalYear` | `long` |

Целочисленные значения без преобразования задаются в единицах измерения, указанных в таблице для чтения. `DateTimeOffset` сохраняет момент времени.
Для `DateTime` учитывается значение `Kind`: значение `Unspecified` интерпретируется как местное время в часовом поясе
столбца, а если для столбца часовой пояс не задан — в часовом поясе сеанса.

<h4 id="type-map-writing-enum">
  Типы Enum
</h4>

| Тип ClickHouse | Допустимый тип .NET |
| - | - |
| `Enum8(...)` | `sbyte`, имя элемента перечисления в виде `string` |
| `Enum16(...)` | `short`, имя элемента перечисления в виде `string` |

Неизвестное имя элемента перечисления отклоняется до отправки данных.

<h4 id="type-map-writing-other-scalar">
  Другие скалярные типы
</h4>

| Тип ClickHouse | Допустимый тип .NET |
| - | - |
| `UUID` | `Guid` |
| `IPv4` | `IPAddress` |
| `IPv6` | `IPAddress` |
| `Nothing` | Не поддерживается |

<h4 id="type-map-writing-composite">
  Составные типы
</h4>

Допустимые структуры определяются рекурсивно: `Array(Nullable(DateTime))` принимает `DateTime?[]` для каждой
строки, а `Map(String, UInt32)` — `KeyValuePair<string, uint>[]`.

| Тип ClickHouse | Допустимый тип .NET для одной строки |
| - | - |
| `Nullable(T)` | Вариант типа, допустимого для `T`, со значением NULL |
| `Array(T)` | `T[]` |
| `Tuple(T1, ..., Tn)` | `(T1, ..., Tn)` |
| `Map(K, V)` | `KeyValuePair<K, V>[]` |
| `LowCardinality(T)` | Тот же, что и для `T` |
| `Nested(...)` | Нет построчного представления CLR |

При прямой записи `Nested` используется плотное представление столбцов, полученное при предыдущем чтении. Для вставок,
формируемых приложением, используйте представление `flatten_nested = 1` по умолчанию и записывайте каждое поле `Array(T)` с именем, содержащим точку,
в отдельный столбец.

<h4 id="type-map-writing-variant-dynamic-json">
  Типы Variant, Dynamic и JSON
</h4>

| Тип ClickHouse | Допустимый тип .NET | Примечания |
| - | - | - |
| `Variant(T1, ..., Tn)` | `object` | Для каждого значения, отличного от null, вариант выбирается по его типу во время выполнения. |
| `Dynamic` | `object` | Тип каждого значения, отличного от null, определяется во время выполнения. |
| `JSON` | `string` | Значение должно содержать корректный текст JSON. |

Для `Variant` тип значения во время выполнения должен однозначно соответствовать типу CLR по умолчанию для одного из вариантов. Дополнительные
преобразования, например запись `DateTime` в вариант `DateTime`, внутри `Variant` не применяются;
вместо них используйте исходный тип по умолчанию для соответствующего варианта. `Dynamic` поддерживает скалярные значения,
а также значения массивов, отображений и кортежей, в том числе вложенные, если их типы поддерживаются.

<h4 id="type-map-writing-geometry">
  Геометрические типы
</h4>

| Тип ClickHouse | Допустимый тип .NET |
| - | - |
| `Point` | `(double, double)` |
| `Ring` | `(double, double)[]` |
| `LineString` | `(double, double)[]` |
| `Polygon` | `(double, double)[][]` |
| `MultiLineString` | `(double, double)[][]` |
| `MultiPolygon` | `(double, double)[][][]` |
| `Geometry` | `object` |

Если столбец `Geometry` создаётся приложением, только `Point` и `MultiPolygon` имеют уникальные структуры CLR.
У `Ring` и `LineString` структура совпадает, как и у `Polygon` и `MultiLineString`, поэтому клиент не может
определить, какой из типов представляет значение `object`. Декодированный столбец `Geometry` сохраняет
дискриминаторы, поэтому его можно повторно вставить без такой неоднозначности.

<h4 id="type-map-writing-qbit">
  Тип QBit
</h4>

| Тип ClickHouse | Допустимый тип .NET |
| - | - |
| `QBit(Int8, N)` | `sbyte[]` длиной `N` |
| `QBit(BFloat16, N)` | `float[]` длиной `N` |
| `QBit(Float32, N)` | `float[]` длиной `N` |
| `QBit(Float64, N)` | `double[]` длиной `N` |

<h4 id="type-map-writing-aggregate-function">
  Типы AggregateFunction
</h4>

| Тип ClickHouse | Допустимый тип .NET |
| - | - |
| `SimpleAggregateFunction(function, T)` | Тот же тип, что и `T` |
| `AggregateFunction(function, ...)` | Не поддерживается |

<h2 id="compression">
  Сжатие
</h2>

По умолчанию клиент использует сжатие LZ4 для записываемых блоков; ClickHouse также по умолчанию
сжимает блоки ответа с помощью LZ4. Сжатие применяется к блокам результатов и блокам, передаваемым при вставке,
а текст запроса, сведения о ходе его выполнения, исключения и другие пакеты протокола передаются без сжатия в обычном формате передачи данных.

Чтобы выбрать кодек для клиента, укажите параметр `Compression` в строке соединения:

```csharp theme={null}
await using var lz4 = new ClickHouseTcpClient(
    "Host=my.clickhouse;Compression=lz4"); // Default

await using var zstd = new ClickHouseTcpClient(
    "Host=my.clickhouse;Compression=zstd");

await using var uncompressed = new ClickHouseTcpClient(
    "Host=my.clickhouse;Compression=none");
```

В эквивалентной конфигурации со строгой типизацией используется `IClickHouseCompressor`:

```csharp theme={null}
using ClickHouse.Driver.Compression;
using ClickHouse.Driver.Tcp;

var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    Compressor = ZstdCompressor.Default,
};

await using var client = new ClickHouseTcpClient(options);
```

Установите `Compressor = null`, чтобы отключить сжатие. Пользовательский компрессор должен поддерживать фрейминг блоков нативного формата;
компрессоры, поддерживающие только тела HTTP-сообщений, не принимаются при создании клиента.

<h3 id="compression-response-codec">
  Выбор кодека для ответа
</h3>

`Compressor` определяет, как сжимаются блоки, записываемые клиентом. Пакет Query нативного протокола
запрашивает у сервера сжатие, но не указывает кодек. Поэтому ClickHouse выбирает кодек для ответа
согласно настройке `network_compression_method`. По умолчанию сервер использует LZ4.

При необходимости задавайте кодек для ответа отдельно для каждой операции:

```csharp theme={null}
var queryOptions = new ClickHouseTcpQueryOptions
{
    Settings = new Dictionary<string, string>
    {
        ["network_compression_method"] = "ZSTD",
    },
};

await foreach (Block block in client.StreamAsync(
    "SELECT * FROM events",
    queryOptions))
{
    // Process the block.
}
```

LZ4 обычно требует меньше ресурсов CPU и лучше всего подходит для быстрых сетей в качестве кодека по умолчанию. ZSTD обычно создаёт
фреймы меньшего размера, но немного сильнее нагружает CPU. Отключить сжатие может быть полезно, если сервер находится на том же хосте
или подключён по другому очень быстрому каналу. Прежде чем менять настройку по умолчанию, проведите измерения на репрезентативных данных.

<h2 id="error-handling">
  Обработка ошибок
</h2>

Ошибки ClickHouse или соединения по собственному протоколу представлены исключениями, унаследованными от `ClickHouseTcpException`,
который, в свою очередь, наследуется от `DbException`:

| Исключение | Значение |
| - | - |
| `ClickHouseTcpServerException` | ClickHouse отклонил рукопожатие или операцию либо не смог выполнить операцию. |
| `ClickHouseTcpConnectionException` | Сбой сокета, согласования TLS или установленного соединения либо истечение тайм-аута `DialTimeout` или `ReadTimeout`. Причина указана в `InnerException`; при тайм-ауте это `TimeoutException`. |
| `ClickHouseTcpProtocolException` | Ответ не соответствовал собственному протоколу либо содержал тип или представление данных при передаче, которое клиент не может обработать. |

`ClickHouseTcpServerException` содержит `RawCode` — точный числовой код, присланный сервером,
и `Code` — соответствующее значение `ClickHouseErrorCode`, если для этого кода есть имя в драйвере. Также в нём доступны
имя исключения сервера в `Name`, трассировка стека в `ServerStackTrace` и вложенные исключения сервера в
`InnerException`.

```csharp theme={null}
try
{
    await client.ExecuteAsync("SELECT * FROM missing_table");
}
catch (ClickHouseTcpServerException ex)
    when (ex.Code == ClickHouseErrorCode.UnknownTable)
{
    Console.WriteLine($"ClickHouse error {ex.RawCode}: {ex.Message}");
}
catch (ClickHouseTcpConnectionException ex)
{
    Console.WriteLine($"Connection failed: {ex.InnerException?.Message}");
}
catch (ClickHouseTcpException ex)
{
    Console.WriteLine($"Native client failure: {ex.Message}");
}
```

Коды, отсутствующие в `ClickHouseErrorCode`, имеют значение `Code == ClickHouseErrorCode.Unknown`; проверяйте
`RawCode`, поскольку перечисление может содержать не все коды ошибок ClickHouse.

Ошибки проверки аргументов и жизненного цикла объектов по-прежнему представлены стандартными для .NET типами исключений.
При отмене операции возникает `OperationCanceledException`. При `DialTimeout` и `ReadTimeout` возникает
`ClickHouseTcpConnectionException` с вложенным исключением `TimeoutException`, а соединение удаляется из пула.
При `PoolTimeout` возникает обычное исключение `TimeoutException`, а не `ClickHouseTcpException`: сбоя
соединения не было. Чтобы устранить проблему, сократите число параллельных операций, увеличьте пул или завершите перебор результатов.

<h3 id="error-handling-retries">
  Повторное выполнение операций
</h3>

Если во время вставки произошёл сбой соединения или истёк тайм-аут, результат операции остаётся неизвестным:
сервер мог записать строки до того, как клиент потерял соединение и не получил ответ. Перед повторной попыткой
задайте для логического батча постоянное значение `DeduplicationToken`:

```csharp theme={null}
var insertOptions = new ClickHouseTcpInsertOptions
{
    DeduplicationToken = batchContentHash,
};

await client.InsertAsync(
    "INSERT INTO events (id, payload) VALUES",
    columns,
    insertOptions);
```

При повторных попытках обработки того же батча используйте прежний токен, а для новых данных — другой. Остальные
операции повторяйте только в том случае, если оператор безопасно выполнять повторно, а конкретный сбой носит временный характер;
в большинстве случаев ошибки сервера требуют не повторной попытки, а изменения запроса или конфигурации.

<h2 id="logging-and-diagnostics">
  Логирование и диагностика
</h2>

TCP-клиент интегрируется с `Microsoft.Extensions.Logging`. Логирование необязательно: без
`LoggerFactory` клиент не создаёт логгеры и не форматирует сообщения лога.

```csharp theme={null}
using ClickHouse.Driver.Tcp;
using Microsoft.Extensions.Logging;

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Debug);
});

var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    LoggerFactory = loggerFactory,
    StatementMaxLength = 100,
};

await using var client = new ClickHouseTcpClient(options);
```

Если клиент зарегистрирован через расширения для внедрения зависимостей, он использует `ILoggerFactory` из контейнера,
если фабрика логирования уже не указана в переданных параметрах.

<h3 id="logging-categories">
  Категории логирования
</h3>

| Категория | События и уровни |
| - | - |
| `ClickHouse.Driver.Tcp.Client` | Начало, завершение, отмена и прекращение выполнения операций, а также количество строк и байтов — на уровне `Debug`; сбои — на уровне `Error`. |
| `ClickHouse.Driver.Tcp.Connection` | Установка соединения, TLS, рукопожатие, версия сервера, ревизия протокола и часовой пояс — на уровне `Debug`; сбои при подключении — на уровне `Warning`. |
| `ClickHouse.Driver.Tcp.Pool` | Повторное использование — на уровне `Trace`; открытие и вывод соединений из эксплуатации, отбрасывание соединений и опустошение пула — на уровне `Debug`; исчерпание пула и фоновые сбои — на уровне `Warning`. |

Настройте категории с помощью стандартных параметров логирования .NET:

```json theme={null}
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "ClickHouse.Driver.Tcp.Client": "Debug",
      "ClickHouse.Driver.Tcp.Connection": "Debug",
      "ClickHouse.Driver.Tcp.Pool": "Debug"
    }
  }
}
```

Сообщение о начале операции записывается с уровнем `Debug` и содержит не более `StatementMaxLength` символов
SQL. По умолчанию — пять символов. Установите значение не выше нуля, чтобы не записывать текст оператора в журналы,
а увеличивайте его только после проверки параметров и литералов на наличие конфиденциальных данных.

<h3 id="logging-server-messages">
  Сообщения лога сервера
</h3>

Интеграция логирования передаёт сведения о работе клиента. Пакеты журнала запросов самого ClickHouse
передаются отдельно в `ClickHouseTcpQueryCallbacks.OnLog`. Чтобы включить их, используйте
настройку сервера `send_logs_level`:

```csharp theme={null}
var serverLogger = loggerFactory.CreateLogger("ClickHouse.Server");

var queryOptions = new ClickHouseTcpQueryOptions
{
    Settings = new Dictionary<string, string>
    {
        ["send_logs_level"] = "warning",
    },
    Callbacks = new ClickHouseTcpQueryCallbacks
    {
        OnLog = block =>
        {
            IColumn<string> messages = block.Column<string>("text");
            for (int row = 0; row < block.RowCount; row++)
            {
                serverLogger.LogWarning("{ClickHouseMessage}", messages[row]);
            }
        },
    },
};
```

Блок журнала предоставляется временно и освобождается после завершения обратного вызова. Обрабатывайте его синхронно или
копируйте значения, которые нужно сохранить. Обратные вызовы должны выполняться быстро и не должны генерировать исключения: исключение в обратном вызове
прерывает операцию и разрывает соответствующее соединение.

<h2 id="opentelemetry">
  OpenTelemetry
</h2>

TCP-клиент создаёт спаны распределённой трассировки с помощью API `System.Diagnostics.Activity` в .NET.
Подпишитесь на источник активности TCP-клиента с помощью провайдера трассировки OpenTelemetry:

```csharp theme={null}
using ClickHouse.Driver.Tcp;
using OpenTelemetry.Trace;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseTcpDiagnostics.ActivitySourceName)
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());
```

Для консольного приложения или настройки вручную:

```csharp theme={null}
using OpenTelemetry;
using OpenTelemetry.Trace;

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(ClickHouseTcpDiagnostics.ActivitySourceName)
    .AddConsoleExporter()
    .Build();
```

Имя источника событий — `ClickHouse.Driver.Tcp`. Оно отличается от имени источника HTTP-клиента, поэтому
события для каждого из этих транспортов можно собирать независимо. Если ни один обработчик не подписан, операции не
создают событий.

<h3 id="opentelemetry-spans-attributes">
  Спаны и атрибуты
</h3>

Клиент создает спаны для SQL-операций, проверок доступности и новых соединений. Спан SQL-операции получает имя
по первому ключевому слову оператора, например `SELECT` или `INSERT`; если его не удается
распознать, используется `query`. Спан `connect` охватывает установление соединения через сокет, согласование TLS и
рукопожатие по нативному протоколу. Спаны SQL-операций также включают время ожидания соединения из пула.

| Атрибут | Описание |
| - | - |
| `db.system.name` | `clickhouse` |
| `db.namespace` | Настроенная база данных |
| `db.operation.name` | Первое ключевое слово SQL, если его удалось определить |
| `db.user` | Настроенное имя пользователя ClickHouse |
| `server.address` | Имя хоста или адрес сервера |
| `server.port` | Определенный порт нативного протокола |
| `db.clickhouse.query_id` | Сгенерированный или заданный вызывающим кодом Query id |
| `db.query.text` | Необязательный, сокращенный текст оператора SQL |
| `db.clickhouse.read_rows` | Количество строк, указанное сервером в пакетах прогресса |
| `db.clickhouse.read_bytes` | Количество байтов, указанное сервером в пакетах прогресса |
| `db.clickhouse.written_rows` | Количество строк, указанное сервером, либо отправленных клиентом при вставке |
| `db.clickhouse.written_bytes` | Количество записанных байтов, если сервер его сообщает |
| `db.clickhouse.elapsed_ns` | Время выполнения на сервере, указанное в пакетах прогресса |
| `db.clickhouse.result_rows` | Количество строк результата из сводки профиля сервера |
| `db.clickhouse.result_bytes` | Количество байтов результата из сводки профиля сервера |
| `error.type` | Тип исключения при неудачной операции |
| `db.response.status_code` | Числовой код ошибки ClickHouse для исключения на сервере |

Счетчики присутствуют, только если у сервера или клиента есть значимые значения для передачи. Например,
при вставке с загрузкой блоков указывается количество строк, отправленных клиентом, но данные о прочитанных строках от
сервера не поступают. Спаны неудачных операций также содержат событие `exception` с типом исключения, сообщением и трассировкой
стека.

<h3 id="opentelemetry-sql-text">
  Текст SQL-запросов и конфиденциальные данные
</h3>

По умолчанию текст SQL-запросов не включается в спаны. Включите его передачу явно и задайте подходящий лимит:

```csharp theme={null}
var options = new ClickHouseTcpClientOptions
{
    Host = "my.clickhouse",
    IncludeSqlInActivityTags = true,
    StatementMaxLength = 1_000,
};
```

`StatementMaxLength` ограничивает длину атрибута `db.query.text` и SQL-текста в отладочных журналах. Если значение равно нулю
или меньше, SQL-текст не включается ни туда, ни туда. Также проверяйте экспортируемые сообщения об исключениях и трассировки стека:
ошибки сервера могут содержать подробности запросов.

<h3 id="opentelemetry-trace-context">
  Передача контекста трассировки
</h3>

Если `Activity.Current` использует формат идентификаторов W3C, клиент записывает идентификатор трассировки, идентификатор спана, флаги
и состояние трассировки в пакет Query нативного протокола. После этого ClickHouse может создавать серверные спаны в той же
распределённой трассировке. Сбор серверных спанов и сэмплирование настраиваются на стороне ClickHouse; например,
вероятность сэмплирования можно задать для каждого запроса:

```csharp theme={null}
var queryOptions = new ClickHouseTcpQueryOptions
{
    Settings = new Dictionary<string, string>
    {
        ["opentelemetry_start_trace_probability"] = "1",
    },
};

await client.ExecuteAsync("SELECT count() FROM events", queryOptions);
```

Передаваемый Query id можно использовать вместе с идентификатором трассировки, чтобы коррелировать клиентский спан с
`system.query_log` и, если ведение журнала спанов включено, с таблицей ClickHouse `system.opentelemetry_span_log`.

<h2 id="limitations">
  Ограничения
</h2>

TCP-клиент пока экспериментальный и поддерживает не все возможности HTTP-клиента.

<h3 id="limitations-http-only">
  Возможности, доступные только через HTTP
</h3>

TCP API не является провайдером ADO.NET и не может использоваться с ORM. Он также не поддерживает
ввод и вывод в форматах CSV, JSONEachRow, Parquet или в виде сырого потока; аутентификацию по схеме bearer и пользовательские HTTP-заголовки;
пользовательские правила определения типов параметров, форматирования и преобразования при чтении; указание базы данных
или роли для каждого запроса. Если приложению нужна хотя бы одна из этих возможностей, используйте
[HTTP-клиент](./http).

<h3 id="limitations-sql-parameters">
  Параметры SQL
</h3>

Поддерживаются только плейсхолдеры ClickHouse формата `{name:Type}`; TCP-клиент не преобразует
плейсхолдеры ADO.NET вида `@name`. В версиях ClickHouse с 25.8 по 26.6 имена параметров, совпадающие
с именами настроек сервера, например `limit` или `offset`, могут быть приняты за настройки и отклонены. Если необходимо поддерживать
эти версии, переименуйте такие параметры, например в `row_limit`.

<h3 id="limitations-data-types">
  Поддержка типов данных
</h3>

Полное соответствие типов описано в разделе [Поддерживаемые типы данных](#supported-data-types). У
текущего нативного кодека есть следующие ограничения:

* Tuple может содержать не более семи элементов.
* Промежуточные состояния `AggregateFunction(function, ...)` не поддерживаются. Завершайте их обработку на
  сервере, например с помощью `sumMerge(column)`, и считывайте готовый результат.
* Поддерживается только структура `QBit(T, N)` с двумя аргументами, без разбиения измерений на группы.
* Для `JSON` используется сериализация ClickHouse в тип String, поэтому необходима настройка
  `output_format_native_write_json_as_string = 1`, которую клиент запрашивает по умолчанию.
* При вставке данных, сформированных приложением, для `Nested(...)` недоступно построчное представление, если
  `flatten_nested = 0`. Вместо него используйте развёрнутые столбцы `Array(T)` с именами через точку, применяемые по умолчанию.
* Для столбца `Geometry`, сформированного приложением, нельзя автоматически определить тип среди вариантов с одинаковой структурой CLR,
  например `Ring` и `LineString`. Вставляйте данные с конкретным геометрическим типом либо повторно вставляйте декодированный
  столбец `Geometry`, сохранивший свои дискриминаторы.

<h3 id="limitations-retries">
  Автоматические повторные попытки
</h3>

Клиент не повторяет операции автоматически после сбоя. Повторяйте только те операции, которые можно безопасно
выполнить ещё раз. Если результат вставки неизвестен, перед повторной попыткой задайте для неё неизменный `DeduplicationToken`.
Подробнее см. в разделе [«Повторные попытки выполнения операций»](#error-handling-retries).
