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

> Официальный клиент C# для подключения к ClickHouse.

# Клиент ClickHouse для C#

export const Image = ({img, alt, size = "lg", background}) => {
  const normalizedSize = ["sm", "md", "lg"].includes(size) ? size : "lg";
  const backgroundColor = background === "white" ? "white" : background === "black" ? "rgb(31 31 28)" : undefined;
  return <div className={`ch-image-${normalizedSize}`}>
      <Frame>
        <img src={img} alt={alt} style={{
    backgroundColor
  }} />
      </Frame>
    </div>;
};

Официальный клиент C# для подключения к ClickHouse.
Исходный код клиента доступен в [репозитории GitHub](https://github.com/ClickHouse/clickhouse-cs).
Изначально разработан [Oleg V. Kozlyuk](https://github.com/DarkWanderer).

Библиотека предоставляет два основных API:

* **`ClickHouseClient`** (рекомендуется): высокоуровневый потокобезопасный клиент, предназначенный для использования в качестве singleton. Предоставляет простой асинхронный API для запросов и массовых вставок. Лучше всего подходит для большинства приложений.

* **ADO.NET** (`ClickHouseDataSource`, `ClickHouseConnection`, `ClickHouseCommand`): стандартные абстракции базы данных в .NET. Требуются для интеграции с ORM (Dapper, Linq2db) и в случаях, когда нужна совместимость с ADO.NET. `ClickHouseBulkCopy` — вспомогательный класс для эффективной вставки данных с использованием ADO.NET-соединения. `ClickHouseBulkCopy` устарел и будет удалён в одном из будущих релизов; вместо него используйте `ClickHouseClient.InsertBinaryAsync`.

Оба API используют один и тот же базовый пул HTTP-соединений и могут применяться вместе в одном приложении.

<h2 id="migration-guide">
  Руководство по миграции
</h2>

1. Обновите файл `.csproj`: укажите новое имя пакета `ClickHouse.Driver` и [последнюю версию на NuGet](https://www.nuget.org/packages/ClickHouse.Driver).
2. Замените в кодовой базе все упоминания `ClickHouse.Client` на `ClickHouse.Driver`.

***

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

`ClickHouse.Driver` поддерживает следующие версии .NET:

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

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

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

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

Установите пакет из NuGet:

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

Или через диспетчер пакетов NuGet:

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

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

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

// Создание клиента (обычно как singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// Выполнение запроса
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);
```

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

Существует два способа настроить подключение к ClickHouse:

* **Строка подключения:** пары ключ/значение, разделённые точкой с запятой, которые задают хост, учётные данные для аутентификации и другие параметры подключения.
* **Объект `ClickHouseClientSettings`:** строго типизированный объект конфигурации, который можно загрузить из файлов конфигурации или задать в коде.

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

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

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| Host | `string` | `"localhost"` | `Host` | Имя хоста или IP-адрес сервера ClickHouse |
| Port | `ushort` | 8123 (HTTP) / 8443 (HTTPS) | `Port` | Номер порта; значение по умолчанию зависит от протокола |
| Username | `string` | `"default"` | `Username` | Имя пользователя для аутентификации |
| Password | `string` | `""` | `Password` | Пароль для аутентификации |
| Database | `string` | `""` | `Database` | База данных по умолчанию; если значение пустое, используются настройки сервера или пользователя по умолчанию |
| Protocol | `string` | `"http"` | `Protocol` | Протокол подключения: `"http"` или `"https"` |
| Path | `string` | `null` | `Path` | URL-путь для сценариев с использованием обратного прокси (например, `/clickhouse`) |
| Timeout | `TimeSpan` | 2 минуты | `Timeout` | Тайм-аут операции (в строке подключения хранится в секундах) |

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

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| UseCompression | `bool` | `true` | `Compression` | Управляет сжатием на транспортном уровне в обоих направлениях для обычного запроса: просит сервер сжать ответ (`enable_http_compression`; кодек задаётся через `AcceptEncoding`, причём явное значение может запросить его даже при выключенной опции) **и** сжимает тело запроса с помощью gzip — за исключением режима `UseFormDataParameters`, чьё multipart-тело всегда отправляется без сжатия. Бинарные вставки это свойство не учитывают; они используют `InsertOptions.Compressor` — см. [Сжатие при вставке](#insert-compression) |
| AcceptEncoding | `string` | `null` | `AcceptEncoding` | Заголовок `Accept-Encoding`, отправляемый с каждым запросом; заменяет кодеки, которые драйвер объявляет по умолчанию (`zstd, lz4, gzip, deflate`). Всё, что вернёт сервер, декодируется прозрачно. См. [Распаковка ответа](#response-decompression) |
| UseCustomDecimals | `bool` | `true` | `UseCustomDecimals` | Использовать `ClickHouseDecimal` для чисел произвольной точности; если `false`, используется .NET `decimal` (предел — 128 бит) |
| ReadStringsAsByteArrays | `bool` | `false` | `ReadStringsAsByteArrays` | Читать столбцы `String` и `FixedString` как `byte[]` вместо `string`; полезно для бинарных данных |
| UseFormDataParameters | `bool` | `false` | `UseFormDataParameters` | Отправлять параметры в виде form data, а не в строке запроса URL |
| ReadBufferSize | `int` | `65536` (64 KiB) | `ReadBufferSize` | Размер буфера в байтах, используемого для чтения HTTP-ответов на запросы. Драйвер берёт буфер из общего пула и возвращает его при освобождении средства чтения, поэтому память не выделяется на каждый запрос. Увеличьте его, чтобы сократить число повторных заполнений буфера для больших результирующих наборов. Драйвер держит по одному буферу на каждое параллельно работающее средство чтения, поэтому потребление памяти растёт с размером буфера и числом параллельных средств чтения. См. [Буферы](#perf-buffers). |
| ParameterTypeResolver | `IParameterTypeResolver` | `null` | — | Пользовательский резолвер для сопоставления типов параметров в стиле `@`; см. [Пользовательское сопоставление типов параметров](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | `null` | — | Пользовательский форматтер для сериализации значений параметров; см. [Пользовательское форматирование значений параметров](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | `null` | — | Пользовательское преобразование, применяемое к значениям, возвращаемым средством чтения данных; см. [Пользовательское преобразование значений при чтении](#read-value-conversion) |
| JsonReadMode | `JsonReadMode` | `Binary` | `JsonReadMode` | Как возвращаются данные JSON: `Binary` (возвращает `JsonObject`) или `String` (возвращает сырую JSON-строку) |
| JsonWriteMode | `JsonWriteMode` | `String` | `JsonWriteMode` | Как отправляются данные JSON: `String` (сериализует через `JsonSerializer`, принимает любые входные данные) или `Binary` (только зарегистрированные POCO с подсказками типов) |
| MapReadMode | `MapReadMode` | `Dictionary` | `MapReadMode` | Как возвращаются данные `Map(K, V)`: `Dictionary` (возвращает `Dictionary<K, V>`; при повторяющемся ключе сохраняется только последнее значение) или `KeyValuePairs` (возвращает `List<KeyValuePair<K, V>>`, сохраняя все пары). См. [Тип Map](#type-map-reading-map) |
| AllowDuplicateJsonKeys | `bool` | `false` | `AllowDuplicateJsonKeys` | Как читать строку типа `JSON`, у которой пересекающиеся пути одновременно содержат значение. При `false` выбрасывается исключение, поскольку сохранение одного значения означает потерю другого; при `true` сохраняется то, которое идёт в строке последним. См. [Пересекающиеся пути](#type-map-reading-json) |

<h3 id="session-management">
  Управление сеансами
</h3>

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| UseSession | `bool` | `false` | `UseSession` | Включает сеансы с сохранением состояния; запросы выполняются последовательно |
| SessionId | `string` | `null` | `SessionId` | Идентификатор сеанса; GUID генерируется автоматически, если `null` и `UseSession` имеет значение `true` |

<Note>
  Флаг `UseSession` включает сохранение сеанса на сервере, что позволяет использовать операторы `SET` и временные таблицы. Сеансы сбрасываются после 60 секунд бездействия (тайм-аут по умолчанию). Время жизни сеанса можно увеличить, задав настройку сеанса через команды ClickHouse или конфигурацию сервера.

  Класс `ClickHouseConnection` обычно поддерживает параллельную работу (несколько потоков могут выполнять запросы одновременно). Однако при включении флага `UseSession` для одного подключения в любой момент времени будет доступен только один активный запрос (это ограничение на стороне сервера).
</Note>

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

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| SkipServerCertificateValidation | `bool` | `false` | — | Пропустить проверку HTTPS-сертификата; **не использовать в продакшне** |

<h3 id="http-client-configuration">
  Конфигурация HTTP-клиента
</h3>

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| HttpClient | `HttpClient` | `null` | — | Пользовательский предварительно настроенный экземпляр HttpClient |
| HttpClientFactory | `IHttpClientFactory` | `null` | — | Пользовательская фабрика для создания экземпляров HttpClient |
| HttpClientName | `string` | `null` | — | Имя, которое HttpClientFactory использует для создания конкретного клиента |

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

| Свойство | Тип | По умолчанию | Ключ строки подключения | Описание |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | Фабрика логгеров для диагностического логирования |
| EnableDebugMode | `bool` | `false` | — | Включает сетевую трассировку .NET (требуется LoggerFactory с уровнем Trace); **существенно влияет на производительность** |

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

| Property | Type | Default | Connection String Key | Description |
| - | - | - | - | - |
| CustomSettings | `IDictionary<string, object>` | Пусто | префикс `set_*` | настройки сервера ClickHouse, см. примечание ниже |
| Roles | `IReadOnlyList<string>` | Пусто | `Roles` | Роли ClickHouse, разделённые запятыми (например, `Roles=admin,reader`) |
| ApplicationInfo | `IReadOnlyDictionary<string, string>` | Пусто | — | Произвольные теги, добавляемые в HTTP-заголовок `User-Agent` для атрибуции запросов приложению. |

<Note>
  Если вы задаёте пользовательские настройки через строку подключения, используйте префикс `set_`, например: "set\_max\_threads=4". Если вы используете объект ClickHouseClientSettings, префикс `set_` указывать не нужно.

  Полный список доступных настроек см. [здесь](/ru/reference/settings/session-settings).
</Note>

***

<h3 id="connection-string-examples">
  Примеры строк подключения
</h3>

<h4 id="basic-connection">
  Базовое подключение
</h4>

```text theme={null}
Host=localhost;Port=8123;Username=default;Password=secret;Database=mydb
```

<h4 id="with-custom-clickhouse-settings">
  С пользовательскими настройками ClickHouse
</h4>

```text theme={null}
Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000
```

***

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

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

| Свойство | Тип | Описание |
| - | - | - |
| QueryId | `string` | Пользовательский идентификатор запроса для отслеживания в `system.query_log` или отмены |
| Database | `string` | Переопределяет базу данных по умолчанию для этого запроса |
| Roles | `IReadOnlyList<string>` | Переопределяет роли клиента для этого запроса |
| CustomSettings | `IDictionary<string, object>` | Настройки сервера ClickHouse для этого запроса (например, `max_threads`) |
| CustomHeaders | `IDictionary<string, string>` | Дополнительные HTTP-заголовки для этого запроса |
| UseSession | `bool?` | Переопределяет поведение сеанса для этого запроса |
| SessionId | `string` | Идентификатор сеанса для этого запроса (требуется `UseSession = true`) |
| BearerToken | `string` | Переопределяет токен аутентификации для этого запроса |
| ParameterTypeResolver | `IParameterTypeResolver` | Переопределяет резолвер уровня клиента для сопоставления типов параметров в стиле `@`; см. [Пользовательское сопоставление типов параметров](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | Переопределяет форматтер уровня клиента для сериализации значений параметров в стиле `@`; см. [Пользовательское форматирование значений параметров](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | Переопределяет преобразование уровня клиента, применяемое к значениям, возвращаемым средством чтения данных; см. [Пользовательское преобразование значений при чтении](#read-value-conversion) |
| MaxExecutionTime | `TimeSpan?` | Тайм-аут запроса на стороне сервера (передаётся как настройка `max_execution_time`); сервер отменяет запрос при превышении |
| AcceptEncoding | `string` | Переопределяет `Accept-Encoding` для отдельного запроса (например, `"br"`, `"identity"`), имеет приоритет над `ClickHouseClientSettings.AcceptEncoding`; также принудительно задаёт `enable_http_compression=1` в URL. См. [Сжатие передачи для отдельного запроса](#per-query-accept-encoding). |

**Пример:**

```csharp theme={null}
var options = new QueryOptions
{
    QueryId = "report-2024-001",
    Database = "analytics",
    CustomSettings = new Dictionary<string, object>
    {
        { "max_threads", 4 },
        { "max_memory_usage", 10_000_000_000 }
    },
    MaxExecutionTime = TimeSpan.FromMinutes(5)
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);
```

***

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

`InsertOptions` дополняет `QueryOptions` настройками, специфичными для массовой вставки через `InsertBinaryAsync`.

| Свойство | Тип | По умолчанию | Описание |
| - | - | - | - |
| BatchSize | `int` | 100,000 | Количество строк в батче |
| MaxDegreeOfParallelism | `int` | 1 | Количество параллельных загрузок батчей |
| Format | `RowBinaryFormat` | `RowBinary` | Бинарный формат: `RowBinary` или `RowBinaryWithDefaults` |
| Compressor | `IClickHouseCompressor` | `ZstdCompressor.Default` | Кодек, применяемый к телу вставки (`Content-Encoding`). При `null` данные отправляются без сжатия. См. [Сжатие при вставке](#insert-compression) |
| QueryPlacement | `InsertQueryPlacement` | `Body` | Куда отправляется оператор `INSERT INTO ... FORMAT ...`: `Body` (перед строками) или `Url` (в виде URL-параметра `query`). См. [Размещение запроса вставки](#insert-query-placement) |
| ColumnTypes | `IReadOnlyDictionary<string, string>` | `null` | Имя столбца → строка типа ClickHouse. Если задано, запрос для определения схемы пропускается. |
| UseSchemaCache | `bool` | `false` | Кэшировать полную схему таблицы для каждой пары (database, table) на всё время жизни клиента. |

Все свойства `QueryOptions` также доступны в `InsertOptions`.

**Пример:**

```csharp theme={null}
var insertOptions = new InsertOptions
{
    BatchSize = 50_000,
    MaxDegreeOfParallelism = 4,
    QueryId = "bulk-import-001"
};

long rowsInserted = await client.InsertBinaryAsync(
    "my_table",
    columns,
    rows,
    insertOptions
);
```

<h4 id="skip-schema-query">
  Пропуск запроса для определения схемы
</h4>

По умолчанию `InsertBinaryAsync` перед каждой вставкой отправляет запрос `SELECT ... WHERE 1=0`, чтобы определить типы столбцов. В сценариях с высокой пропускной способностью эти накладные расходы можно исключить двумя способами:

**Вариант 1: Явно указать типы столбцов**

Если схема таблицы известна на этапе компиляции, передайте её напрямую через `ColumnTypes`. В этом случае запрос схемы вообще не отправляется:

```csharp theme={null}
var options = new InsertOptions
{
    ColumnTypes = new Dictionary<string, string>
    {
        ["id"] = "UInt64",
        ["name"] = "Nullable(String)",
        ["score"] = "Float32",
    },
};

await client.InsertBinaryAsync("my_table", ["id", "name", "score"], rows, options);
```

**Вариант 2: Кэшируйте схему**

Если вы многократно выполняете вставку в одну и ту же таблицу, установите `UseSchemaCache = true`, чтобы запросить схему один раз и повторно использовать её для последующих вставок через тот же экземпляр `ClickHouseClient`:

```csharp theme={null}
var options = new InsertOptions { UseSchemaCache = true };

// Первый вызов получает схему с сервера
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// Второй вызов использует кэшированную схему — без лишних обращений к серверу
await client.InsertBinaryAsync("my_table", columns, batch2, options);
```

<Note>
  * `ColumnTypes` имеет приоритет над `UseSchemaCache`. Если заданы оба параметра, используются явно указанные типы.
  * Кэш схемы не отслеживает изменения, внесённые командой `ALTER TABLE`. Если вы изменяете схему таблицы, создайте новый `ClickHouseClient` или не используйте `UseSchemaCache` для этой таблицы.
  * Кэш привязан к экземпляру `ClickHouseClient`, а в качестве ключа используются (database, table). Разные подмножества столбцов одной и той же таблицы используют одну общую кэшированную схему.
</Note>

<h2 id="clickhouse-client">
  ClickHouseClient
</h2>

`ClickHouseClient` — рекомендуемый API для работы с ClickHouse. Он потокобезопасен, рассчитан на использование как singleton и самостоятельно управляет пулом HTTP-соединений.

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

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

Сведения о вашем сервисе ClickHouse Cloud доступны в консоли ClickHouse Cloud.

Выберите сервис и нажмите **Connect**:

<Image img="https://mintcdn.com/private-7c7dfe99-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/cloud-connect-button.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=d059c1bbcc7317ff8df85b20189e65f4" size="md" alt="Кнопка подключения сервиса ClickHouse Cloud" border width="998" height="932" data-path="images/_snippets/cloud-connect-button.webp" />

Выберите **C#**. Ниже отобразятся сведения о подключении.

<Image img="https://mintcdn.com/private-7c7dfe99-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/connection-details-csharp.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=487b14816a8a8711d46ae022d82d74ef" size="md" alt="Сведения о подключении C# для ClickHouse Cloud" border width="851" height="805" data-path="images/_snippets/connection-details-csharp.webp" />

Если вы используете самоуправляемый ClickHouse, сведения о подключении задаёт ваш администратор ClickHouse.

Использование строки подключения:

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

using var client = new ClickHouseClient("Host=localhost;Username=default;Password=secret");
```

Или с помощью `ClickHouseClientSettings`:

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

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    Username = "default",
    Password = "secret"
};
using var client = new ClickHouseClient(settings);
```

Для сценариев с инъекцией зависимостей используйте `IHttpClientFactory`:

```csharp theme={null}
// In your DI configuration. No AutomaticDecompression needed — the driver decodes
// compressed responses itself, and a mask here would widen its Accept-Encoding.
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
});

// Create client with factory
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = new ClickHouseClient("Host=localhost", factory, "ClickHouse");
```

<Note>
  `ClickHouseClient` рассчитан на длительное использование и совместное использование во всём приложении. Создайте его один раз (обычно как singleton) и затем повторно используйте для всех операций с базой данных. Клиент сам управляет пулом HTTP-соединений.
</Note>

***

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

Используйте `ExecuteNonQueryAsync` для команд, которые не возвращают результатов:

```csharp theme={null}
// Создать таблицу
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// Удалить таблицу
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");
```

Используйте `ExecuteScalarAsync`, чтобы получить единственное значение:

```csharp theme={null}
var count = await client.ExecuteScalarAsync("SELECT count() FROM default.my_table");
Console.WriteLine($"Количество строк: {count}");

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Версия сервера: {version}");
```

***

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

<h4 id="parameterized-inserts">
  Параметризованные вставки
</h4>

Для вставки данных с помощью параметризованных запросов используйте `ExecuteNonQueryAsync`. Типы параметров должны быть указаны в SQL с использованием синтаксиса `{name:Type}`:

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

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("id", 1L);
parameters.AddParameter("name", "Alice");

await client.ExecuteNonQueryAsync(
    "INSERT INTO default.my_table (id, name) VALUES ({id:Int64}, {name:String})",
    parameters
);
```

***

<h4 id="bulk-insert">
  Массовая вставка
</h4>

Используйте `InsertBinaryAsync` для эффективной вставки большого количества строк. Метод передает данные в потоковом режиме в нативном бинарном формате строк ClickHouse, поддерживает параллельную загрузку батчей и позволяет избежать ошибок "URL too long", которые могут возникать при параметризованных запросах.

```csharp theme={null}
// Подготовка данных как IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

var columns = new[] { "id", "name" };

// Базовая вставка
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");
```

Для больших объёмов данных настройте пакетную обработку и степень параллелизма с помощью `InsertOptions`:

```csharp theme={null}
var options = new InsertOptions
{
    BatchSize = 100_000,           // Строк в батче (по умолчанию: 100 000)
    MaxDegreeOfParallelism = 4     // Параллельная загрузка батчей (по умолчанию: 1)
};
```

<Note>
  * Перед вставкой клиент автоматически получает структуру таблицы с помощью `SELECT * FROM <table> WHERE 1=0`. Передаваемые значения должны соответствовать типам целевых столбцов. Чтобы пропустить этот запрос, используйте [`InsertOptions.ColumnTypes` или `InsertOptions.UseSchemaCache`](#skip-schema-query).
  * Если `MaxDegreeOfParallelism > 1`, батчи загружаются параллельно. Сеансы несовместимы с параллельной вставкой; либо отключите сеансы, либо задайте `MaxDegreeOfParallelism = 1`.
  * Используйте `RowBinaryFormat.RowBinaryWithDefaults` в `InsertOptions.Format`, если хотите, чтобы сервер применял значения DEFAULT для столбцов, которые не были переданы.
</Note>

<h4 id="poco-insert">
  Вставка POCO
</h4>

Вместо создания массивов `object[]` можно напрямую вставлять строго типизированные объекты POCO. Зарегистрируйте тип один раз, а затем передайте `IEnumerable<T>`:

```csharp theme={null}
// Определите POCO, соответствующий столбцам вашей таблицы
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// Зарегистрируйте тип (один раз за время жизни клиента)
client.RegisterBinaryInsertType<SensorReading>();

// Вставка напрямую — имена столбцов выводятся из имён свойств
var readings = Enumerable.Range(0, 100_000)
    .Select(i => new SensorReading
    {
        Id = (ulong)i,
        SensorName = $"sensor_{i % 10}",
        Value = Random.Shared.NextDouble() * 100,
        Timestamp = DateTime.UtcNow,
    });

long rowsInserted = await client.InsertBinaryAsync("sensors", readings);
```

По умолчанию все общедоступные свойства, доступные для чтения, сопоставляются со столбцами по строгому совпадению имён с учётом регистра. Вы можете настроить это сопоставление с помощью атрибутов:

```csharp theme={null}
public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // Сопоставить со столбцом с другим именем
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // Явный тип ClickHouse
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Исключить из вставки
    public string InternalTag { get; set; }
}
```

| Атрибут | Назначение |
| - | - |
| `[ClickHouseColumn(Name = "...")]` | Переопределяет имя целевого столбца |
| `[ClickHouseColumn(Type = "...")]` | Явно задаёт тип ClickHouse |
| `[ClickHouseNotMapped]` | Исключает свойство из вставки |

Когда **все** сопоставленные свойства явно задают `Type`, запрос для определения схемы полностью пропускается. Если явные типы указаны только у части свойств, драйвер возвращается к запросу для определения схемы для полного набора столбцов.

`InsertBinaryAsync<T>` поддерживает те же `InsertOptions` (батчинг, параллелизм, кэширование схемы), что и перегрузка `object[]`.

<Note>
  В отличие от перегрузки `object[]`, `InsertBinaryAsync<T>` не принимает явный список столбцов. Столбцы определяются сопоставленными свойствами зарегистрированного типа. Чтобы управлять тем, какие столбцы вставляются, используйте `[ClickHouseNotMapped]`, чтобы исключить свойства, или `[ClickHouseColumn(Name = "...")]`, чтобы переименовать их.

  Если в `InsertOptions` задан `ColumnTypes`, он имеет приоритет над атрибутами POCO.
</Note>

<h4 id="poco-insert-schema-evolution">
  Эволюция схемы
</h4>

Вставка POCO работает без проблем, если после регистрации типа в целевую таблицу добавляются новые столбцы. Поскольку драйвер вставляет только те столбцы, которые сопоставлены с POCO, все новые столбцы с `DEFAULT` (или другими выражениями по умолчанию) сервер заполняет автоматически. Никаких изменений в коде или повторной регистрации не требуется.

<h4 id="insert-query-placement">
  Размещение запроса вставки
</h4>

Бинарная вставка записывает оператор `INSERT INTO ... FORMAT ...` в первой строке тела запроса, перед строками данных. Тело по умолчанию сжимается, поэтому механизмы маршрутизации и логирования, которые анализируют только URL, этот оператор не увидят. Задайте для `InsertOptions.QueryPlacement` значение `InsertQueryPlacement.Url`, чтобы оператор передавался в URL-параметре `query`, а в теле оставались только строки данных:

```csharp theme={null}
var options = new InsertOptions { QueryPlacement = InsertQueryPlacement.Url };
await client.InsertBinaryAsync("events", columns, rows, options);
```

Используйте этот режим, когда proxy, load balancer или gateway маршрутизирует запросы по параметру `query` или анализирует его, либо когда нужно, чтобы оператор попадал в журналы доступа и инструменты обсервабилити. Режим включается явно, поскольку оператор при этом учитывается в длине URL. Фактическое ограничение определяется наименьшим из тех, что накладывают .NET runtime, посредник и server. В .NET 6 — .NET 9 `System.Uri` ограничивает полный закодированный URI запроса 65 519 символами; при превышении этого предела driver генерирует исключение `InvalidOperationException`, которое подсказывает вернуться к `InsertQueryPlacement.Body`. В ClickHouse параметр `http_max_uri_size` по умолчанию равен 1 МиБ, однако посредник может устанавливать более низкий предел. В режиме body на оператор и строки такое ограничение длины URL не распространяется; прочие параметры запроса по-прежнему могут присутствовать в URL.

Эта настройка не зависит от `Compressor`: тело кодируется одинаково в обоих режимах.

***

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

Используйте `ExecuteReaderAsync` для выполнения SELECT-запросов. Возвращаемый `ClickHouseDataReader` предоставляет типизированный доступ к столбцам результата с помощью таких методов, как `GetInt64()`, `GetString()` и `GetFieldValue<T>()`.

Вызовите `Read()`, чтобы перейти к следующей строке. Метод возвращает `false`, когда строк больше не осталось. К столбцам можно обращаться по индексу (с нуля) или по имени столбца.

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

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("max_id", 100L);

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM default.my_table WHERE id < {max_id:Int64}",
    parameters
);

while (reader.Read())
{
    Console.WriteLine($"Id: {reader.GetInt64(0)}, Name: {reader.GetString(1)}");
}
```

<h4 id="poco-read">
  Чтение в POCO
</h4>

Вместо чтения столбцов по индексу или имени можно направлять результаты запроса напрямую в собственные классы. Один раз зарегистрируйте тип в клиенте, а затем используйте `QueryAsync<T>`:

```csharp theme={null}
// Define a POCO matching your result columns
public class SensorReading
{
    public ulong Id { get; set; }
    public DateTime Timestamp { get; set; }

    [ClickHouseColumn(Name = "sensor_name")]
    public string SensorName { get; set; }
    public double Value { get; set; }

}

// Register the type (once per client lifetime)
client.RegisterPocoType<SensorReading>();

// Stream results as typed objects
await foreach (var reading in client.QueryAsync<SensorReading>(
    "SELECT Id, sensor_name, Value, Timestamp FROM sensors"))
{
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
```

<h5 id="poco-read-registration">
  Регистрация
</h5>

`RegisterPocoType<T>()` настраивает сопоставления как для вставки, так и для чтения, и заранее проверяет оба. `RegisterBinaryInsertType<T>()` не изменился и по-прежнему используется только для вставки в целях обратной совместимости.

Зарегистрированный тип должен иметь:

* Публичный конструктор без параметров.
* Как минимум одно публичное свойство с публичным сеттером, не являющимся `init`. Свойства `required` поддерживаются.

<h5 id="poco-read-column-matching">
  Сопоставление столбцов
</h5>

Сопоставление столбцов выполняется с учётом регистра. Если в результате отсутствуют столбцы, свойства сохраняют значения по умолчанию; лишние столбцы в результате игнорируются.

Драйвер не расширяет и не сужает значения. За исключением альтернативных представлений, перечисленных ниже,
тип столбца во фреймворке должен быть присваиваемым типу свойства, а несоответствие вызывает
`InvalidOperationException`. Поэтому свойство типа `object` принимает любой столбец.

<h5 id="poco-read-types">
  Поддерживаемые типы свойств
</h5>

`QueryAsync<T>` считывает каждый из этих столбцов напрямую в соответствующее свойство:

| Столбец ClickHouse | Тип(ы) свойства |
| - | - |
| `Int8`/`Int16`/`Int32`/`Int64` | `sbyte`/`short`/`int`/`long` |
| `UInt8`/`UInt16`/`UInt32`/`UInt64` | `byte`/`ushort`/`uint`/`ulong` |
| `Int128`/`UInt128` | `BigInteger` либо встроенные `System.Int128`/`System.UInt128` в .NET 8 и более поздних версиях |
| `Int256`/`UInt256` | `BigInteger` |
| `Float32`/`Float64`/`BFloat16` | `float`/`double`/`float` |
| `Bool` | `bool` |
| `Decimal` | `decimal` или `ClickHouseDecimal` |
| `Date`/`Date32`/`DateTime`/`DateTime64` | `DateTime`, `DateTimeOffset` или `DateOnly` |
| `Time`/`Time64` | `TimeSpan` |
| `UUID` | `Guid` |
| `IPv4`/`IPv6` | `IPAddress` |
| `Enum8`/`Enum16` | `string` (метка) или `int` (порядковый номер в бинарном представлении) |
| `String`/`FixedString` | `string` или `byte[]` |

Каждая строка также допускает nullable-форму своего типа свойства (`long?`, `DateOnly?` и так далее)
независимо от того, объявлен ли столбец как `Nullable(...)`. Свойство значимого типа, не допускающего NULL, для столбца
`Nullable(T)` принимается при регистрации, но генерирует исключение при поступлении NULL.

Обёртки вроде `LowCardinality(T)`, `SimpleAggregateFunction(f, T)` и `Object(T)` отображаются точно так же, как `T`.

Составные столбцы также поддерживаются и используют тип фреймворка, указанный в
[справочнике типов при чтении](#clickhouse-native-type-map-reading): `Array(T)` — в `T[]`, `Tuple(...)`
— в `System.Tuple<...>`, `Nested(...)` — в `Tuple<...>[]`, `JSON` — в `JsonObject` (или `string`
при [`JsonReadMode=String`](#type-map-reading-json)), а `Variant`/`Dynamic` — в `object`.

Столбец `Map(K, V)` — особый случай: свойство `List<KeyValuePair<K, V>>` или `KeyValuePair<K, V>[]`
читается по пути без упаковки и сохраняет порядок передачи, а также любые повторяющиеся ключи, в любом
режиме [`MapReadMode`](#type-map-reading-map). Свойство `Dictionary<K, V>` работает только в режиме
по умолчанию. Типы ключа и значения должны совпадать в точности, поэтому
для `Map(String, Nullable(Int32))` требуется `KeyValuePair<string, int?>`.

Если для столбца доступно несколько типов свойства (столбец `DateTime` — как `DateTime`,
`DateTimeOffset` или `DateOnly`, столбец `String` — как `string` или `byte[]`), представление определяется объявленным типом свойства. Эти альтернативные представления
относятся к пути POCO, поэтому они доступны в `QueryAsync<T>` и отсутствуют в `MapTo<T>`.

<h5 id="poco-read-mapto">
  Материализация одной строки
</h5>

При ручном переборе через reader используйте `ClickHouseDataReader.MapTo<T>()`, чтобы материализовать текущую строку в зарегистрированный объект POCO, не продвигая reader дальше:

```csharp theme={null}
var reader = await client.ExecuteReaderAsync("SELECT Id, SensorName, Value, Timestamp FROM sensors");

while (reader.Read())
{
    SensorReading reading = reader.MapTo<SensorReading>();
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
```

Используйте `MapTo<T>`, когда цикл чтения нужно вести самостоятельно — например, чтобы сочетать
прямой доступ к столбцам с материализацией в POCO. Метод читает строку через упакованные значения reader,
поэтому не предоставляет альтернативные типы свойств, описанные выше, и выделяет больше памяти, чем
`QueryAsync<T>`. Если вам нужны только строки, предпочтительнее `QueryAsync<T>`; цифры приведены в разделе
[выбор способа материализации](#perf-read-path).

<h5 id="poco-read-converters">
  Конвертеры значений при чтении
</h5>

[Конвертер значений при чтении](#read-value-conversion), заданный на уровне client или для отдельного запроса, применяется к обоим путям и
не отключает чтение без упаковки. Driver преобразует каждый столбец той перегрузкой, которая соответствует способу
чтения этого столбца: типизированной `ConvertValue<T>` — для столбца, прочитанного без упаковки,
и упакованной `ConvertValue` — для составного столбца. Реализуйте обе перегрузки
согласованно, иначе один и тот же столбец будет давать разные результаты на разных путях.

<h5 id="poco-read-diagnostics">
  Диагностика регистрации
</h5>

Если настроен `LoggerFactory`, `RegisterPocoType<T>()` и `RegisterBinaryInsertType<T>()` выводят сообщение журнала уровня `Debug` (категория `ClickHouse.Driver.Client`) со списком того, какие свойства сопоставлены с какими столбцами, а также какие были пропущены и почему. См. [Логирование и диагностика](#logging-and-diagnostics).

***

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

В ClickHouse стандартный формат параметров в SQL-запросах — `{parameter_name:DataType}`.

**Примеры:**

```sql theme={null}
SELECT {value:Array(UInt16)} as a
```

```sql theme={null}
SELECT * FROM table WHERE val = {tuple_in_tuple:Tuple(UInt8, Tuple(String, UInt8))}
```

```sql theme={null}
INSERT INTO table VALUES ({val1:Int32}, {val2:Array(UInt8)})
```

<Note>
  SQL-параметры 'bind' передаются как параметры HTTP-запроса в URI, поэтому их слишком большое количество может привести к исключению "URL too long". Чтобы избежать этого ограничения при массовой вставке данных, используйте `InsertBinaryAsync`.
</Note>

<h4 id="at-style-placeholders">
  Плейсхолдеры `@name` в стиле ADO
</h4>

Драйвер также принимает плейсхолдеры `@name`, которые генерируют ORM вроде Dapper. Это удобство
на стороне клиента: перед отправкой запроса каждый из них переписывается в
`{name:ResolvedType}`, так что сервер никогда не видит `@`. О том, как выбирается тип, см.
[разрешение типов](#parameter-type-mapping). Там, где это возможно, используйте явную
форму `{name:Type}`.

`@name`, для которого нет соответствующего параметра, остаётся без изменений — его отклонит сервер. Сопоставление
чувствительно к регистру, поэтому `@ID` не привяжет параметр с именем `id`.

<Note>
  Чтобы отключить переписывание, установите переключатель AppContext `ClickHouse.Driver.DisableReplacingParameters`
  до первого использования драйвера. Прекращается только переписывание текста; параметры по-прежнему отправляются, поэтому
  запросы, написанные с использованием нативного синтаксиса `{name:Type}`, продолжают работать.
</Note>

<h4 id="identifier-parameters">
  Параметры `Identifier`
</h4>

Тип параметра `Identifier` позволяет безопасно подставлять имя базы данных, таблицы или столбца вместо строкового литерала в кавычках. Используйте его с помощью синтаксиса `{name:Identifier}` в SQL или задав `ClickHouseDbParameter.ClickHouseType = "Identifier"`:

```csharp theme={null}
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("name", "my_database");

await client.ExecuteNonQueryAsync("CREATE DATABASE {name:Identifier}", parameters);
```

```csharp theme={null}
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("col", "user_id");

var reader = await client.ExecuteReaderAsync("SELECT {col:Identifier} FROM t", parameters);
```

Значение передаётся как есть, а сервер подставляет его как обычный SQL-идентификатор, самостоятельно заключая в обратные кавычки и экранируя спецсимволы. Идентификаторы, содержащие специальные символы (включая обратные кавычки), безопасно проходят полный цикл преобразования.

***

<h3 id="query-id">
  Query ID
</h3>

Каждому запросу назначается уникальный `query_id`, который можно использовать, чтобы получить данные из таблицы `system.query_log` или отменить долго выполняющиеся запросы. Вы можете указать собственный идентификатор запроса через `QueryOptions`:

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

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);
```

<Tip>
  Если вы задаёте собственный `QueryId`, убедитесь, что он уникален для каждого вызова. Хорошим выбором будет случайный GUID.
</Tip>

***

<h3 id="parameter-type-mapping">
  Пользовательское сопоставление типов параметров
</h3>

При использовании параметров в стиле `@` (например, `WHERE id = @id`) драйвер автоматически определяет тип ClickHouse по типу значения .NET. Например, `int` сопоставляется с `Int32`.

<Warning>
  **Поведение для автоматически определяемых параметров `DateTime`**

  Для параметров в стиле `@`, у которых в SQL нет подсказки `{name:Type}` и не задан `ClickHouseType`, значения, представляющие момент времени, определяются как `DateTime('UTC')`, а не как обычный `DateTime`. Значения `DateTime` с `Kind`, равным `Utc` или `Local`, а также все значения `DateTimeOffset` отправляются как `DateTime('UTC')`, что сохраняет момент времени при любом часовом поясе сервера.

  Явные подсказки (`{name:DateTime}`) имеют приоритет над автоматическим определением и являются рекомендуемым способом построения запросов.
</Warning>

Чтобы переопределить эти значения по умолчанию, задайте `ParameterTypeResolver` в `ClickHouseClientSettings`. Это полезно, если вы хотите, чтобы все параметры `DateTime` использовали `DateTime64(3)` с точностью до миллисекунд, или чтобы для всех десятичных значений использовался определённый масштаб, без необходимости задавать `ClickHouseType` для каждого отдельного параметра.

**Использование `DictionaryParameterTypeResolver` для простых сопоставлений типов:**

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

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterTypeResolver = new DictionaryParameterTypeResolver(new Dictionary<Type, string>
    {
        [typeof(DateTime)] = "DateTime64(3)",
        [typeof(decimal)] = "Decimal64(4)",
    }),
};
using var client = new ClickHouseClient(settings);

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("dt", DateTime.UtcNow);     // Mapped to DateTime64(3)
parameters.AddParameter("amount", 99.1234m);         // Mapped to Decimal64(4)

await client.ExecuteReaderAsync("SELECT @dt, @amount", parameters);
```

**Пользовательский `IParameterTypeResolver` для расширенных сценариев:**

Если нужно определять тип по значению или имени, реализуйте интерфейс `IParameterTypeResolver` напрямую. Верните `null`, чтобы использовать определение типа по умолчанию:

```csharp theme={null}
public class SmartDecimalResolver : IParameterTypeResolver
{
    public string ResolveType(Type clrType, object value, string parameterName)
    {
        if (clrType != typeof(decimal))
            return null; // Fall through to default

        var scale = (decimal.GetBits((decimal)value)[3] >> 16) & 0x7F;
        return scale <= 4 ? $"Decimal64({scale})" : $"Decimal128({scale})";
    }
}
```

Вы также можете задать resolver для отдельного запроса через `QueryOptions.ParameterTypeResolver`. Если он задан, он имеет приоритет над resolver на уровне клиента.

**Приоритет разрешения типов:**

Resolver — это один из шагов в цепочке приоритетов. От наивысшего приоритета к наименьшему:

1. Явно заданный `ClickHouseType` у параметра
2. Подсказка типа SQL из синтаксиса `{name:Type}` в запросе
3. `IParameterTypeResolver` (из `QueryOptions.ParameterTypeResolver` с откатом к `ClickHouseClientSettings.ParameterTypeResolver`)
4. Встроенный вывод типов (`TypeConverter.ToClickHouseType`)

Resolver также работает с путём ADO.NET `ClickHouseConnection` — настройки наследуются соединениями, созданными клиентом.

***

<h3 id="parameter-value-formatting">
  Пользовательское форматирование значений параметров
</h3>

`IParameterFormatter` — это хук, который определяет, как сериализуются значения параметров. Используйте его, если встроенное форматирование (например, точность для DateTime, локаль для decimal, экранирование строк, представление чисел) не соответствует тому, что ожидают ваша схема или последующие инструменты.

Задайте `ParameterFormatter` в `ClickHouseClientSettings`, чтобы подключить форматтер для всех параметризованных запросов. Форматтер получает значение, разрешённое имя типа ClickHouse и имя параметра, а затем возвращает строковое представление, которое отправляется на сервер. Верните `null`, чтобы использовать форматтер по умолчанию.

**Использование `DictionaryParameterFormatter` для простого форматирования по типам CLR:**

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

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterFormatter = new DictionaryParameterFormatter(new Dictionary<Type, Func<object, string>>
    {
        [typeof(DateTime)] = v => ((DateTime)v).ToString("yyyy-MM-ddTHH:mm:ss.ffffff",
            System.Globalization.CultureInfo.InvariantCulture),
        [typeof(decimal)] = v => ((decimal)v).ToString("F4",
            System.Globalization.CultureInfo.InvariantCulture),
    }),
};
using var client = new ClickHouseClient(settings);
```

**Пользовательский `IParameterFormatter` для сложных сценариев:**

```csharp theme={null}
public class FixedDecimalFormatter : IParameterFormatter
{
    public string Format(object value, string typeName, string parameterName)
    {
        if (value is decimal d)
            return d.ToString("F4", System.Globalization.CultureInfo.InvariantCulture);
        return null; // Fall through for anything else
    }
}
```

Вы также можете задать форматтер для отдельного запроса через `QueryOptions.ParameterFormatter`. Если он задан, то имеет приоритет над форматтером на уровне клиента.

**Составные значения:**

Форматтер применяется как к параметрам-коллекциям верхнего уровня, так и к каждому элементу внутри составных значений (`Array`, `Tuple`, `Map`, `Nullable`, `LowCardinality`, `Variant`). Например, сопоставление `typeof(int)` форматирует каждый элемент `Int32` в `Array(Int32)` по отдельности.

**Оборачивание в одинарные кавычки в составных контекстах:**

Для строкоподобных типов ClickHouse (`String`, `FixedString`, `Enum8`, `Enum16`, `IPv4`, `IPv6`, `UUID`), встроенных в составной литерал, драйвер оборачивает вывод форматтера в одинарные кавычки, но не экранирует его содержимое. Если возвращаемая строка содержит неэкранированную одинарную кавычку или обратную косую черту, составной литерал будет некорректным, и сервер отклонит запрос.

Строковые параметры верхнего уровня (не встроенные в составной тип) используются как есть, без оборачивания, поэтому экранирование там не требуется.

**Приоритет форматтера:**

1. `IParameterFormatter` (из `QueryOptions.ParameterFormatter`, с переходом к `ClickHouseClientSettings.ParameterFormatter`). Если он возвращает не-NULL, используется это значение.
2. Встроенное форматирование для конкретных типов в `HttpParameterFormatter`.

Форматтер не вызывается для значений `null` или `DBNull`; они всегда сериализуются как null-маркер ClickHouse (`\N`).

***

<h3 id="read-value-conversion">
  Пользовательское преобразование значений при чтении
</h3>

`IReadValueConverter` позволяет преобразовывать значения, возвращаемые средством чтения данных после десериализации, не меняя их CLR-тип. Типичные сценарии использования: установка `DateTime.Kind = Utc` для столбца `DateTime` без часового пояса, обрезка или нормализация строк, а также постобработка JSON-столбца перед передачей в прикладной код.

Задайте `ReadValueConverter` в `ClickHouseClientSettings`, чтобы установить преобразователь для всех операций чтения. Преобразователь вызывается один раз для каждого столбца в каждой строке как через boxed-путь (`GetValue`), так и через обобщённый путь (`GetFieldValue<T>`). Если преобразователь не задан, дополнительная нагрузка отсутствует — средство чтения возвращает значения напрямую.

**Использование `DictionaryReadValueConverter` для простого преобразования по CLR-типу:**

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

var converter = new DictionaryReadValueConverter()
    .For<DateTime>(dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc))
    .For<string>(s => s.Trim());

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ReadValueConverter = converter,
};
using var client = new ClickHouseClient(settings);
```

Значения, тип CLR которых во время выполнения не зарегистрирован через `For<T>`, передаются без изменений. Диспетчеризация выполняется по точному типу CLR, поэтому регистрируйте фактический тип, который возвращает reader (например, `For<JsonObject>` для JSON-столбца в `JsonReadMode.Binary`).

**Пользовательский `IReadValueConverter` для продвинутых сценариев:**

Если вам нужно выполнять диспетчеризацию по строковому представлению типа на стороне ClickHouse (например, чтобы различать `DateTime` и `DateTime('UTC')` — хотя оба отображаются как один и тот же тип CLR), реализуйте `IReadValueConverter` напрямую:

```csharp theme={null}
public class UtcKindForNoTzDateTimeConverter : IReadValueConverter
{
    public object ConvertValue(object value, string columnName, string clickHouseType)
    {
        if (value is DateTime dt && clickHouseType == "DateTime")
            return DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }

    public T ConvertValue<T>(T value, string columnName, string clickHouseType)
    {
        if (typeof(T) == typeof(DateTime) && value is DateTime dt && clickHouseType == "DateTime")
            return (T)(object)DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }
}
```

Конвертер должен сохранять CLR-тип времени выполнения; метаданные столбца (`GetFieldType`, `GetSchemaTable`) не перенаправляются через него и должны оставаться согласованными с возвращаемым значением.

Вы также можете задать конвертер для каждого запроса через `QueryOptions.ReadValueConverter`; если он задан, приоритет будет у него, а не у конвертера уровня клиента.

**Граница диспетчеризации:**

Конвертер вызывается один раз для каждого столбца и получает всё десериализованное значение ячейки целиком; он **не** выполняет рекурсивную обработку составных контейнеров. Для столбца `Array(Int32)` передаваемым значением будет `int[]`; для `Tuple(Int32, String)` — `ITuple`.

**Какая перегрузка выполняется:**

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

* `ConvertValue<T>` — типизированные аксессоры `GetByte`, `GetSByte`, `GetInt16`/`32`/`64`,
  `GetUInt16`/`32`/`64`, `GetFloat`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetIPAddress`,
  `GetBigInteger` и `GetFieldValue<T>`, а также каждый столбец без упаковки на
  [пути чтения POCO](#poco-read-converters).
* `ConvertValue` (boxed) — `GetValue`, `GetValues`, индексаторы, `GetChar`, `GetTuple`, а также
  пути с приведением типов в `GetBoolean`, `GetDecimal` и `GetString`.

`IsDBNull` не запускает конвертер вовсе: он считывает флаг null напрямую, поэтому конвертер никак не может
повлиять на то, считается ли значение null. `TryGetEnumOrdinal` также обходит его — см.
[чтение порядкового номера enum](#ado-net-reader-enum-ordinal).

Конвертер работает с ADO.NET-путём `ClickHouseConnection` — настройки наследуются соединениями, созданными из клиента.

***

<h3 id="raw-streaming">
  Прямая потоковая передача
</h3>

Используйте `ExecuteRawResultAsync`, чтобы напрямую передавать результаты запроса в указанном формате, минуя средство чтения данных. Это удобно для экспорта данных в файлы или передачи в другие системы:

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"
);

await using var stream = await result.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync();
```

Распространённые форматы: `JSONEachRow`, `CSV`, `TSV`, `Parquet`, `Native`. Все доступные варианты см. в [документации по форматам](/ru/reference/formats/index).

***

<h3 id="per-query-accept-encoding">
  Сжатие передачи для отдельного запроса
</h3>

По умолчанию клиент согласовывает `zstd, lz4, gzip, deflate`, когда `Compression=true` (значение по умолчанию в строке подключения), и сам прозрачно декодирует поток.

Для экспорта в исходном формате (например, Parquet, Arrow, Native) может потребоваться согласовать другой кодек (например, `zstd` или `lz4`), чтобы снизить нагрузку на CPU в обмен на пропускную способность, не меняя настройку для всего соединения. `QueryOptions.AcceptEncoding` и `ClickHouseCommand.AcceptEncoding` задают HTTP-заголовок `Accept-Encoding` для одного запроса, заменяя любое значение по умолчанию, и принудительно устанавливают `enable_http_compression=1` в URL (ClickHouse требует этого, чтобы учитывать `Accept-Encoding`).

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT Parquet",
    options: new QueryOptions { AcceptEncoding = "zstd" });

// Decode yourself or write to a file
await using var body = await result.ReadAsStreamAsync();
```

<h4 id="per-query-accept-encoding-httpclient">
  Настройка HttpClient
</h4>

Настраивать ничего не нужно: в `HttpClient`, который создаёт драйвер, параметр `AutomaticDecompression` остаётся равным `DecompressionMethods.None`, а декодированием ответов занимается сам драйвер, поэтому `Content-Encoding` никогда не удаляется незаметно для вас, и необработанное тело ответа доходит до вас ровно в том виде, в каком его отправил сервер.

<Warning>
  Если вы передаёте собственный `HttpClient`, также оставьте `AutomaticDecompression` выключенным. Это настройка не только для стороны ответа: при отправке обработчик **добавляет в исходящий `Accept-Encoding` каждый алгоритм из своей маски, которого там нет**. Поэтому обработчик с `GZip | Deflate` превращает явно заданный `AcceptEncoding = "lz4"` в `lz4, gzip, deflate`, а явный `"identity"` — в `identity, gzip, deflate` на уровне передачи. А поскольку ClickHouse разрешает этот заголовок по собственному фиксированному приоритету кодеков (игнорируя порядок и q-значения), он может ответить кодеком, который вы не запрашивали, — а обработчик затем декодирует его и удалит заголовок, так что вы даже не увидите, что это произошло. Если маска выключена, отправляется ровно то, что вы задали.
</Warning>

<Warning>
  Если в `AcceptEncoding` запрошен кодек, который драйвер не умеет декодировать (`snappy`), безопасен только `ExecuteRawResultAsync`. `ExecuteReaderAsync`, `ExecuteScalarAsync` и `ExecuteNonQueryAsync` завершатся с `NotSupportedException` с указанием кодека (ранее они разбирали сжатые байты как формат результата и выдавали мусор).
</Warning>

<h4 id="per-query-accept-encoding-errors">
  Тела ошибок
</h4>

Когда сервер отвечает кодом 4xx/5xx и задан параметр `enable_http_compression=1`, он сжимает тело ошибки тем же кодеком, который использовал бы для успешного ответа. Драйвер декодирует такие ответы для всех поддерживаемых кодеков (`lz4`, `zstd`, `gzip`, `deflate`, `br`/`brotli`), поэтому сообщение, передаваемое в `ClickHouseServerException`, остаётся читаемым. Для всего остального (`snappy`, …) он возвращает сообщение-заполнитель, в котором указан кодек и дана ссылка на `system.query_log`, где можно найти исходный текст ошибки.

***

<h3 id="response-decompression">
  Распаковка ответа
</h3>

`Accept-Encoding` лишь просит сервер сжать ответ — декодировать его всё равно кто-то должен. Драйвер делает это сам, ориентируясь на `Content-Encoding` ответа, поэтому все обычные API чтения (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, Dapper, EF Core, linq2db) работают со сжатым ответом без какой-либо настройки. Поддерживается декодирование `lz4`, `zstd`, `gzip`, `deflate` и `br`; `snappy` не поддерживается.

По умолчанию драйвер объявляет **`zstd, lz4, gzip, deflate`**, и ClickHouse отвечает в `zstd`. Чтобы выбрать другой вариант, задайте `Accept-Encoding` самостоятельно — для всего клиента:

```csharp theme={null}
using var client = new ClickHouseClient(new ClickHouseClientSettings("Host=localhost")
{
    AcceptEncoding = "br",      // decodable, but not advertised by default
});
```

для каждого запроса, и она имеет старшинство:

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions { AcceptEncoding = "identity" });   // opt this query out
```

или в строке подключения — для пользователей ORM, которые никогда не обращаются к `ClickHouseClientSettings`:

```text theme={null}
Host=localhost;AcceptEncoding=br, gzip
```

Установка этого значения также принудительно добавляет `enable_http_compression=1` в URL — без этого ClickHouse вообще не учитывает заголовок. Это происходит и тогда, когда `UseCompression` равно `false`, поскольку явное указание кодека воспринимается как запрос сжатия. Если значение не задано, при `UseCompression=false` заголовок `Accept-Encoding` не отправляется вовсе.

`Accept-Encoding` можно задать в четырёх местах. Побеждает первое из них, в котором назван кодек:

1. `QueryOptions.AcceptEncoding` (или `ClickHouseCommand.AcceptEncoding`)
2. `CustomHeaders["Accept-Encoding"]` на уровне запроса
3. `CustomHeaders["Accept-Encoding"]` на уровне клиента
4. `ClickHouseClientSettings.AcceptEncoding` или ключевое слово строки подключения `AcceptEncoding`

Если кодек не назван ни в одном из них, драйвер отправляет свой список по умолчанию. Значение, в котором не назван ни один кодек (null, пустая строка,
пробелы или только запятые), считается незаданным, и обработка переходит к следующему месту. Чтобы отключить сжатие, используйте `identity`.

**Кодек выбирает сервер, а не клиент.** ClickHouse ищет в `Accept-Encoding` токены в собственном фиксированном порядке предпочтения — `zstd` > `br` > `lz4` > `snappy` > `gzip` > `deflate` — и игнорирует как порядок их перечисления, так и любые q-значения. Таким образом, этот заголовок сообщает о поддерживаемых возможностях, а не выдвигает требование, и единственный способ повлиять на выбор — исключить часть токенов. Список по умолчанию включает `zstd`, поэтому обычный запрос получает ответ, сжатый zstd; остальные токены служат резервным вариантом. `br` поддерживается при декодировании, но по умолчанию не объявляется.

Соотношение кодеков по размеру полезной нагрузки, нагрузке на CPU сервера и клиента зависит от ваших данных, канала связи и серверного параметра `http_zlib_compression_level` (значение по умолчанию в поставке: 3) — см. [Настройка сжатия](#tuning-compression).

* **`http_zlib_compression_level`.** Этот параметр применяется ко всем HTTP-кодекам, значение по умолчанию — 3. Его следует подбирать исходя из ваших данных, скорости канала и потребления CPU.
* **Клиент, ограниченный CPU, на быстром канале.** Драйвер декодирует тело ответа в вызывающем потоке, поэтому, когда сеть не является узким местом, ограничивающим фактором может стать скорость декодирования на стороне клиента.

Запрашивайте другой кодек — для отдельного запроса или для всего клиента — в любом из этих случаев:

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions { AcceptEncoding = "lz4" });   // decode this one with lz4 instead
```

Поскольку решение принимается на основе ответа, тело декодируется всякий раз, когда на это указывает его `Content-Encoding`, независимо от того, что было запрошено: при отсутствии заголовка или значении `identity` тело передаётся без изменений, поддерживаемый кодек декодируется, а всё остальное приводит к ошибке с указанием этого кодека. Риска двойного декодирования нет: если `AutomaticDecompression` у обработчика, предоставленного вызывающей стороной, уже декодировал тело, он также удаляет `Content-Encoding`, поэтому драйвер видит незашифрованные данные и не трогает их.

**Raw-результаты не объявляют кодек.** `ExecuteRawResultAsync` (а также публичные `PostStreamAsync` / `InsertRawStreamAsync`) отдают вам тело ответа дословно, поэтому, если вы сами не укажете кодек, они вообще не запрашивают никакого сжатия — драйвер такое тело не декодирует, и предложить кодек здесь означало бы незаметно превратить экспорт в сжатый файл. Отсюда простое правило, не зависящее от настроек конкретного `HttpClient`: **дословное тело приходит ровно в том виде, в каком его отправил сервер, а сервер отправляет незашифрованный текст, если вы не запросили кодек.** Запросить его (для всего клиента или для отдельного запроса) — это и есть способ намеренно выгрузить сжатые байты.

Явно заданный `AcceptEncoding` (на любом из уровней) по-прежнему применяется к raw-запросам, а `ClickHouseRawResult.ReadDecompressedStreamAsync()` декодирует результат, когда вам это нужно; `ReadAsStreamAsync`, `ReadAsByteArrayAsync`, `ReadAsStringAsync` и `CopyToAsync` всегда возвращают байты ровно в том виде, в каком они пришли.

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT JSONEachRow",
    options: new QueryOptions { AcceptEncoding = "lz4" });

Console.WriteLine(result.ContentEncoding); // "lz4"

await using var body = await result.ReadDecompressedStreamAsync();
using var bodyReader = new StreamReader(body);
var json = await bodyReader.ReadToEndAsync();
```

Прочитайте возвращённый поток до конца, прежде чем он выйдет из области видимости, как показано выше. Если ответ *сжат*, вы получаете декодер, созданный с `leaveOpen`, поэтому его освобождение оставляет ответ нетронутым; если же ответ **не** сжат, вы получаете сам поток HTTP-содержимого, и его освобождение завершает тело ответа. В любом случае `ClickHouseRawResult` владеет ответом — не обращайтесь к другим его членам для чтения после того, как поток был освобождён. Освобождение `ClickHouseRawResult` обязательно всегда и само по себе достаточно: оно освобождает и ответ, и любой вставленный здесь декодер (декодеры удерживают буферы из пула). Поэтому `await using` выше необязателен, но его можно безопасно оставить. Повторные последовательные вызовы возвращают тот же самый поток; тип небезопасен для параллельного использования.

Готовый к запуску пример см. в [Select\_007\_ResponseCompression.cs](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Select/Select_007_ResponseCompression.cs).

<h4 id="insert-compression">
  Сжатие вставок (запросов)
</h4>

Zstd — кодек по умолчанию для вставок: `InsertOptions.Compressor` изначально имеет значение `ZstdCompressor.Default`,
то есть zstd с уровнем сжатия 3. Задайте другой компрессор, чтобы изменить кодек, или `null`, чтобы отправлять
тело без сжатия.

```csharp theme={null}
var options = new InsertOptions { Compressor = GZipCompressor.Default };  // Content-Encoding: gzip
await client.InsertBinaryAsync("events", columns, rows, options);
```

В состав драйвера входят четыре кодека. У каждого есть экземпляр `Default`, а также конструктор, принимающий уровень
и размер буфера записи:

| Компрессор | `Content-Encoding` | Конструктор | `Default` |
| - | - | - | - |
| `ZstdCompressor` | `zstd` | `(int level = 3, int bufferSize = 262144)` | уровень 3 |
| `Lz4Compressor` | `lz4` | `(Lz4Level level = Lz4Level.Fast, int bufferSize = 262144)` | `Lz4Level.Fast` |
| `GZipCompressor` | `gzip` | `(CompressionLevel level = CompressionLevel.Fastest, int bufferSize = 262144)` | `Fastest` |
| `BrotliCompressor` | `br` | `(CompressionLevel level = CompressionLevel.Fastest, int bufferSize = 262144)` | `Fastest` |

```csharp theme={null}
var options = new InsertOptions { Compressor = new ZstdCompressor(level: 1) };
```

<Note>
  *Переиспользуйте инстансы компрессоров.* Каждый `Default` — это один общий инстанс, и все четыре компрессора
  безопасно использовать одновременно из нескольких потоков — именно это и происходит, когда
  `InsertOptions.MaxDegreeOfParallelism` больше 1, поскольку одна вставка использует один компрессор на каждый
  батч. Ни один из них не реализует `IDisposable`. Создайте собственный инстанс один раз и переиспользуйте его
  так же, как используется `Default`.
</Note>

<h5 id="custom-compressor">
  Пользовательский кодек
</h5>

Интерфейс `IClickHouseCompressor` является публичным, и в его реализации нужно определить всего два члена:

```csharp theme={null}
public sealed class MyCompressor : IClickHouseCompressor
{
    public string ContentEncoding => "my-codec";

    public Stream Compress(Stream destination, bool leaveOpen) => /* a compressing write stream */;
}
```

Сервер должен принимать указанный вами `Content-Encoding`. Остальные члены —
`Decompress`, `MethodByte`, `MaxEncodedLength`, `Encode` и `Decode` — имеют реализации по умолчанию,
которые генерируют исключение `NotSupportedException`, поэтому переопределяйте только те, что нужны вашему кодеку.
Реализуйте `Decompress`, чтобы не только сжимать запросы, но и декодировать тела ответов, и генерируйте
`InvalidDataException` из возвращаемого им потока, если тело повреждено или имеет неверный формат.

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

* **Любой запрос с SQL-текстом** (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, `ExecuteRawResultAsync`, слой ADO.NET) отправляет свой оператор с `Content-Encoding: gzip`, если `UseCompression` равно `true` — то есть по умолчанию. Кодек не настраивается: `AcceptEncoding` влияет только на ответ, так что выбор здесь — gzip или ничего. При `Compression=false` оператор отправляется в открытом виде. Команды невелики, поэтому об этом редко стоит задумываться — но это полезно знать, когда вы наблюдаете за запросами через proxy или в перехвате пакетов.
* **Multipart-тело** — запрос, параметры которого отправляются как form data (`UseFormDataParameters=true`) — всегда отправляется без сжатия, независимо от значения `UseCompression`.
* **Raw-загрузка** (`InsertRawStreamAsync`, `PostStreamAsync`) использует собственный флаг для каждого вызова и не учитывает ни `UseCompression`, ни `InsertOptions.Compressor`: gzip, если флаг установлен, и без сжатия в противном случае. Обратите внимание, что параметр `useCompression` у `InsertRawStreamAsync` по умолчанию равен `true`, поэтому raw-загрузка сжимается gzip, если не передать `false` — даже при `Compression=false` на клиенте.

***

<h3 id="tuning-compression">
  Настройка сжатия
</h3>

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

<h4 id="the-one-number-that-decides-it">
  Одно число, которое всё решает
</h4>

Сжатие оправдано до тех пор, пока кодек работает быстрее сети.

На пути чтения этот порог ниже, чем принято считать, поскольку ClickHouse сжимает
HTTP-ответы в один поток в выходном буфере. По измерениям на сервисе ClickHouse Cloud с 16 vCPU
(`hits`, RowBinary, уровень 3) сервер выдаёт сжатый вывод со скоростью примерно 100-200 МБ/с.

Поэтому для большого результата и при условии, что одновременно обрабатывается один запрос, сжатие перестаёт окупаться где-то в районе 100 МБ/с. Один HTTPS-поток
внутри одного облачного региона обычно превышает это значение, тогда как всё, что идёт через публичный интернет, VPN или границу региона, как правило, остаётся ниже.

Путь вставки допускает сжатие при более высоких скоростях канала, поскольку ваш клиент сжимает данные на отдельном ядре и обычно работает быстрее, чем сжатие ответа на сервере.

<h4 id="rough-guide-by-deployment">
  Ориентировочные рекомендации по вариантам развертывания
</h4>

| Где работает клиент | Типичная пропускная способность | Чтения | Вставки |
| - | - | - | - |
| Тот же хост / loopback | > 500 МБ/с | `identity` | `lz4` — самый быстрый, либо без сжатия |
| Тот же регион, то же облако | \~100–500 МБ/с | `identity` или `lz4` | `zstd:1` |
| Межрегиональное подключение, то же облако | \~10–100 МБ/с | `zstd` | `zstd:3` |
| Интернет / VPN / другое облако | \< 25 МБ/с | `zstd` | `zstd:3` |
| Тарифицируемый или сильно ограниченный канал | \< 5 МБ/с | `zstd` | `zstd:5`+ или `br` |

Три момента, которые эта таблица не учитывает:

* **Стоимость исходящего трафика:** если передача данных тарифицируется, байты стоят денег, а не только времени, и это
  склоняет к более высокой степени сжатия независимо от скорости канала.
* **Небольшие результаты:** всё сказанное выше относится к большим полезным нагрузкам. Для небольших ответов кодек почти
  не имеет значения — определяющими становятся накладные расходы на каждый запрос.
* **Параллельные вставки поднимают пороговые значения для вставок.** Все приведенные выше показатели пропускной способности относятся к *одному* потоку. Значение `InsertOptions.MaxDegreeOfParallelism` по умолчанию равно `1`, но при его увеличении батчи сжимаются параллельно, поэтому совокупная скорость кодирования на стороне клиента растет примерно пропорционально числу выделенных ядер. Поэтому на быстром канале параллельную вставку по-прежнему выгодно
  сжимать даже на тех скоростях, при которых однопоточная вставка уже не выигрывает от сжатия. Считайте строки таблицы, относящиеся к вставкам, *нижней границей*: если вы уже формируете батчи параллельно, проведите повторные измерения, прежде чем делать вывод, что ваш канал слишком быстр для сжатия.

Путь чтения распараллеливается только между несколькими запросами.

<h4 id="choosing-a-codec">
  Выбор кодека
</h4>

| Кодек | Коэффициент | Когда использовать | На что обратить внимание |
| - | - | - | - |
| `lz4` | самый низкий | Быстрые каналы; CPU в большем дефиците, чем пропускная способность. С большим отрывом самый дешёвый в декодировании и самый быстрый на небольших результатах — именно его стоит указывать, когда нужно уйти от zstd по умолчанию. | В нём **нет энтропийного кодировщика**, поэтому на данных с перекосом, но без повторов (например, длинные последовательности числового текста) его коэффициент заметно уступает всем остальным. Кроме того, он сильнее всех страдает от повышения `http_zlib_compression_level`: переход с уровня 1 на 3 обходится ему примерно в 2,7× по CPU ради выигрыша примерно в 29% объёма. |
| `zstd` | высокий | Универсальный выбор всегда, когда в деле участвует реальная сеть. Лучшее соотношение коэффициента и затрат CPU в значимом диапазоне, а на уровне 3 он обходит `lz4` и по числу байт, *и* по CPU сервера, *и* по времени выполнения. | дороже `lz4` при **декодировании** — по нашим измерениям в 1,6 раза на уровне 3, хотя на уровне 1 они сопоставимы — и драйвер декодирует в вашем вызывающем потоке. Именно при `http_zlib_compression_level=1` он расходует немного *больше* CPU сервера, чем `lz4`. |
| `gzip` | средний | Совместимость — его понимают все прокси и шлюзы. | По нашим измерениям проигрывает и `lz4`, и `zstd` по всем параметрам: объём больше, чем у `zstd`, при этом кодирование обходится в несколько раз дороже по CPU, а декодирование — в 5–9 раз. Выбирайте его ради совместимости, а не производительности. |
| `br` | самый высокий на низких уровнях | Пропускная способность действительно является узким местом, и вы готовы платить за неё процессорным временем. | Резко сдаёт на более высоких уровнях — при `http_zlib_compression_level=6` мы намеряли расход CPU сервера в 3–4 раза выше, чем у `zstd`. По умолчанию не анонсируется, поскольку опережает все запасные токены в списке по умолчанию. |

<h4 id="levels">
  Levels
</h4>

Сжатие ответов управляется одной настройкой сервера — `http_zlib_compression_level`, которая применяется к *каждому* HTTP-кодеку, а не только к zlib. Значение по умолчанию — 3.

Не трогайте её без измеренных оснований. Выше значения по умолчанию выигрыш в размере минимален, а расход CPU велик (для `zstd` переход с 3 на 6 примерно удваивает нагрузку на CPU сервера ради \~14% экономии байтов), а `br` ведёт себя патологически. Ниже, на уровне 1, картина действительно меняется: `lz4` становится намного дешевле, а `zstd` теряет своё преимущество по CPU перед ним. При необходимости задавайте её на уровне запроса:

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions
    {
        AcceptEncoding = "zstd",
        CustomSettings = new Dictionary<string, object> { ["http_zlib_compression_level"] = 1 },
    });
```

<h4 id="measuring-your-own-crossover">
  Измерение собственной точки перехода
</h4>

Быстрее всего подобрать оптимальный кодек и уровень сжатия можно, замерив время выполнения одного и того же запроса на нескольких кодеках и сравнив результаты.

```csharp theme={null}
foreach (var codec in new[] { "identity", "lz4", "zstd" })
{
    var sw = Stopwatch.StartNew();
    using var reader = await client.ExecuteReaderAsync(
        "SELECT ... FROM big_table",
        options: new QueryOptions { AcceptEncoding = codec });
    while (await reader.ReadAsync()) { }
    Console.WriteLine($"{codec,-9} {sw.ElapsedMilliseconds} ms");
}
```

Чтобы увидеть ту же картину со стороны сервера, прочитайте `ProfileEvents` из `system.query_log` — задайте
`QueryOptions.QueryId`, чтобы найти нужную строку:

```sql theme={null}
SELECT ProfileEvents['UserTimeMicroseconds'] + ProfileEvents['SystemTimeMicroseconds'] AS cpu_us,
       ProfileEvents['NetworkSendBytes'] AS sent_bytes,
       query_duration_ms
FROM system.query_log
WHERE query_id = 'your-query-id' AND type = 'QueryFinish';
```

Одна ловушка, если вы решите провести бенчмарк самостоятельно: сам по себе `LIMIT n` без `ORDER BY` возвращает *разные строки
при каждом запуске*, поэтому в каждом повторе сжимаются разные данные, а соотношения превращаются в шум. Сравнивайте
на фиксированном результирующем наборе.

***

<h3 id="raw-stream-insert">
  Вставка из сырого потока
</h3>

Используйте `InsertRawStreamAsync` для вставки данных напрямую из файловых потоков или потоков в памяти в таких форматах, как CSV, JSON, Parquet или любой другой [поддерживаемый формат ClickHouse](/ru/reference/formats/index).

**Вставка из CSV-файла:**

```csharp theme={null}
using var response = await client.InsertRawStreamAsync(
    table: "my_table",
    stream: File.OpenRead("data.csv"),
    format: "CSV",
    columns: ["id", "product", "price"] // Optional: specify columns
);
```

<Warning>
  *Драйвер берёт владение потоком на себя.* `InsertRawStreamAsync` и `PostStreamAsync` освобождают
  переданный вами поток по завершении запроса — независимо от того, завершился он успешно или с ошибкой. Не освобождайте его
  самостоятельно и не используйте повторно — именно поэтому в примере выше
  `FileStream` не обёрнут в `using`.

  Ваш собственный `using` сработает уже после того, как драйвер освободил поток. Для `FileStream` или
  `MemoryStream` такой повторный вызов безвреден, но для потока, чей `Dispose` возвращает буфер в пул
  или уменьшает счётчик ссылок, ресурс будет освобождён дважды.

  Владение переходит только после того, как аргументы приняты: если вызов генерирует `ArgumentException` или
  `ArgumentNullException` из-за отсутствующей таблицы, потока или формата, поток по-прежнему принадлежит вам.
</Warning>

<Note>
  Опции управления поведением ингестии данных см. в [документации по настройкам форматов](/ru/reference/settings/formats).
</Note>

***

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

Дополнительные практические примеры использования см. в каталоге [examples](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples) репозитория GitHub.

<h2 id="ado-net">
  ADO.NET
</h2>

Библиотека предоставляет полную поддержку ADO.NET через `ClickHouseConnection`, `ClickHouseCommand` и `ClickHouseDataReader`. Этот API необходим для интеграции с ORM (Dapper, Linq2db), а также в случаях, когда нужны стандартные абстракции базы данных .NET.

<h3 id="ado-net-datasource">
  Управление жизненным циклом с ClickHouseDataSource
</h3>

**Всегда создавайте соединения через `ClickHouseDataSource`**, чтобы обеспечить корректное управление жизненным циклом и использование пула соединений. DataSource внутренне использует один `ClickHouseClient`, и все соединения совместно используют его пул HTTP-соединений.

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

// Создать DataSource один раз (зарегистрировать как singleton в DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Создавать лёгкие соединения по мере необходимости
await using var connection = await dataSource.OpenConnectionAsync();

// Использовать соединение
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();
```

При использовании внедрения зависимостей:

```csharp theme={null}
// В Startup.cs или Program.cs
services.AddSingleton(sp =>
{
    var factory = sp.GetRequiredService<IHttpClientFactory>();
    return new ClickHouseDataSource("Host=localhost", factory, "ClickHouse");
});

// В вашем сервисе
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

    public MyService(ClickHouseDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Используйте соединение...
    }
}
```

<Warning>
  **Не создавайте `ClickHouseConnection` напрямую** в коде для продакшн. При каждом таком создании экземпляра создаются новый HTTP-клиент и новый пул соединений, что под нагрузкой может привести к исчерпанию сокетов:

  ```csharp theme={null}
  // НЕ ДЕЛАЙТЕ ТАК - каждый раз создается новый пул соединений
  using var conn = new ClickHouseConnection("Host=localhost");
  await conn.OpenAsync();
  ```

  Вместо этого всегда используйте `ClickHouseDataSource` или переиспользуйте один экземпляр `ClickHouseClient`.
</Warning>

***

<h3 id="ado-net-command">
  Использование ClickHouseCommand
</h3>

Создавайте команды на основе соединения для выполнения SQL:

```csharp theme={null}
await using var connection = await dataSource.OpenConnectionAsync();

// Создание команды с SQL
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// Выполнение и чтение результатов
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}
```

Методы команд:

* `ExecuteNonQueryAsync()` — Для операторов INSERT, UPDATE, DELETE и DDL-операторов
* `ExecuteScalarAsync()` — Возвращает первый столбец первой строки
* `ExecuteReaderAsync()` — Возвращает `ClickHouseDataReader` для перебора результатов

***

<h3 id="ado-net-reader">
  Использование `ClickHouseDataReader`
</h3>

`ClickHouseDataReader` обеспечивает типизированный доступ к результатам запроса:

```csharp theme={null}
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");

    // Проверка на null
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}
```

<h4 id="ado-net-reader-enum-ordinal">
  Чтение порядкового номера enum
</h4>

Столбец `Enum8` или `Enum16` материализуется как его метка: `GetFieldType` возвращает `string`, а
`GetString`, `GetValue` и `GetFieldValue<string>` — саму метку. Числовые аксессоры
для столбца enum генерируют исключение `InvalidCastException`, поскольку хранимое значение является строкой.

Чтобы получить число, стоящее за меткой, используйте `TryGetEnumOrdinal`:

```csharp theme={null}
if (reader.TryGetEnumOrdinal(ordinal, out int value))
    Console.WriteLine(value);   // e.g. 1 for 'Active' in Enum8('Active' = 1)
```

Метод возвращает `true` и задаёт `value` для столбца типа `Enum8`/`Enum16`, а также для столбца
`Nullable(Enum...)`, ячейка которого не равна NULL. Он возвращает `false`, а `value` устанавливается в `0`,
если ячейка содержит NULL или если столбец не является перечислением. Порядковый номер — это
знаковое значение, полученное из сетевого представления данных, поэтому оно может быть отрицательным, а порядковый номер `Enum16` может
не помещаться в один байт.

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

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

`ClickHouse.Driver` использует `System.Net.Http.HttpClient` внутри. У `HttpClient` есть отдельный пул соединений для каждой конечной точки. В результате:

* Сеансы базы данных мультиплексируются через HTTP-соединения, которыми управляет пул соединений.
* HTTP-соединения автоматически переиспользуются пулом.
* Соединения могут оставаться активными даже после освобождения объектов `ClickHouseClient` или `ClickHouseConnection`.

**Рекомендуемые подходы:**

| Сценарий | Рекомендуемый подход |
| - | - |
| Общий случай | Используйте singleton `ClickHouseClient` |
| ADO.NET / ORM | Используйте `ClickHouseDataSource` (он создает соединения, использующие один и тот же пул) |
| Окружения с DI | Регистрируйте `ClickHouseClient` или `ClickHouseDataSource` как singleton через `IHttpClientFactory` |

<Warning>
  При использовании пользовательского `HttpClient` или `HttpClientFactory` убедитесь, что для `PooledConnectionIdleTimeout` задано значение меньше, чем `keep_alive_timeout` сервера, чтобы избежать ошибок из-за полузакрытых соединений. Значение `keep_alive_timeout` по умолчанию для развертываний в Cloud составляет 10 секунд.
</Warning>

<Warning>
  Не создавайте несколько экземпляров `ClickHouseClient` или автономных экземпляров `ClickHouseConnection` без общего `HttpClient`. Каждый экземпляр создает собственный пул соединений.
</Warning>

***

<h3 id="best-practice-datetime">
  Обработка DateTime
</h3>

1. **По возможности используйте UTC.** Храните временные метки в столбцах `DateTime('UTC')` и используйте `DateTimeKind.Utc` в коде. Это устраняет неоднозначность, связанную с часовыми поясами.

2. **Используйте `DateTimeOffset` для явной работы с часовыми поясами.** Он всегда представляет конкретный момент времени и содержит информацию о смещении.

3. **Указывайте часовой пояс в подсказках типов SQL.** Если вы используете параметры со значениями DateTime `Unspecified` для столбцов не в UTC, указывайте часовой пояс прямо в SQL:
   ```csharp theme={null}
   var parameters = new ClickHouseParameterCollection();
   parameters.AddParameter("dt", myDateTime);

   await client.ExecuteNonQueryAsync(
       "INSERT INTO table (dt) VALUES ({dt:DateTime('Europe/Amsterdam')})",
       parameters
   );
   ```

***

<h3 id="async-inserts">
  Асинхронные вставки
</h3>

[Асинхронные вставки](/ru/concepts/features/operations/insert/asyncinserts) переносят ответственность за батчинг с клиента на сервер. Вместо батчинга на стороне клиента сервер буферизует входящие данные и сбрасывает их в хранилище при достижении настраиваемых пороговых значений. Это особенно полезно в сценариях с высоким параллелизмом, например для рабочих нагрузок обсервабилити, где множество агентов отправляют небольшие полезные нагрузки.

Включите асинхронные вставки через `CustomSettings` или строку подключения:

```csharp theme={null}
// Использование CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Рекомендуется: ожидать подтверждения сброса буфера

// Или через строку подключения
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"
```

**Два режима** (управляются параметром `wait_for_async_insert`):

| Режим | Поведение | Сценарий использования |
| - | - | - |
| `wait_for_async_insert=1` | Вставка завершается после того, как данные сбрасываются на диск. Ошибки возвращаются клиенту. | **Рекомендуется** для большинства рабочих нагрузок |
| `wait_for_async_insert=0` | Вставка завершается сразу после буферизации данных. Нет гарантии, что данные будут сохранены. | Только если допустима потеря данных |

<Warning>
  При `wait_for_async_insert=0` ошибки проявляются только во время сброса на диск, и их нельзя связать с исходной вставкой. Кроме того, клиент не обеспечивает обратного давления, что создает риск перегрузки сервера.
</Warning>

**Ключевые настройки:**

| Настройка | Описание |
| - | - |
| `async_insert_max_data_size` | Сбрасывать, когда буфер достигает этого размера (в байтах) |
| `async_insert_busy_timeout_ms` | Сбрасывать по истечении этого тайм-аута (в миллисекундах) |
| `async_insert_max_query_number` | Сбрасывать после накопления такого количества запросов |

***

<h3 id="best-practices-sessions">
  Сеансы
</h3>

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

* Временные таблицы (`CREATE TEMPORARY TABLE`)
* Сохранение контекста запроса между несколькими командами
* Настройки уровня сеанса (`SET max_threads = 4`)

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

```csharp theme={null}
var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session", // Optional -- will be auto-generated if not provided
};

using var client = new ClickHouseClient(settings);

await client.ExecuteNonQueryAsync("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await client.ExecuteNonQueryAsync("INSERT INTO temp_ids VALUES (1), (2), (3)");

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)"
);
```

**Использование ADO.NET (для совместимости с ORM):**

```csharp theme={null}
var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session",
};

var dataSource = new ClickHouseDataSource(settings);
await using var connection = await dataSource.OpenConnectionAsync();

await using var cmd1 = connection.CreateCommand("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await cmd1.ExecuteNonQueryAsync();

await using var cmd2 = connection.CreateCommand("INSERT INTO temp_ids VALUES (1), (2), (3)");
await cmd2.ExecuteNonQueryAsync();

await using var cmd3 = connection.CreateCommand("SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)");
await using var reader = await cmd3.ExecuteReaderAsync();
```

***

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

`ClickHouse.Driver` поддерживает все типы данных ClickHouse. В таблицах ниже показано соответствие между типами ClickHouse и встроенными типами .NET при чтении данных из базы данных.

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

<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 | `BigInteger` |
| UInt128 | `BigInteger` |
| Int256 | `BigInteger` |
| UInt256 | `BigInteger` |

***

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

| Тип ClickHouse | Тип .NET |
| - | - |
| Float32 | `float` |
| Float64 | `double` |
| BFloat16 | `float` |

***

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

| Тип ClickHouse | Тип .NET |
| - | - |
| Decimal(P, S) | `decimal` / `ClickHouseDecimal` |
| Decimal32(S) | `decimal` / `ClickHouseDecimal` |
| Decimal64(S) | `decimal` / `ClickHouseDecimal` |
| Decimal128(S) | `decimal` / `ClickHouseDecimal` |
| Decimal256(S) | `decimal` / `ClickHouseDecimal` |

<Note>
  Преобразование типа Decimal регулируется настройкой UseCustomDecimals.
</Note>

***

<h4 id="type-map-reading-boolean">
  Логический тип
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| Bool | `bool` |

***

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

| Тип ClickHouse | Тип .NET |
| - | - |
| String | `string` |
| FixedString(N) | `string` |

<Note>
  По умолчанию столбцы `String` и `FixedString(N)` возвращаются как `string`. Установите `ReadStringsAsByteArrays=true` в строке подключения, чтобы вместо этого считывать их как `byte[]`. Это полезно при хранении бинарных данных, которые могут быть не в корректной кодировке UTF-8.

  Эта настройка распространяется и на строки, вложенные в другие типы, поэтому `Array(String)` считывается как `byte[][]`,
  а `Map(String, String)` — как `Dictionary<byte[], byte[]>`, включая ключи. Единственное исключение —
  `JSON`-столбец, строковые листья которого всегда представлены текстом; см. [JSON type](#type-map-reading-json).
</Note>

***

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

| Тип ClickHouse | Тип .NET |
| - | - |
| Date | `DateTime` |
| Date32 | `DateTime` |
| DateTime | `DateTime` |
| DateTime32 | `DateTime` |
| DateTime64 | `DateTime` |
| Time | `TimeSpan` |
| Time64 | `TimeSpan` |

ClickHouse хранит значения `DateTime` и `DateTime64` внутри как Unix-временные метки (секунды или доли секунды с начала эпохи Unix). Хотя хранение всегда выполняется в UTC, со столбцами может быть связан часовой пояс, который влияет на то, как значения отображаются и интерпретируются.

При чтении значений `DateTime` свойство `DateTime.Kind` устанавливается на основе часового пояса столбца:

| Определение столбца | Возвращаемый DateTime.Kind | Примечания |
| - | - | - |
| `DateTime('UTC')` | `Utc` | Явно указан часовой пояс UTC |
| `DateTime('Europe/Amsterdam')` | `Unspecified` | Применяется смещение |
| `DateTime` | `Unspecified` | Локальное время сохраняется как есть |

Для столбцов не в UTC возвращаемый `DateTime` представляет локальное время в этом часовом поясе. Используйте `ClickHouseDataReader.GetDateTimeOffset()`, чтобы получить `DateTimeOffset` с корректным смещением для этого часового пояса:

```csharp theme={null}
var reader = (ClickHouseDataReader)await connection.ExecuteReaderAsync(
    "SELECT toDateTime('2024-06-15 14:30:00', 'Europe/Amsterdam')");
reader.Read();

var dt = reader.GetDateTime(0);    // 2024-06-15 14:30:00, Kind=Unspecified
var dto = reader.GetDateTimeOffset(0); // 2024-06-15 14:30:00 +02:00 (CEST)
```

Для столбцов **без** явно заданного часового пояса (то есть `DateTime`, а не `DateTime('Europe/Amsterdam')`) драйвер возвращает `DateTime` с `Kind=Unspecified`. Это позволяет сохранить локальное время в точности в том виде, в котором оно хранится, не делая предположений о часовом поясе.

Если для столбцов без явно заданных часовых поясов вам нужно поведение с учётом часового пояса, сделайте одно из следующего:

1. Используйте явные часовые пояса в определениях столбцов: `DateTime('UTC')` или `DateTime('Europe/Amsterdam')`
2. Задайте часовой пояс самостоятельно после чтения.

***

<h4 id="type-map-reading-json">
  Тип JSON
</h4>

| Тип ClickHouse | Тип .NET | Примечания |
| - | - | - |
| Json | `JsonObject` | По умолчанию (`JsonReadMode=Binary`) |
| Json | `string` | При `JsonReadMode=String` |

Возвращаемый тип для JSON-столбцов задаётся параметром `JsonReadMode`:

* **`Binary` (по умолчанию)**: Возвращает `System.Text.Json.Nodes.JsonObject`. Обеспечивает структурированный доступ к JSON-данным, но специализированные типы ClickHouse (например, IP-адреса, UUID и большие decimal-значения) внутри структуры JSON преобразуются в строковое представление.

* **`String`**: Возвращает исходный JSON в виде `string`. Сохраняет точное представление JSON из ClickHouse, что полезно, когда JSON нужно передать дальше без парсинга или если вы хотите самостоятельно выполнять десериализацию.

```csharp theme={null}
// Configure string mode via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// Or via connection string
// "Host=localhost;JsonReadMode=String"
```

`None` — третий режим. Данные читаются точно так же, как в `Binary`, но настройка сервера не отправляется вместе с запросом — используйте его для подключений, которым не разрешено её задавать.

<h5 id="type-map-reading-json-nulls">
  Типизированные пути и null-значения
</h5>

Путь, объявленный в типе столбца, — это **typed path**; любой другой путь в документе — это
**dynamic path**. Различия между ними проявляются, когда значение равно null.

Typed path всегда присутствует в `JsonObject`. Будучи объявленным как `Nullable(T)` или `Dynamic`, он возвращается
как JSON null и в случае, когда сохранённое значение равно null, и в случае, когда в документе такого пути нет, — эти
два случая неразличимы:

```csharp theme={null}
// Column type JSON(x Nullable(Int64))
// stored '{"x":null}'  ->  {"x":null}
// stored '{}'          ->  {"x":null}
```

Если path объявлен с типом, не допускающим NULL, то при его отсутствии подставляется значение по умолчанию для этого типа: `JSON(x String)`
даёт `{"x":""}`, а `JSON(x Int64)` — `{"x":0}`.

Динамический path со значением null полностью удаляется из объекта, поэтому `ContainsKey` возвращает для него false. Чтение `{"x":null}` из обычного столбца `JSON` даёт `{}`.

Вложенные typed paths создают свои parents, поэтому `JSON(a.b Nullable(Int64))` даёт `{"a":{"b":null}}`
даже для пустого документа.

<Note>
  Именно так это отображает сам server, поэтому режимы `Binary` и `String` теперь согласованы. До версии 1.4.0
  typed path со значением null удалялся из `JsonObject`, из-за чего `{"x":null}` считывался как
  `{}`, а для вложенного path вида `JSON(a.b Nullable(Int64))` исчезало всё поддерево `a`.
</Note>

<h5 id="type-map-reading-json-strings">
  Строки внутри JSON-столбца
</h5>

Строковые листья внутри столбца `JSON` всегда возвращаются как текст, независимо от значения
`ReadStringsAsByteArrays` — у `JsonValue` нет формы массива байтов, поэтому `byte[]` отображался бы
в base64. Это справедливо для `String`, `FixedString`, а также для них же, обёрнутых в
`LowCardinality`, `Nullable` или `SimpleAggregateFunction`, и для строк внутри `Array` и `Map`,
включая ключи map.

<Note>
  Массив байтов, тип которого JSON-считыватель определить не может, всё же отображается в base64:
  типизированный путь `Variant` или `Dynamic` содержит значение, тип которого известен только для каждой отдельной строки, поэтому строка
  под `Variant(Array(UInt8), String)` возвращается закодированной в base64. Это одинаково при обоих значениях настройки.

  Тип ключа JSON-map, отличный от строго `String` — например, `Map(LowCardinality(String), String)` —
  приводит к `NotSupportedException`.
</Note>

<h5 id="overlapping-paths">
  Пересекающиеся пути
</h5>

ClickHouse допускает столбец, в котором путь объявлен одновременно и как значение, и как родитель другого
пути, например `JSON(a Int64, a.b Int64)`. Оба пути присутствуют в каждой строке, поэтому сервер
формирует строку с дублирующимся ключом: `{"a":0,"a":{"b":7}}`. Объект `JsonObject` не может хранить два значения
для одного ключа, поэтому `JsonReadMode.Binary` генерирует исключение `SerializationException` с указанием обоих путей. То
же самое происходит, когда значением является `Map`, как в случае `JSON(a Map(String, Int64))`, прочитанного из строки, в
которой также есть динамический `a.b`.

Это относится только к случаям, когда в данной строке значение есть у обеих сторон. Сторона, в которой ничего нет — null,
пустой объект или поддерево, все значения которого равны null, — уступает стороне с данными,
независимо от того, какой из двух путей сервер отправит первым. Поэтому пересечение, объявленное с типами `Nullable`, заполняет по одной стороне на строку и читается без
ошибок: `JSON(a Nullable(Int64), a.b Nullable(Int64))` даёт `{"a":5}` и `{"a":{"b":7}}`, как и
ожидается.

Читайте такой столбец в режиме `JsonReadMode.String`, чтобы получить JSON-текст сервера без изменений, включая дублирующийся
ключ.

Задайте `AllowDuplicateJsonKeys`, чтобы столбец по-прежнему читался как `JsonObject`, а не приводил к исключению. В этом случае
драйвер сохраняет то из двух значений, которое идёт в строке последним, и отбрасывает второе, поэтому
результат получается с потерями: `JSON(a Int64, a.b Int64)`, содержащий `{"a.b":7}`, читается как `{"a":0}`. Путь, у которого
есть значение и родитель которого содержит скаляр или массив, всё равно приводит к исключению, поскольку поддерево нельзя
разместить ни под тем, ни под другим.

```csharp theme={null}
var settings = new ClickHouseClientSettings("Host=localhost")
{
    AllowDuplicateJsonKeys = true
};

// Or via connection string
// "Host=localhost;AllowDuplicateJsonKeys=true"
```

***

<h4 id="type-map-reading-map">
  Map type
</h4>

| ClickHouse Type | .NET Type | Notes |
| - | - | - |
| Map(K, V) | `Dictionary<K, V>` | По умолчанию (`MapReadMode=Dictionary`) |
| Map(K, V) | `List<KeyValuePair<K, V>>` | При `MapReadMode=KeyValuePairs` |

Тип `Map(K, V)` в ClickHouse физически представляет собой `Array(Tuple(K, V))` и может содержать несколько записей с одним и тем же ключом. `Dictionary` этого не допускает, поэтому в режиме по умолчанию для повторяющегося ключа сохраняется только последнее значение, а предыдущие пары отбрасываются. Настройка `MapReadMode` задаёт используемое представление:

* **`Dictionary` (по умолчанию)**: возвращает `Dictionary<K, V>`.

* **`KeyValuePairs`**: возвращает `List<KeyValuePair<K, V>>` в том порядке, в котором пары были отправлены сервером, поэтому сохраняются все пары, включая записи с повторяющимися ключами.

```csharp theme={null}
// Configure key-value-pair mode via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    MapReadMode = MapReadMode.KeyValuePairs
};

// Or via connection string
// "Host=localhost;MapReadMode=KeyValuePairs"
```

Режим определяет тип .NET-представления для столбца `Map`, поэтому он влияет и на `GetFieldValue<T>`, и на типы схемы, сообщаемые драйвером, и на сопоставление свойств POCO. Он действует везде, где map встречается в дереве типов столбца, включая `Array(Map(...))`, `Map(K, Map(...))`, `Tuple(..., Map(...))` и `Dynamic`.

При записи в любом из режимов принимаются оба представления — см. [запись maps](#type-map-writing-other).

***

<h4 id="type-map-reading-other">
  Другие типы
</h4>

| Тип ClickHouse | Тип .NET |
| - | - |
| UUID | `Guid` |
| IPv4 | `IPAddress` |
| IPv6 | `IPAddress` |
| Nothing | `DBNull` |
| Dynamic | См. примечание |
| Array(T) | `T[]` (вложенный `Array(Array(T))` читается как ступенчатый `T[][]`; используйте `reader.GetFieldValue<T[,]>(ordinal)` для материализации прямоугольных данных в виде многомерного массива CLR) |
| Tuple(T1, T2, ...) | `Tuple<T1, T2, ...>` / `LargeTuple` |
| Map(K, V) | `Dictionary<K, V>` или `List<KeyValuePair<K, V>>` при `MapReadMode=KeyValuePairs` — см. [Map type](#type-map-reading-map) |
| Nullable(T) | `T?` |
| Enum8 | `string` |
| Enum16 | `string` |
| LowCardinality(T) | То же, что и T |
| SimpleAggregateFunction | То же, что и базовый тип |
| Nested(...) | `Tuple[]` |
| Variant(T1, T2, ...) | См. примечание |
| QBit(T, dimension) | `T[]` |

<Note>
  Типы Dynamic и Variant преобразуются в тип, соответствующий фактическому базовому типу в каждой строке.
</Note>

***

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

| Тип ClickHouse | Тип .NET |
| - | - |
| Point | `Tuple<double, double>` |
| Ring | `Tuple<double, double>[]` |
| LineString | `Tuple<double, double>[]` |
| Polygon | `Ring[]` |
| MultiLineString | `LineString[]` |
| MultiPolygon | `Polygon[]` |
| Geometry | См. примечание |

<Note>
  Тип Geometry — это Variant, который может содержать любой из геометрических типов. Он будет преобразован в соответствующий тип.
</Note>

***

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

При вставке данных драйвер преобразует типы .NET в соответствующие типы ClickHouse. В таблицах ниже показано, какие типы .NET допускаются для каждого типа столбца ClickHouse.

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

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Int8 | `sbyte`, любой тип, совместимый с `Convert.ToSByte()` | |
| UInt8 | `byte`, любой тип, совместимый с `Convert.ToByte()` | |
| Int16 | `short`, любой тип, совместимый с `Convert.ToInt16()` | |
| UInt16 | `ushort`, любой тип, совместимый с `Convert.ToUInt16()` | |
| Int32 | `int`, любой тип, совместимый с `Convert.ToInt32()` | |
| UInt32 | `uint`, любой тип, совместимый с `Convert.ToUInt32()` | |
| Int64 | `long`, любой тип, совместимый с `Convert.ToInt64()` | |
| UInt64 | `ulong`, любой тип, совместимый с `Convert.ToUInt64()` | |
| Int128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, любой тип, совместимый с `Convert.ToInt64()` | |
| UInt128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, любой тип, совместимый с `Convert.ToInt64()` | |
| Int256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, любой тип, совместимый с `Convert.ToInt64()` | |
| UInt256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, любой тип, совместимый с `Convert.ToInt64()` | |

***

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

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Float32 | `float`, любой тип, совместимый с `Convert.ToSingle()` | |
| Float64 | `double`, любой тип, совместимый с `Convert.ToDouble()` | |
| BFloat16 | `float`, любой тип, совместимый с `Convert.ToSingle()` | Преобразуется в 16-битный формат brain floating point с усечением |

***

<h4 id="type-map-writing-boolean">
  Логический тип
</h4>

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Bool | `bool` | |

***

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

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

***

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

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Date | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | Преобразуется в дни Unix как UInt16; поддерживаемый диапазон `[1970-01-01, 2149-06-06]` |
| Date32 | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | Преобразуется в дни Unix как Int32; поддерживаемый диапазон `[1900-01-01, 2299-12-31]` |
| DateTime | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | Подробности см. ниже; поддерживаемый диапазон `[1970-01-01, 2106-02-07 06:28:15]` UTC |
| DateTime32 | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | То же, что и DateTime |
| DateTime64 | `DateTime`, `DateTimeOffset`, `DateOnly`, типы NodaTime | Точность зависит от параметра Scale |
| Time | `TimeSpan`, `TimeOnly`, `int` | Ограничивается диапазоном ±999:59:59; `int` интерпретируется как секунды |
| Time64 | `TimeSpan`, `TimeOnly`, `decimal`, `double`, `float`, `int`, `long`, `string` | Строка разбирается как `[-]HHH:MM:SS[.fraction]`; ограничивается диапазоном ±999:59:59.999999999 |

<Note>
  **Значения вне диапазона**

  При записи по бинарному пути значения `Date`, `Date32`, `DateTime` и `DateTime32`, выходящие за пределы поддерживаемого диапазона, вызывают `ArgumentOutOfRangeException` во время `Write`, при этом указываются тип столбца и поддерживаемый диапазон. Ранее значения вне диапазона могли молча усекаться через 32-битное целое число и затем переинтерпретироваться сервером, что приводило к появлению реальных, но неверных временных меток.
</Note>

Драйвер учитывает `DateTime.Kind` при записи значений:

| DateTime.Kind | HTTP-параметры | Пакетная вставка |
| - | - | - |
| Utc | Точный момент времени сохраняется | Точный момент времени сохраняется |
| Local | Точный момент времени сохраняется | Точный момент времени сохраняется |
| Unspecified | Интерпретируется как локальное время в часовом поясе типа параметра (по умолчанию UTC) | Интерпретируется как локальное время в часовом поясе столбца |

Значения `DateTimeOffset` всегда сохраняют точный момент времени.

**Пример: UTC DateTime (точный момент времени сохраняется)**

```csharp theme={null}
var utcTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
// Stored as 12:00 UTC
// Read from DateTime('Europe/Amsterdam') column: 13:00 (UTC+1)
// Read from DateTime('UTC') column: 12:00 UTC
```

**Пример: DateTime без указания часового пояса (время по местным часам)**

```csharp theme={null}
var wallClock = new DateTime(2024, 1, 15, 14, 30, 0, DateTimeKind.Unspecified);
// Written to DateTime('Europe/Amsterdam') column: stored as 14:30 Amsterdam time
// Read back from DateTime('Europe/Amsterdam') column: 14:30
```

**Рекомендация:** для максимально простого и предсказуемого поведения используйте `DateTimeKind.Utc` или `DateTimeOffset` во всех операциях с DateTime. Это гарантирует, что ваш код будет работать одинаково независимо от часового пояса сервера, клиента или столбца.

<h4 id="datetime-http-param-vs-bulkcopy">
  HTTP-параметры vs пакетная загрузка
</h4>

При записи значений DateTime с `Unspecified` есть важное различие между привязкой HTTP-параметров и пакетной загрузкой:

**Bulk Copy** знает часовой пояс целевого столбца и корректно интерпретирует значения `Unspecified` в этом часовом поясе.

**HTTP Parameters** не знают часовой пояс столбца автоматически. Его необходимо указать в подсказке типа SQL:

```csharp theme={null}
// ВЕРНО: Часовой пояс в подсказке типа SQL — тип извлекается автоматически
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// НЕВЕРНО: Без подсказки часового пояса интерпретируется как UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// Строковое значение "2024-01-15 14:30:00" интерпретируется как UTC, а не как амстердамское время!
```

| `DateTime.Kind` | Целевой столбец | HTTP-параметр (с указанием часового пояса) | HTTP-параметр (без указания часового пояса) | Пакетная загрузка |
| - | - | - | - | - |
| `Utc` | UTC | Точный момент времени сохраняется | Точный момент времени сохраняется | Точный момент времени сохраняется |
| `Utc` | Europe/Amsterdam | Точный момент времени сохраняется | Точный момент времени сохраняется | Точный момент времени сохраняется |
| `Local` | Любой | Точный момент времени сохраняется | Точный момент времени сохраняется | Точный момент времени сохраняется |
| `Unspecified` | UTC | Интерпретируется как UTC | Интерпретируется как UTC | Интерпретируется как UTC |
| `Unspecified` | Europe/Amsterdam | Интерпретируется как время Амстердама | **Интерпретируется как UTC** | Интерпретируется как время Амстердама |

***

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

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Decimal(P,S) | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Вызывает `OverflowException` при превышении точности |
| Decimal32 | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Максимальная точность 9 |
| Decimal64 | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Максимальная точность 18 |
| Decimal128 | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Максимальная точность 38 |
| Decimal256 | `decimal`, `ClickHouseDecimal`, любой тип, совместимый с `Convert.ToDecimal()` | Максимальная точность 76 |

***

<h4 id="type-map-writing-json">
  Тип JSON
</h4>

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Json | `string`, `JsonObject`, `JsonNode`, любой объект | Поведение зависит от настройки `JsonWriteMode` |

Поведение при записи JSON определяется настройкой `JsonWriteMode`:

| Тип входных данных | `JsonWriteMode.String` (по умолчанию) | `JsonWriteMode.Binary` |
| - | - | - |
| `string` | Передаётся как есть | Генерирует `ArgumentException` |
| `JsonObject` | Сериализуется через `ToJsonString()` | Генерирует `ArgumentException` |
| `JsonNode` | Сериализуется через `ToJsonString()` | Генерирует `ArgumentException` |
| Зарегистрированный POCO | Сериализуется через `JsonSerializer.Serialize()` | Двоичное кодирование с подсказками типов, поддерживаются пользовательские атрибуты путей |
| Незарегистрированный POCO / анонимный объект | Сериализуется через `JsonSerializer.Serialize()` | Вызывает `ClickHouseJsonSerializationException` |

* **`String` (по умолчанию)**: Принимает `string`, `JsonObject`, `JsonNode` или любой объект. Все входные данные сериализуются через `System.Text.Json.JsonSerializer` и отправляются как JSON-строки для разбора на стороне сервера. Это самый гибкий режим, который работает без регистрации типов.

* **`Binary`**: Принимает только зарегистрированные типы POCO. На стороне клиента данные преобразуются в двоичный JSON-формат ClickHouse с полной поддержкой подсказок типов. Перед использованием необходимо вызвать `connection.RegisterJsonSerializationType<T>()`. Запись значений `string` или `JsonNode` в этом режиме генерирует `ArgumentException`.

```csharp theme={null}
// Режим String по умолчанию работает с любыми входными данными
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// Режим Binary требует явного включения и регистрации типа
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
```

<h5 id="json-typed-columns">
  Типизированные JSON-столбцы
</h5>

Когда у JSON-столбца есть подсказки типа (например, `JSON(id UInt64, price Decimal128(2))`), драйвер использует их для сериализации значений с полным сохранением точности типов. Это позволяет сохранить точность для таких типов, как `UInt64`, `Decimal`, `UUID` и `DateTime64`, которая иначе могла бы теряться при сериализации в обычный JSON.

<h5 id="json-poco-serialization">
  Сериализация POCO
</h5>

POCO можно записывать в JSON-столбцы двумя способами в зависимости от `JsonWriteMode`:

**Режим String (по умолчанию)**: POCO сериализуются через `System.Text.Json.JsonSerializer`. Регистрировать типы не требуется. Это самый простой вариант, и он работает с анонимными объектами.

**Бинарный режим**: POCO сериализуются с использованием бинарного JSON-формата драйвера с полной поддержкой подсказок типа. Перед использованием типы необходимо зарегистрировать с помощью `connection.RegisterJsonSerializationType<T>()`. Этот режим поддерживает пользовательские сопоставления путей с помощью атрибутов:

* **`[ClickHouseJsonPath("path")]`**: Связывает свойство с пользовательским JSON-путём. Полезно для вложенных структур или когда имя свойства отличается от нужного JSON-ключа. **Работает только в бинарном режиме.**

* **`[ClickHouseJsonIgnore]`**: Исключает свойство из сериализации. **Работает только в бинарном режиме.**

```sql theme={null}
CREATE TABLE events (
    id UInt32,
    data JSON(`user.id` Int64, `user.name` String, Timestamp DateTime64(3))
) ENGINE = MergeTree() ORDER BY id
```

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

public class UserEvent
{
    [ClickHouseJsonPath("user.id")]
    public long UserId { get; set; }

    [ClickHouseJsonPath("user.name")]
    public string UserName { get; set; }

    public DateTime Timestamp { get; set; }

    [ClickHouseJsonIgnore]
    public string InternalData { get; set; }  // Не сериализуется
}

// Для бинарного режима: зарегистрируйте тип и включите бинарный режим
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Вставка POCO — сериализуется в JSON с вложенной структурой через пользовательские атрибуты пути
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// Результирующий JSON: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}
```

Сопоставление имён свойств с подсказками типов столбцов чувствительно к регистру. Свойство `UserId` будет сопоставлено только с подсказкой, заданной как `UserId`, а не `userid`. Это соответствует поведению ClickHouse, где пути вроде `userName` и `UserName` могут сосуществовать как отдельные поля.

**Ограничения (только для режима Binary):**

* Типы POCO должны быть зарегистрированы для подключения с помощью `connection.RegisterJsonSerializationType<T>()` до сериализации. Попытка сериализовать незарегистрированный тип вызывает исключение `ClickHouseJsonSerializationException`.
* Для корректной сериализации свойств словарей и массивов/списков требуются подсказки типов в определении столбца. Без таких подсказок используйте режим String.
* Значения NULL в свойствах POCO записываются только в том случае, если для пути в определении столбца указана подсказка типа `Nullable(T)`. ClickHouse не допускает типы `Nullable` внутри динамических JSON-путей, поэтому свойства со значением null без подсказок пропускаются.
* Атрибуты `ClickHouseJsonPath` и `ClickHouseJsonIgnore` игнорируются в режиме String (они работают только в режиме Binary).

***

<h4 id="type-map-writing-other">
  Другие типы
</h4>

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| UUID | `Guid`, `string` | Строка преобразуется в `Guid` |
| IPv4 | `IPAddress`, `string` | Должен быть IPv4; строка разбирается с помощью `IPAddress.Parse()` |
| IPv6 | `IPAddress`, `string` | Должен быть IPv6; строка разбирается с помощью `IPAddress.Parse()` |
| Nothing | Any | Ничего не записывает (no-op) |
| Dynamic | — | **Не поддерживается** (генерирует `NotImplementedException`) |
| Array(T) | `IList`, `null` | При `null` записывается пустой массив. Для вложенных типов (`Array(Array(T))` и глубже) поддерживаются как ступенчатые массивы (`T[][]`, `List<List<T>>`), так и прямоугольные многомерные массивы CLR (`T[,]`, `T[,,]`, …); ранг CLR должен соответствовать глубине вложенности ClickHouse. |
| Tuple(T1, T2, ...) | `ITuple`, `IList` | Число элементов должно соответствовать арности кортежа. Для случаев с более чем 7 элементами см. [оговорку о ValueTuple](#valuetuple-caveat). |
| Map(K, V) | `IDictionary`, `IEnumerable<KeyValuePair<K, V>>` | Последовательность пар (например, `List<KeyValuePair<K, V>>`, получаемый при `MapReadMode=KeyValuePairs`) допускается в любом режиме чтения и может содержать повторяющиеся ключи. Применимо к бинарным вставкам и к параметрам запроса |
| Nullable(T) | `null`, `DBNull`, или типы, допустимые для T | Перед значением записывается байт флага null |
| Enum8 | `string`, `sbyte`, числовые типы | Для строки выполняется поиск в словаре enum |
| Enum16 | `string`, `short`, числовые типы | Для строки выполняется поиск в словаре enum |
| LowCardinality(T) | Типы, допустимые для T | Обработка делегируется базовому типу |
| SimpleAggregateFunction | Типы, допустимые для базового типа | Обработка делегируется базовому типу |
| Nested(...) | `IList` из кортежей | Число элементов должно соответствовать числу полей |
| Variant(T1, T2, ...) | Значение, соответствующее одному из T1, T2, ... | Генерирует `ArgumentException`, если не найдено совпадение ни с одним типом |
| QBit(T, dim) | `IList` | Обработка делегируется `Array`; размерность используется только как метаданные |

***

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

| Тип ClickHouse | Допустимые типы .NET | Примечания |
| - | - | - |
| Point | `System.Drawing.Point`, `ITuple`, `IList` (2 элемента) | |
| Ring | `IList` из точек | |
| LineString | `IList` из точек | |
| Polygon | `IList` из колец | |
| MultiLineString | `IList` из объектов LineString | |
| MultiPolygon | `IList` из объектов Polygon | |
| Geometry | Любой из перечисленных выше геометрических типов | Variant всех геометрических типов |

***

<h4 id="type-map-writing-not-supported">
  Не поддерживается при записи
</h4>

| Тип ClickHouse | Примечания |
| - | - |
| Dynamic | Возникает `NotImplementedException` |
| AggregateFunction | Возникает `AggregateFunctionException` |

***

<h3 id="nested-type-handling">
  Обработка вложенных типов
</h3>

Вложенные типы ClickHouse (`Nested(...)`) можно читать и записывать как массивы.

```sql theme={null}
CREATE TABLE test.nested (
    id UInt32,
    params Nested (param_id UInt8, param_val String)
) ENGINE = Memory
```

```csharp theme={null}
var row1 = new object[] { 1, new[] { 1, 2, 3 }, new[] { "v1", "v2", "v3" } };
var row2 = new object[] { 2, new[] { 4, 5, 6 }, new[] { "v4", "v5", "v6" } };

await client.InsertBinaryAsync(
    "test.nested",
    new[] { "id", "params.param_id", "params.param_val" },
    new[] { row1, row2 }
);
```

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

Клиент ClickHouse для .NET интегрируется с абстракциями `Microsoft.Extensions.Logging` и предоставляет легковесное логирование, которое можно включить при необходимости. Когда оно включено, драйвер выводит структурированные сообщения о событиях жизненного цикла соединения, выполнении команд, транспортных операциях и операциях массовой вставки. Логирование полностью опционально — приложения, в которых не настроен логгер, продолжают работать без дополнительной нагрузки.

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

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

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

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);
```

<h4 id="logging-appsettings-config">
  Использование appsettings.json
</h4>

Вы можете настроить уровни логирования с помощью стандартной конфигурации .NET:

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

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .Build();

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);
```

<h4 id="logging-inmemory-config">
  Использование конфигурации в памяти
</h4>

Вы также можете настроить в коде уровень детализации журналирования по категориям:

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

var categoriesConfiguration = new Dictionary<string, string>
{
    { "LogLevel:Default", "Warning" },
    { "LogLevel:ClickHouse.Driver.Connection", "Information" },
    { "LogLevel:ClickHouse.Driver.Command", "Debug" }
};

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(categoriesConfiguration)
    .Build();

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(config)
        .AddSimpleConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);
```

<h3 id="logging-categories">
  Категории и источники
</h3>

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

| Категория | Источник | Описание |
| - | - | - |
| `ClickHouse.Driver.Connection` | `ClickHouseConnection` | Жизненный цикл соединения, выбор фабрики HTTP-клиентов, открытие/закрытие соединения, управление сеансом. |
| `ClickHouse.Driver.Command` | `ClickHouseCommand` | Начало/завершение выполнения запроса, время выполнения, идентификаторы запросов, статистика сервера и сведения об ошибках. |
| `ClickHouse.Driver.Transport` | `ClickHouseConnection` | Низкоуровневые запросы для потоковой передачи по HTTP, флаги сжатия, коды состояния ответа и ошибки транспорта. |
| `ClickHouse.Driver.Client` | `ClickHouseClient` | Бинарная вставка, запросы и другие операции |
| `ClickHouse.Driver.NetTrace` | `TraceHelper` | Сетевая трассировка, только при включенном режиме отладки |

<h4 id="logging-config-example">
  Пример: Диагностика проблем с подключением
</h4>

```json theme={null}
{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}
```

Будет записываться в журнал:

* Выбор фабрики HTTP-клиента (пул по умолчанию или одиночное соединение)
* Конфигурация HTTP-обработчика (SocketsHttpHandler или HttpClientHandler)
* Настройки пула соединений (MaxConnectionsPerServer, PooledConnectionLifetime и т. д.)
* Настройки тайм-аутов (ConnectTimeout, Expect100ContinueTimeout и т. д.)
* Настройка SSL/TLS
* События открытия и закрытия соединения
* Отслеживание идентификатора сеанса

<h3 id="logging-debugmode">
  Режим отладки: сетевая трассировка и диагностика
</h3>

Чтобы упростить диагностику сетевых проблем, библиотека драйвера содержит вспомогательный механизм, который включает низкоуровневую трассировку внутренних механизмов сетевой подсистемы .NET. Чтобы включить его, необходимо передать LoggerFactory с установленным уровнем Trace и задать EnableDebugMode = true (или включить его вручную через класс `ClickHouse.Driver.Diagnostic.TraceHelper`). События будут записываться в категорию `ClickHouse.Driver.NetTrace`. Предупреждение: это приведет к созданию чрезвычайно подробных журналов и повлияет на производительность. Включать режим отладки в продакшн не рекомендуется.

```csharp theme={null}
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Необходим уровень Trace для отображения сетевых событий
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Включить низкоуровневую трассировку сети
};
```

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

Драйвер поддерживает встроенную распределённую трассировку OpenTelemetry через API .NET [`System.Diagnostics.Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). При включении драйвер создаёт спаны для операций с базой данных, которые можно экспортировать в системы обсервабилити, такие как Jaeger или сам ClickHouse (через [OpenTelemetry Collector](/ru/guides/use-cases/observability/build-your-own/integrating-opentelemetry)).

<h3 id="opentelemetry-enabling">
  Включение трассировки
</h3>

В приложениях ASP.NET Core добавьте `ActivitySource` драйвера ClickHouse в конфигурацию OpenTelemetry:

```csharp theme={null}
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Подписка на spans драйвера ClickHouse
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // Или AddJaegerExporter() и т.д.
```

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

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

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

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

Каждый спан включает стандартные атрибуты OpenTelemetry для базы данных, а также специфичную для ClickHouse статистику запросов, которую можно использовать для отладки.

| Атрибут | Описание |
| - | - |
| `db.system` | Всегда `"clickhouse"` |
| `db.name` | Имя базы данных |
| `db.user` | Имя пользователя |
| `db.statement` | SQL-запрос (если включен) |
| `db.clickhouse.read_rows` | Строки, прочитанные запросом |
| `db.clickhouse.read_bytes` | Байты, прочитанные запросом |
| `db.clickhouse.written_rows` | Строки, записанные запросом |
| `db.clickhouse.written_bytes` | Байты, записанные запросом |
| `db.clickhouse.elapsed_ns` | Время выполнения на стороне сервера в наносекундах |

<h3 id="opentelemetry-configuration">
  Параметры конфигурации
</h3>

Настройте поведение трассировки с помощью `ClickHouseDiagnosticsOptions`:

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

// Включать SQL-операторы в spans (по умолчанию: false из соображений безопасности)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Усекать длинные SQL-операторы (по умолчанию: 1000 символов)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;
```

<Warning>
  Включение `IncludeSqlInActivityTags` может привести к раскрытию конфиденциальных данных в трассировках. Используйте с осторожностью в продакшн-средах.
</Warning>

<h2 id="tls-configuration">
  Настройка TLS
</h2>

При подключении к ClickHouse по HTTPS поведение TLS/SSL можно настроить несколькими способами.

<h3 id="custom-certificate-validation">
  Пользовательская проверка сертификатов
</h3>

Для продакшн-окружений, в которых требуется пользовательская логика проверки сертификатов, передайте собственный `HttpClient` с настроенным обработчиком `ServerCertificateCustomValidationCallback`:

```csharp theme={null}
using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // No AutomaticDecompression needed: the driver decodes compressed responses itself.
    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // Example: Accept a specific certificate thumbprint
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // Example: Accept certificates from a specific issuer
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // Default: Use standard validation
        return sslPolicyErrors == SslPolicyErrors.None;
    },
};

var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(5) };

var settings = new ClickHouseClientSettings
{
    Host = "my.clickhouse.server",
    Protocol = "https",
    HttpClient = httpClient,
};

using var client = new ClickHouseClient(settings);
```

<Note>
  Важные моменты при использовании пользовательского HttpClient

  * **Автоматическая декомпрессия**: оставьте `AutomaticDecompression` выключенным. Драйвер сам декодирует сжатые ответы, поэтому она не нужна — и её включение играет против вас на стороне запроса: при отправке обработчик *дополнительно добавляет* каждый алгоритм из своей маски в исходящий заголовок `Accept-Encoding`, расширяя набор, объявленный драйвером, так что ClickHouse может ответить кодеком, который вы не запрашивали. См. [Декомпрессия ответа](#response-decompression).
  * **Тайм-аут простоя**: Установите `PooledConnectionIdleTimeout` меньше значения `keep_alive_timeout` сервера (10 секунд для ClickHouse Cloud), чтобы избежать ошибок подключения из-за полуоткрытых соединений.
</Note>

<h2 id="performance-tuning">
  Настройка производительности
</h2>

В этом разделе описывается, как добиться оптимальной производительности при работе с клиентом, а также какие параметры можно настроить, чтобы клиент работал эффективно в вашем конкретном сценарии использования.

<h3 id="perf-at-a-glance">
  Кратко
</h3>

\| Если вы | Сделайте так |
\|---|---|---|
\| Читаете строки в POCO | Используйте [`QueryAsync<T>`](#perf-read-path), а не `MapTo<T>` |
\| Выполняете крупные вставки | Увеличьте [`InsertOptions.BatchSize`](#perf-insert-batching) |
\| Запускаете консольное или воркер-приложение с интенсивными вставками | Включите [Server GC](#perf-gc) |
\| Читаете большие результаты по сети | Оставьте сжатие ответов включённым (как по умолчанию) |
\| Вставляете данные по быстрому каналу | Попробуйте [`InsertOptions.Compressor = null`](#perf-compression) |
\| Много раз вставляете в одну и ту же таблицу | Используйте [`UseSchemaCache` или `ColumnTypes`](#skip-schema-query) |
\| Читаете очень большие результаты | Увеличьте [`ReadBufferSize`](#perf-buffers) |

***

<h3 id="perf-read-path">
  Чтение: выбор пути материализации
</h3>

Получить строку из результата можно тремя способами, и обходятся они по-разному. Некоторые из путей упаковывают значения, что увеличивает число выделений памяти и снижает производительность.

| Способ чтения | Упаковка каждого значения | Примечания |
| - | - | - |
| `QueryAsync<T>` | **Нет** | Читает из потока напрямую в ваши свойства. Быстрый путь. |
| Типизированные аксессоры reader (`GetInt32`, `GetInt64`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetFieldValue<T>`) | **Нет** | Чтение без упаковки из типизированного хранилища значений. |
| `MapTo<T>` | Да | Сначала материализует строку, затем копирует из неё значения. |
| `GetValue` и `GetValues` | Да | Они возвращают `object`, поэтому при обращении значение приходится упаковывать. |

Для чтения 1 000 000 строк по 105 столбцам набора данных *hits*:

| API | Выделено памяти |
| - | -: |
| `QueryAsync<T>` | **1 372 МБ** |
| `MapTo<T>` | 3 133 МБ |

```csharp theme={null}
// Fast path: register the type once, then stream rows directly into it.
client.RegisterPocoType<HitRow>();

await foreach (var row in client.QueryAsync<HitRow>("SELECT * FROM hits"))
    Process(row);
```

<Note>
  *ORM получают быстрый путь, когда используют типизированные аксессоры.* linq2db регистрирует `GetInt64`,
  `GetDouble` и `GetDateTime` для каждого столбца, поэтому чтение выполняется без упаковки. Код, который читает через
  `GetValue` (включая результат типа `dynamic` из Dapper), упаковывает каждое значение. Если запрос ORM выполняется часто
  и читает через `GetValue`, используйте для него `QueryAsync<T>`.
</Note>

***

<h3 id="perf-insert-batching">
  Вставка: размер батча и параллелизм
</h3>

Размер батча — главный фактор, влияющий на пропускную способность вставки. Значение `InsertOptions.BatchSize` по умолчанию — 100 000 строк.

**Используйте большие батчи.** При вставке 1 000 000 строк увеличение размера батча с 10 000 до 100 000 строк дало:

| Вставка | 10 000 строк на батч | 100 000 строк на батч | |
| - | -: | -: | -: |
| POCO | 15 308 мс | 7 853 мс | −49% |
| `object[]` | 17 027 мс | 10 671 мс | −37% |

Если размером батча управлять нельзя (например, когда множество мелких producer'ов отправляют строки независимо друг от друга), используйте [async inserts](#async-inserts) и доверьте батчинг серверу.

**Параллельная отправка.** Значение `InsertOptions.MaxDegreeOfParallelism` по умолчанию равно `1`. Увеличьте его, чтобы отправлять батчи одновременно. Наибольший эффект это даёт при включённом сжатии, поскольку каждый батч сжимается в собственном потоке. Сеансы с параллельными вставками не работают: либо отключите сеансы, либо оставьте `MaxDegreeOfParallelism = 1`.

**Уберите schema probe.** Каждый вызов `InsertBinaryAsync` сначала отправляет запрос `SELECT ... WHERE 1=0`, чтобы определить типы столбцов. См. [Пропуск запроса schema probe](#skip-schema-query) — это позволит исключить лишний обмен с сервером с помощью `ColumnTypes` или `UseSchemaCache`.

<Note>
  Путь вставки без упаковки значений применяется к формату `RowBinary`, используемому по умолчанию. `RowBinaryWithDefaults` вынужден проверять каждое значение на наличие маркера `DBDefault`, поэтому для него сохраняется более медленный путь.
</Note>

***

<h3 id="perf-compression">
  Сжатие: два направления передачи ведут себя по-разному
</h3>

Сжатие меняет CPU на байты. Выгоден ли такой обмен, зависит от направления передачи, пропускной способности соединения с сервером ClickHouse, от того, насколько хорошо ваши данные поддаются выбранному алгоритму сжатия, а также от того, платите ли вы за каждый переданный байт.

**Чтение:** оставьте сжатие включённым, если только сервер не работает на той же машине. Это поведение по умолчанию. По сравнению с передачей без сжатия `zstd` на уровне 1 дал:

| От клиента к серверу | Эффект сжатия |
| - | - |
| Тот же хост (loopback) | Потеря 8% |
| Тот же облачный регион | **Выигрыш 16%** |
| Соседний регион | **Выигрыш 33%** |

**Вставки:** сначала измерьте, потом включайте сжатие. Экономия может оказаться недостаточной, чтобы оправдать его включение. Учитывайте также, что распаковка создаёт дополнительную нагрузку на сервер: для Zstd и LZ4 она умеренная, но для других алгоритмов (например, Brotli) может быть высокой.

Чтобы отключить сжатие при вставке:

```csharp theme={null}
var options = new InsertOptions { Compressor = null };
await client.InsertBinaryAsync("my_table", columns, rows, options);
```

О выборе кодека, уровнях сжатия и о том, как найти собственную точку перехода, см. раздел
[Настройка сжатия](#tuning-compression).

***

<h3 id="perf-buffers">
  Буферы
</h3>

`ReadBufferSize` задаёт размер буфера, в который считываются HTTP-ответы. Значение по умолчанию — 64 КиБ.

Драйвер берёт этот буфер из общего пула и возвращает обратно при освобождении reader, поэтому память не выделяется заново для каждого запроса. Увеличьте размер буфера, чтобы сократить число его перезаполнений при больших результатах. Драйвер удерживает по одному буферу на каждый одновременно открытый reader, поэтому потребление памяти растёт вместе с размером буфера и числом параллельных reader.

```csharp theme={null}
var settings = new ClickHouseClientSettings("Host=localhost") { ReadBufferSize = 256 * 1024 };
```

<Warning>
  *Всегда освобождайте reader'ы.* При освобождении reader возвращает свой буфер в пул и закрывает
  HTTP-соединение. Если же оставить reader без освобождения, буфер в пул не вернётся, а HTTP-соединение
  может остаться недоступным; обычная сборка мусора не заменяет явное освобождение.
</Warning>

***

<h3 id="perf-gc">
  Среда выполнения и сборка мусора
</h3>

**Включайте Server GC для приложений с интенсивной вставкой данных.** При одном и том же коде и одинаковом
количестве выделенных байтов Workstation GC работал на вставках до 97% медленнее, чем Server GC.

```xml theme={null}
<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
```

В проектах ASP.NET Core это уже настроено. В консольных приложениях, воркер-сервисах и большинстве контейнерных образов — нет.

Причина в размере бюджета поколения 0. Workstation GC использует небольшой бюджет, поэтому короткоживущие буферы, создаваемые при вставке, не успевают освободиться в поколении 0. Вместо этого они переходят в поколение 1, что усиливает продвижение объектов и приводит к значительно большему объёму работы с поколением 2. В одном из сценариев вставки число сборок поколения 2 на каждые 1 000 операций составило 4 000 с Server GC и 73 000 с Workstation GC.

<Note>
  Server GC — это настройка пропускной способности, а не задержки. В тех же измерениях Server GC суммарно провёл в паузах менее половины времени, но отдельные паузы были длиннее (95-й перцентиль — 114,6 мс против 61,9 мс). Если ваш сервис чувствителен к задержкам в хвосте распределения, измерьте оба режима, прежде чем делать выбор.
</Note>

***

<h3 id="perf-latency">
  Задержка: переиспользуйте соединения
</h3>

Установление нового TCP-соединения и выполнение TLS-рукопожатия занимает значительное время.
Переиспользование соединений существенно снизит задержку ваших запросов.

* Не создавайте отдельный клиент для каждого запроса. Каждый новый клиент со своим `HttpClient` создаёт новый
  пул соединений и снова тратит время на рукопожатие. Используйте один `ClickHouseClient` на всё время жизни приложения. Он потокобезопасен и рассчитан на
  использование в качестве singleton.
* Для ADO.NET и ORM используйте `ClickHouseDataSource`, чтобы все подключения использовали общий пул.

Полный набор рекомендуемых подходов см. в разделе
[Время жизни подключения и пулинг](#best-practices-connection-lifetime).

***

<h3 id="perf-measuring">
  Измеряйте сами
</h3>

Во многих случаях производительность зависит от структуры ваших данных, скорости соединения с сервером,
от того, готовы ли вы разменять CPU клиента на CPU сервера (или наоборот), от ограничений вашего оборудования и т. д.
Поэтому рекомендуется измерять производительность самостоятельно, на своих данных и в своём окружении.

Чтобы увидеть, какую часть работы выполняет сервер, задайте `QueryOptions.QueryId` и прочитайте счётчики:

```sql theme={null}
SELECT ProfileEvents['UserTimeMicroseconds'] + ProfileEvents['SystemTimeMicroseconds'] AS cpu_us,
       ProfileEvents['NetworkSendBytes'] AS sent_bytes,
       query_duration_ms
FROM system.query_log
WHERE query_id = 'your-query-id' AND type = 'QueryFinish';
```

***

<h2 id="orm-support">
  Поддержка ORM
</h2>

Для ORM требуется API ADO.NET (`ClickHouseConnection`). Чтобы корректно управлять временем жизни подключения, создавайте подключения через `ClickHouseDataSource`:

```csharp theme={null}
// Зарегистрировать DataSource как singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// Создать подключения для использования с ORM
await using var connection = await dataSource.OpenConnectionAsync();
// Передать подключение в вашу ORM...
```

<h3 id="orm-support-dapper">
  Dapper
</h3>

`ClickHouse.Driver` работает с Dapper. Драйвер автоматически преобразует синтаксис Dapper `@parameter` в нативный для ClickHouse синтаксис `{parameter:Type}`, при этом типы выводятся автоматически на основе значений .NET.

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

```csharp theme={null}
var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Зарегистрировать как singleton в DI

using var connection = dataSource.CreateConnection();
```

<h4 id="dapper-parameter-passing">
  Способы передачи параметров
</h4>

Поддерживаются все стандартные способы передачи параметров в Dapper:

**Анонимные объекты:**

```csharp theme={null}
await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)",
    new { Id = 1, Name = "alice", Balance = 3.14 });
```

**Классы POCO:**

```csharp theme={null}
class InsertParams
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

var param = new InsertParams { Id = 42, Name = "bob", Balance = 99.9 };
await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)", param);
```

**Словарь:**

```csharp theme={null}
var parameters = new Dictionary<string, object> { { "Id", 2 } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", parameters);
```

**`DynamicParameters` (из словаря или анонимного объекта):**

```csharp theme={null}
var dynParams = new DynamicParameters(new { Id = 1 });
// или: new DynamicParameters(new Dictionary<string, object> { { "Id", 1 } });

var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", dynParams);
```

<h4 id="dapper-pocos">
  Запросы в объекты POCO
</h4>

Dapper сопоставляет столбцы со свойствами по имени (регистронезависимо):

```csharp theme={null}
class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

// Из таблицы
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// Из литерала
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();
```

<h4 id="dapper-clickhouse-param-syntax">
  Собственный синтаксис параметров ClickHouse
</h4>

Если вам нужен явный контроль над типами, используйте непосредственно в SQL синтаксис ClickHouse `{param:Type}`, а значения параметров передавайте через `Dictionary<string, object>`. Не используйте синтаксис `@param` и синтаксис `{param:Type}` одновременно для одного и того же параметра.

```csharp theme={null}
var parameters = new Dictionary<string, object> { { "value", 42 } };
var result = await connection.QueryAsync<int>("SELECT {value:Int32}", parameters);
```

<h4 id="dapper-where-in">
  WHERE IN
</h4>

**Встроенное в Dapper раскрытие IN работает:**

```csharp theme={null}
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id IN @Ids ORDER BY id",
    new { Ids = new[] { 1, 3, 5 } });
```

Dapper преобразует это в `WHERE id IN (@Ids1, @Ids2, @Ids3)`, а драйвер обрабатывает каждый развёрнутый параметр.

**Функция `has()` в ClickHouse с параметром `Array` тоже работает:**

```csharp theme={null}
var parameters = new Dictionary<string, object> { { "ids", new[] { 1, 3, 5 } } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE has({ids:Array(Int32)}, id) ORDER BY id",
    parameters);
```

<h4 id="dapper-type-handlers">
  Пользовательские обработчики типов
</h4>

Для некоторых типов ClickHouse, например `ITuple`, `BigInteger` и `ClickHouseDecimal`, необходимо зарегистрировать обработчики при запуске:

```csharp theme={null}
// ClickHouseDecimal (для столбцов Decimal64/128/256)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger (для столбцов Int128/Int256/UInt128/UInt256)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (для столбцов IPv4/IPv6)
SqlMapper.AddTypeHandler(new IpAddressHandler());
```

См. [пример Dapper](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/ORM/ORM_001_Dapper.cs), где показана реализация обработчика типов.

<h4 id="dapper-contrib">
  Dapper.Contrib
</h4>

`GetAll<T>()` и `Get<T>(id)` работают. `Insert<T>()` не работает — этот метод генерирует синтаксис SQL Server (`SCOPE_IDENTITY`, `[]`). Вместо него рекомендуется использовать нативный метод `InsertBinaryAsync` клиента `ClickHouseClient`.

```csharp theme={null}
[Table("test.users")]
record class UserRecord(int Id, string Name, DateTime Timestamp);

var all = await connection.GetAllAsync<UserRecord>();
var one = await connection.GetAsync<UserRecord>(1);
```

Имена свойств должны в точности совпадать с именами столбцов ClickHouse (с учетом регистра).

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

| Что | Статус | Подробности |
| - | - | - |
| Tuple как **результат** | Работает | Требуется регистрация `SqlMapper.TypeHandler<ITuple>` |
| Tuple как **параметр** | Не поддерживается | Dapper не может сериализовать `ITuple`/`Tuple<>` в качестве значения `DbParameter` |
| Вложенные типы как параметры | Не поддерживается | По той же причине — Dapper отклоняет сложные типы в качестве значений параметров |
| Гео-типы как параметры | Не поддерживается | Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon |
| `Dapper.Contrib.Insert<T>()` | Не поддерживается | Генерирует синтаксис, специфичный для SQL Server |
| Тип `Nothing` | Не поддерживается | В .NET нет осмысленного представления |

<h3 id="orm-support-linq2db">
  Linq2db
</h3>

Этот драйвер совместим с [linq2db](https://github.com/linq2db/linq2db) — легковесным ORM и LINQ-провайдером для .NET. Подробную документацию см. на сайте проекта.

**Пример использования:**

Создайте `DataConnection`, используя провайдер ClickHouse:

```csharp theme={null}
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.ClickHouse;

var connectionString = "Host=localhost;Port=8123;Database=default";
var options = new DataOptions()
    .UseClickHouse(connectionString, ClickHouseProvider.ClickHouseDriver);

await using var db = new DataConnection(options);
```

Сопоставление таблиц можно задать с помощью атрибутов или Fluent-конфигурации. Если имена класса и его свойств в точности совпадают с именами таблицы и столбцов, конфигурация не требуется:

```csharp theme={null}
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
```

**Запросы:**

```csharp theme={null}
await using var db = new DataConnection(options);

var products = await db.GetTable<Product>()
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Name)
    .ToListAsync();
```

**Пакетная загрузка:**

Используйте `BulkCopyAsync` для эффективной пакетной вставки.

```csharp theme={null}
await using var db = new DataConnection(options);
var table = db.GetTable<Product>();

var options = new BulkCopyOptions
{
    MaxBatchSize = 100000,
    MaxDegreeOfParallelism = 1,
    WithoutSession = true
};

await table.BulkCopyAsync(options, products);
```

<h3 id="orm-support-ef-core">
  Entity Framework Core
</h3>

Официальный провайдер Entity Framework Core для ClickHouse. Сопоставляйте классы C# с таблицами ClickHouse, выполняйте запросы с помощью LINQ и добавляйте данные через `SaveChanges` — всё это в привычных шаблонах EF Core.

* **NuGet**: [`ClickHouse.EntityFrameworkCore`](https://www.nuget.org/packages/ClickHouse.EntityFrameworkCore)
* **Source**: [GitHub](https://github.com/ClickHouse/ClickHouse.EntityFrameworkCore)

<Note>
  Этот провайдер активно развивается. Текущая версия поддерживает LINQ-запросы (включая JOIN, подзапросы и операции над множествами), `INSERT` через `SaveChanges` / `BulkInsertAsync`, миграции с полной поддержкой DDL (CREATE / ALTER / DROP), а также настройку движка таблицы ClickHouse. `UPDATE` / `DELETE` не поддерживаются.
</Note>

<h4 id="ef-core-installation">
  Установка
</h4>

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

Необходимы .NET 10.0 и EF Core 10.

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

Определите сущность и `DbContext`, затем выполните запрос с помощью LINQ:

```csharp theme={null}
using Microsoft.EntityFrameworkCore;

public class PageView
{
    public long Id { get; set; }
    public string Path { get; set; }
    public DateOnly Date { get; set; }
    public string UserAgent { get; set; }
}

public class AnalyticsContext : DbContext
{
    public DbSet<PageView> PageViews { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseClickHouse("Host=localhost;Database=analytics");
}

// Запрос
await using var ctx = new AnalyticsContext();

var topPages = await ctx.PageViews
    .Where(v => v.Date >= new DateOnly(2024, 1, 1))
    .GroupBy(v => v.Path)
    .Select(g => new { Path = g.Key, Views = g.Count() })
    .OrderByDescending(x => x.Views)
    .Take(10)
    .ToListAsync();
```

<h4 id="ef-core-types">
  Поддерживаемые типы
</h4>

| Категория | Типы ClickHouse | Типы CLR |
| - | - | - |
| **Целые числа** | `Int8`–`Int64`, `UInt8`–`UInt64` | `sbyte`, `short`, `int`, `long`, `byte`, `ushort`, `uint`, `ulong` |
| **Большие целые числа** | `Int128`, `Int256`, `UInt128`, `UInt256` | `BigInteger` |
| **Числа с плавающей точкой** | `Float32`, `Float64`, `BFloat16` | `float`, `double` |
| **Десятичные числа** | `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` | `decimal` или `ClickHouseDecimal` |
| **Bool** | `Bool` | `bool` |
| **Строки** | `String`, `FixedString(N)` | `string` |
| **Перечисления** | `Enum8(...)`, `Enum16(...)` | `string` или C# `enum` |
| **Дата/время** | `Date`, `Date32`, `DateTime`, `DateTime64(P, 'TZ')` | `DateOnly`, `DateTime` |
| **Время** | `Time`, `Time64(N)` | `TimeSpan` |
| **UUID** | `UUID` | `Guid` |
| **Сетевые типы** | `IPv4`, `IPv6` | `IPAddress` |
| **Массивы** | `Array(T)` | `T[]`, `List<T>`, `IList<T>`, `ICollection<T>`, `IReadOnlyList<T>`, `IReadOnlyCollection<T>`, `IEnumerable<T>` |
| **Map** | `Map(K, V)` | `Dictionary<K,V>` |
| **Tuple** | `Tuple(T1, ...)` | `Tuple<...>` или `ValueTuple<...>` |
| **Variant** | `Variant(T1, T2, ...)` | `object` |
| **Dynamic** | `Dynamic` | `object` |
| **JSON** | `Json` | `JsonNode` или `string` |
| **Геопространственные** | `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` | `Tuple<double,double>` и их массивы; `object` для Geometry |
| **Обёртки** | `Nullable(T)`, `LowCardinality(T)` | Автоматически разворачиваются |

Используйте `ClickHouseDecimal` (из `ClickHouse.Driver.Numerics`) вместо `decimal`, если нужна полная точность столбцов `Decimal128`/`Decimal256`: `decimal` в .NET ограничен 28–29 значащими цифрами.

<h4 id="ef-core-linq">
  Поддерживаемые операции LINQ
</h4>

**Запросы:** `Where`, `OrderBy`, `Take`, `Skip`, `Select`, `First`, `Single`, `Any`, `All`, `Count`, `Distinct`, `AsNoTracking`

**GROUP BY и агрегатные функции:** `GroupBy` с `Count`, `LongCount`, `Sum`, `Average`, `Min`, `Max` — включая `HAVING` (`.Where()` после `.GroupBy()`), несколько агрегатных функций в одной проекции и `OrderBy` по результатам агрегации.

**JOIN:** `Join` (INNER), шаблоны `GroupJoin`/`SelectMany` (LEFT и CROSS). LEFT JOIN возвращает реальный `null` для строк без совпадений (см. [семантику NULL в LEFT JOIN](#ef-core-join-nulls) ниже).

**Подзапросы:** коррелированные `Contains` / `IN`, `Any` / `EXISTS`, `All`, а также скалярные подзапросы в проекциях.

**Операции над множествами:** `Concat` (→ `UNION ALL`), `Union` (→ `UNION DISTINCT`), `Intersect`, `Except`.

**Встроенные локальные коллекции:** JOIN и `Contains` с коллекциями в памяти (`int[]`, `List<T>` и т. д.) преобразуются в последовательность UNION.

**Строковые методы:** `Contains`, `StartsWith`, `EndsWith`, `IndexOf`, `Replace`, `Substring`, `Trim`/`TrimStart`/`TrimEnd`, `ToLower`, `ToUpper`, `Length`, `IsNullOrEmpty`, `Concat` (и оператор `+`).

**Математические функции:** стандартные методы `Math` и `MathF`, преобразуемые в эквивалентные функции ClickHouse — арифметические, логарифмические, тригонометрические и вспомогательные функции.

<h5 id="ef-core-join-nulls">
  Семантика NULL в LEFT JOIN
</h5>

Провайдер автоматически добавляет `set_join_use_nulls=1` во все подключения, чтобы поведение JOIN соответствовало ожиданиям Entity Framework.

Если ваш сервер ClickHouse или профиль запрещает изменять эту настройку (например, профиль `readonly=1`), отключите это поведение с помощью:

```csharp theme={null}
optionsBuilder.UseClickHouse(connectionString, o => o.DisableJoinNullSemantics());
```

При включенном opt-out LEFT JOIN возвращает значения столбцов ClickHouse по умолчанию, и механизм определения навигации по null в EF больше не работает должным образом. Используйте явные сравнения с `0` / `""` вместо `== null`.

<h4 id="ef-core-insert">
  Вставка данных
</h4>

`SaveChanges` использует нативный API драйвера `InsertBinaryAsync` — кодирование RowBinary со сжатым телом запроса, что гораздо эффективнее параметризованного SQL:

```csharp theme={null}
await using var ctx = new AnalyticsContext();

ctx.PageViews.Add(new PageView
{
    Id = 1,
    Path = "/home",
    Date = new DateOnly(2024, 6, 15),
    UserAgent = "Mozilla/5.0"
});

await ctx.SaveChangesAsync();
```

Сущности после сохранения переходят из состояния `Added` в `Unchanged`, как и у любого другого поставщика EF Core.

**Размер батча** можно настроить (по умолчанию — 1000):

```csharp theme={null}
optionsBuilder.UseClickHouse("Host=localhost", o => o.MaxBatchSize(5000));
```

<h4 id="ef-core-bulk-insert">
  Пакетная вставка
</h4>

Для высоконагруженной вставки данных используйте `BulkInsertAsync` вместо `SaveChanges`. Это метод расширения для `DbContext`, который полностью обходит механизм отслеживания изменений EF Core, разрешение идентичности и управление состоянием — он напрямую вызывает `InsertBinaryAsync` драйвера с кодированием RowBinary и сжатым телом запроса.

Поэтому он хорошо подходит для загрузки больших наборов данных, когда после вставки отслеживание сущностей не требуется:

```csharp theme={null}
var events = Enumerable.Range(0, 100_000)
    .Select(i => new PageView
    {
        Id = i,
        Path = $"/page/{i}",
        Date = DateOnly.FromDateTime(DateTime.Today)
    });

long rowsInserted = await ctx.BulkInsertAsync(events);
```

На вход можно передать любой `IEnumerable<T>` — сущности обрабатываются последовательно, без загрузки всех данных в память. Возвращаемое значение — количество вставленных строк. Сущности **не** прикрепляются к `DbContext` после вставки, поэтому перехода состояния `Added` → `Unchanged` не происходит.

<h4 id="ef-core-enums">
  Перечисления
</h4>

Столбцы ClickHouse `Enum8`/`Enum16` можно сопоставить со свойствами `string` или типами C# `enum`. При использовании перечислений C# провайдер автоматически преобразует значения перечисления в их строковое представление и обратно:

```csharp theme={null}
public enum Status { Active, Inactive, Pending }

public class User
{
    public long Id { get; set; }
    public Status Status { get; set; }
}

// Запрос с enum-значениями
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();
```

<h4 id="ef-core-value-converters">
  Пользовательские преобразования типов
</h4>

Система `ValueConverter` в EF Core позволяет сопоставлять пользовательские типы с типами, которые уже поддерживает провайдер. Сам провайдер ваш пользовательский тип не видит — EF Core выполняет преобразование на границе.

**Преобразование для отдельного свойства:**

```csharp theme={null}
public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
}

public class Order
{
    public long Id { get; set; }
    public Money Price { get; set; }
}

// В OnModelCreating:
modelBuilder.Entity<Order>()
    .Property(o => o.Price)
    .HasConversion(
        m => $"{m.Amount}|{m.Currency}",
        s => new Money
        {
            Amount = decimal.Parse(s.Split('|')[0]),
            Currency = s.Split('|')[1]
        })
    .HasColumnType("String");
```

**Переиспользуемый класс-конвертер:**

```csharp theme={null}
public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter() : base(
        m => $"{m.Amount}|{m.Currency}",
        s => Parse(s)) { }

    private static Money Parse(string s)
    {
        var parts = s.Split('|');
        return new Money { Amount = decimal.Parse(parts[0]), Currency = parts[1] };
    }
}

// Применить к одному свойству:
.HasConversion<MoneyConverter>()

// Или применить ко всем свойствам типа через соглашения:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}
```

<h4 id="ef-core-column-types">
  Аннотации типов столбцов
</h4>

Для скалярных типов, таких как `string`, `int`, `DateTime` и т. д., провайдер автоматически определяет тип ClickHouse. Для параметризованных типов и типов-обёрток необходимо явно указать тип ClickHouse.

**Использование аннотаций данных (атрибутов):**

```csharp theme={null}
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

[Table("sensor_readings")]
public class SensorReading
{
    public long Id { get; set; }

    [Column(TypeName = "Array(String)")]
    public string[] Tags { get; set; }

    [Column(TypeName = "Map(String, String)")]
    public Dictionary<string, string> Metadata { get; set; }

    [Column(TypeName = "Nullable(Float64)")]
    public double? Value { get; set; }

    [Column(TypeName = "Decimal128(18)")]
    public decimal HighPrecision { get; set; }
}
```

**Использование fluent API в `OnModelCreating`:**

```csharp theme={null}
modelBuilder.Entity<SensorReading>(e =>
{
    e.ToTable("sensor_readings");
    e.Property(x => x.Tags).HasColumnType("Array(String)");
    e.Property(x => x.Metadata).HasColumnType("Map(String, String)");
    e.Property(x => x.Value).HasColumnType("Nullable(Float64)");
    e.Property(x => x.Category).HasColumnType("LowCardinality(String)");
    e.Property(x => x.HighPrecision).HasColumnType("Decimal128(18)");
});
```

Поддерживаются вложенные обёртки, такие как `Array(Nullable(Int32))` и `LowCardinality(Nullable(String))` — провайдер автоматически снимает обёртки `Nullable` и `LowCardinality` на каждом уровне вложенности.

<h4 id="ef-core-variant-dynamic">
  Столбцы Variant и Dynamic
</h4>

Столбцы ClickHouse `Variant(T1, T2, ...)` и `Dynamic` в .NET сопоставляются с `object`. Поскольку `object` — слишком общий тип для автоматического вывода типов, необходимо явно указать тип хранения через `.HasColumnType()`:

```csharp theme={null}
public class Event
{
    public long Id { get; set; }
    public object? Payload { get; set; }
}

// В OnModelCreating:
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// или:
entity.Property(e => e.Payload).HasColumnType("Dynamic");
```

При чтении значение автоматически десериализуется в соответствующий тип .NET на основе сохранённого дискриминатора (например, `string`, `ulong`, `ulong[]`).

<h4 id="ef-core-json">
  JSON-столбцы
</h4>

Провайдер поддерживает тип столбца `Json` в ClickHouse, сопоставляя его с `System.Text.Json.Nodes.JsonNode` (по умолчанию) или `string` (через автоматический `ValueConverter`):

```csharp theme={null}
using System.Text.Json.Nodes;

public class Event
{
    public long Id { get; set; }
    public JsonNode? Data { get; set; }
}

// В OnModelCreating:
entity.Property(e => e.Data).HasColumnType("Json");
```

Чтение и запись JSON поддерживаются как через `SaveChanges`, так и через `BulkInsertAsync`:

```csharp theme={null}
ctx.Events.Add(new Event
{
    Id = 1,
    Data = JsonNode.Parse("""{"action": "click", "x": 100, "y": 200}""")
});
await ctx.SaveChangesAsync();

var ev = await ctx.Events.Where(e => e.Id == 1).SingleAsync();
string action = ev.Data!["action"]!.GetValue<string>(); // "click"
```

Если вы предпочитаете необработанные JSON-строки, задайте для свойства тип `string`, а для столбца — тип `Json` — `ValueConverter` будет применён автоматически:

```csharp theme={null}
public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // необработанная JSON-строка
}

entity.Property(e => e.Data).HasColumnType("Json");
```

<Note>
  * **Пути JSON не транслируются** — `entity.Data["name"]` в LINQ не преобразуется в SQL-синтаксис ClickHouse `data.name`. Фильтруйте по не-JSON-столбцам и анализируйте JSON в памяти.
  * **Семантика NULL** — JSON type в ClickHouse возвращает `{}` (пустой объект) для значений NULL, а не SQL NULL.
  * **Точность целых чисел** — ClickHouse хранит все целые числа в JSON как `Int64`. При чтении через `JsonNode` используйте `GetValue<long>()`, а не `GetValue<int>()`.
</Note>

<h4 id="ef-core-engines">
  Движки таблиц
</h4>

Настраивайте движки таблиц ClickHouse и специфичные для движка секции через fluent API `ToTable(name, t => ...)`. Если движок не настроен, провайдер по умолчанию использует `MergeTree`, а `ORDER BY` определяется на основе первичного ключа сущности.

```csharp theme={null}
modelBuilder.Entity<Event>(e =>
{
    e.ToTable("events", t => t
        .HasMergeTreeEngine()
        .WithOrderBy("UserId", "Timestamp")
        .WithPartitionBy("toYYYYMM(Timestamp)")
        .WithPrimaryKey("UserId")
        .WithSettings("index_granularity = 8192"));
});
```

Поддерживаемые семейства движков:

| Engine | Fluent method | Notes |
| - | - | - |
| `MergeTree` | `HasMergeTreeEngine()` | Используется по умолчанию, если ничего не настроено |
| `ReplacingMergeTree` | `HasReplacingMergeTreeEngine("Version", "IsDeleted")` или `HasReplacingMergeTreeEngine<T>(e => e.Version)` | Столбцы Version / IsDeleted необязательны |
| `SummingMergeTree` | `HasSummingMergeTreeEngine(…)` или `HasSummingMergeTreeEngine<T>(e => new { … })` | Необязательные суммируемые столбцы |
| `AggregatingMergeTree` | `HasAggregatingMergeTreeEngine()` | — |
| `CollapsingMergeTree` | `HasCollapsingMergeTreeEngine("Sign")` или `HasCollapsingMergeTreeEngine<T>(e => e.Sign)` | Столбец `Sign` должен иметь тип `Int8` |
| `VersionedCollapsingMergeTree` | `HasVersionedCollapsingMergeTreeEngine("Sign", "Version")` или `<T>(e => e.Sign, e => e.Version)` | — |
| `GraphiteMergeTree` | `HasGraphiteMergeTreeEngine("config_section")` | — |
| `Log`, `TinyLog`, `StripeLog`, `Memory` | `HasLogEngine()`, `HasTinyLogEngine()`, `HasStripeLogEngine()`, `HasMemoryEngine()` | Без ORDER BY / PARTITION BY |

**Секции движка:** `WithOrderBy`, `WithPartitionBy`, `WithPrimaryKey`, `WithSampleBy`, `WithTtl`, `WithSettings`. Все они применяются к построителю движка, который возвращает `HasXxxEngine()`.

**Возможности на уровне столбца:** `HasCodec`, `HasTtl`, `HasComment`, `HasDefault` — все они участвуют в миграциях.

**Индексы пропуска данных** — через `HasIndex(...).HasSkippingIndexType(...)`:

```csharp theme={null}
modelBuilder.Entity<Event>()
    .HasIndex(e => e.UserId)
    .HasSkippingIndexType("minmax")
    .HasGranularity(4);

// Индекс с параметрами (например, bloom_filter, tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);
```

Стандартные индексы (без пропуска) молча игнорируются, поскольку в ClickHouse нет их аналога. Для уникальных индексов генерируется исключение, так как ClickHouse не поддерживает уникальность.

<h4 id="ef-core-migrations">
  Миграции
</h4>

Стандартный процесс миграций EF Core:

```bash theme={null}
dotnet ef migrations add InitialCreate
dotnet ef database update
```

Поддерживаемые операции:

| Операция | Формирует |
| - | - |
| `CREATE TABLE` | Включает секцию ENGINE, ORDER BY, PARTITION BY, SETTINGS, кодеки/TTL/комментарии/значения по умолчанию для столбцов |
| `ALTER TABLE ADD COLUMN` | — |
| `ALTER TABLE DROP COLUMN` | — |
| `ALTER TABLE MODIFY COLUMN` | Поддерживает изменение типа, а также добавление/удаление аннотаций (CODEC, TTL, COMMENT, DEFAULT) |
| `ALTER TABLE RENAME COLUMN` | — |
| `RENAME TABLE` | — |
| `ALTER TABLE ADD INDEX` / `DROP INDEX` | Только индексы пропуска данных |
| `CREATE DATABASE` / `DROP DATABASE` | Через `EnsureCreated` / `EnsureDeleted` и миграции |

<h4 id="ef-core-limitations">
  Ограничения миграций
</h4>

| Возможность | Причина |
| - | - |
| Внешние ключи | ClickHouse не проверяет внешние ключи. Миграции отклоняют `AddForeignKey`, а валидатор модели выдаёт предупреждение при построении модели. |
| Уникальные ограничения / уникальные индексы | ClickHouse не обеспечивает уникальность. Уникальные индексы вызывают исключение при выполнении миграции. |
| Значения, генерируемые сервером (auto-increment / `IDENTITY`) | В ClickHouse нет эквивалента. |
| Столбцы `Nested(…)` | Пока не поддерживаются как сопоставляемый тип CLR. |
| Принадлежащие сущности в JSON (`.ToJson()`) | Структурное сопоставление JSON для принадлежащих сущностей пока не реализовано. Вместо этого используйте `JsonNode` / `string` в столбце `Json` (см. [JSON-столбцы](#ef-core-json)). |

Помимо миграций, провайдер также пока не поддерживает:

* **`UPDATE` / `DELETE`**
* **Транзакции**: `BeginTransaction` — no-op. ClickHouse не поддерживает ACID-транзакции.
* **Трансляцию запросов с JSON-путём**: `entity.Data["key"]` в LINQ не транслируется в SQL-синтаксис ClickHouse `data.key`. Фильтруйте по не-JSON-столбцам, а JSON анализируйте в памяти.

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

<h3 id="valuetuple-caveat">
  Tuple из 8+ элементов с вложенным кортежем на последней позиции
</h3>

Типы C# `ValueTuple`, содержащие более 7 элементов, используют схему вложенности, генерируемую компилятором: 8-й универсальный аргумент (`TRest`) сам является `ValueTuple`, содержащим оставшиеся элементы. Например, `(int, int, int, int, int, int, int, string, string)` компилируется в `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

Из-за этого возникает неоднозначность, когда столбец ClickHouse представляет собой 8-элементный кортеж, у которого последний элемент сам является кортежем — например, `Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String))`. Драйвер не может различить:

* **Плоский 9-элементный кортеж** (вложенность TRest, сгенерированная компилятором)
* **8-элементный кортеж**, у которого последний элемент — вложенный `Tuple(String, String)`

В обоих случаях получается один и тот же тип .NET: `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

Драйвер обрабатывает 8-й аргумент как TRest (то есть разворачивает его), а значит, случай с 8 элементами и вложенным кортежем будет сериализован некорректно.

Это затрагивает как `System.Tuple`, так и `ValueTuple`, поскольку оба используют вложенность TRest для случаев с более чем 7 элементами. Tuple с 7 или меньшим числом элементов, а также кортежи, у которых последний элемент сам по себе не является кортежем, этой проблеме не подвержены.

**Обходной путь:** оберните внутренний кортеж в дополнительный слой, чтобы драйвер мог отличить его от вложенности TRest:

```csharp theme={null}
// Instead of this (ambiguous — is it 8 elements or 9 flat?):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create("a", "b"))

// Do this (unambiguous — inner tuple is wrapped):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(Tuple.Create("a", "b")))
```

***

<h3 id="aggregatefunction-columns">
  Столбцы AggregateFunction
</h3>

Столбцы типа `AggregateFunction(...)` нельзя запрашивать или напрямую вставлять в них данные.

Чтобы выполнить вставку:

```sql theme={null}
INSERT INTO t VALUES (uniqState(1));
```

Чтобы выбрать:

```sql theme={null}
SELECT uniqMerge(c) FROM t;
```

***
