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

> O cliente oficial de C# para conexão com o ClickHouse via HTTP.

# Cliente HTTP para C# do ClickHouse

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>;
};

O cliente oficial em C# para se conectar ao ClickHouse.
O código-fonte do cliente está disponível no [repositório do GitHub](https://github.com/ClickHouse/clickhouse-cs).
Desenvolvido originalmente por [Oleg V. Kozlyuk](https://github.com/DarkWanderer).

A biblioteca oferece duas APIs principais:

* **`ClickHouseClient`** (recomendado): Cliente de alto nível, thread-safe, projetado para ser usado como singleton. Oferece uma API assíncrona simples para consultas e inserções em massa. É a melhor opção para a maioria das aplicações.

* **ADO.NET** (`ClickHouseDataSource`, `ClickHouseConnection`, `ClickHouseCommand`): Abstrações padrão do .NET para bancos de dados. Necessárias para integração com ORMs (Dapper, Linq2db) e para casos que exigem compatibilidade com ADO.NET. `ClickHouseBulkCopy` é uma classe auxiliar para inserir dados de forma eficiente por meio de uma conexão ADO.NET. `ClickHouseBulkCopy` está obsoleto e será removido em um lançamento futuro; use `ClickHouseClient.InsertBinaryAsync` em seu lugar.

As duas APIs compartilham o mesmo pool de conexões HTTP e podem ser usadas juntas na mesma aplicação.

<h2 id="migration-guide">
  Guia de migração
</h2>

1. Atualize o seu arquivo `.csproj` com o novo nome do pacote, `ClickHouse.Driver`, e com [a versão mais recente disponível no NuGet](https://www.nuget.org/packages/ClickHouse.Driver).
2. Substitua todas as referências a `ClickHouse.Client` por `ClickHouse.Driver` na sua base de código.

***

<h2 id="supported-net-versions">
  Versões do .NET compatíveis
</h2>

`ClickHouse.Driver` é compatível com as seguintes versões do .NET:

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

<h2 id="supported-clickhouse-versions">
  Versões do ClickHouse com suporte
</h2>

O cliente oferece suporte oficial aos três lançamentos mais recentes, além dos dois lançamentos LTS mais recentes.

<h2 id="installation">
  Instalação
</h2>

Instale o pacote pelo NuGet:

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

Ou usando o Gerenciador de Pacotes NuGet:

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

<h2 id="quick-start">
  Início rápido
</h2>

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

// Create a client (typically as a singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

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

<h2 id="configuration">
  Configuração
</h2>

Você pode configurar a conexão com o ClickHouse de duas maneiras:

* **String de conexão:** Pares de chave e valor separados por ponto e vírgula que especificam o host, as credenciais de autenticação e outras opções de conexão.
* **Objeto `ClickHouseClientSettings`:** Objeto de configuração fortemente tipado que pode ser carregado de arquivos de configuração ou definido no código.

A seguir, você encontra a lista completa das configurações, seus valores default e seus efeitos.

<h3 id="connection-settings">
  Configurações de conexão
</h3>

| Propriedade | Tipo | Padrão | Chave da string de conexão | Descrição |
| - | - | - | - | - |
| Host | `string` | `"localhost"` | `Host` | Nome do host ou endereço IP do servidor ClickHouse |
| Porta | `ushort` | 8123 (HTTP) / 8443 (HTTPS) | `Port` | Número da porta; o valor padrão depende do protocolo |
| Nome de usuário | `string` | `"default"` | `Username` | Nome de usuário para autenticação |
| Senha | `string` | `""` | `Password` | Senha para autenticação |
| Banco de dados | `string` | `""` | `Database` | Banco de dados padrão; quando vazio, usa o padrão do servidor ou do usuário |
| Protocolo | `string` | `"http"` | `Protocol` | Protocolo de conexão: `"http"` ou `"https"` |
| Caminho | `string` | `null` | `Path` | Caminho da URL em cenários com proxy reverso (por exemplo, `/clickhouse`) |
| Tempo limite | `TimeSpan` | 2 minutos | `Timeout` | Tempo limite da operação (armazenado em segundos na string de conexão) |

<h3 id="data-format-serialization">
  Formato de dados e serialização
</h3>

| Propriedade | Tipo | Padrão | Chave da string de conexão | Descrição |
| - | - | - | - | - |
| UseCompression | `bool` | `true` | `Compression` | Controla a compressão na transmissão em ambos os sentidos para consultas comuns: solicita que o servidor comprima a resposta (`enable_http_compression`; consulte `AcceptEncoding` para o codec, que pode ser solicitado por um valor explícito mesmo com esta opção desativada) **e** comprime o corpo da requisição com gzip — exceto quando `UseFormDataParameters` é usado, pois o corpo multipart é sempre enviado sem compressão. Inserts binários não usam esta opção; usam `InsertOptions.Compressor` — consulte [Compressão na inserção](#insert-compression) |
| AcceptEncoding | `string` | `null` | `AcceptEncoding` | Cabeçalho `Accept-Encoding` enviado em cada requisição, substituindo os codecs que o driver anuncia por padrão (`zstd, lz4, gzip, deflate`). A resposta do servidor é descomprimida automaticamente. Consulte [Descompressão da resposta](#response-decompression) |
| UseCustomDecimals | `bool` | `true` | `UseCustomDecimals` | Usa `ClickHouseDecimal` para precisão arbitrária; se `false`, usa `decimal` do .NET (limite de 128 bits) |
| ReadStringsAsByteArrays | `bool` | `false` | `ReadStringsAsByteArrays` | Lê colunas `String` e `FixedString` como `byte[]` em vez de `string`; útil para dados binários |
| UseFormDataParameters | `bool` | `false` | `UseFormDataParameters` | Envia parâmetros como dados de formulário em vez de usar a string de consulta da URL |
| ReadBufferSize | `int` | `65536` (64 KiB) | `ReadBufferSize` | Tamanho, em bytes, do buffer usado para ler as respostas de consultas HTTP. O driver obtém o buffer de um pool compartilhado e o devolve quando descarta o leitor, evitando uma alocação a cada consulta. Aumente esse valor para reduzir as recargas do buffer em conjuntos de resultados grandes. O driver mantém um buffer por leitor ativo ao mesmo tempo, de modo que o uso de memória aumenta conforme o tamanho do buffer e o número desses leitores. Consulte [Buffers](#perf-buffers). |
| ParameterTypeResolver | `IParameterTypeResolver` | `null` | — | Resolvedor personalizado para mapear tipos de parâmetros no estilo `@`; consulte [Mapeamento personalizado de tipos de parâmetros](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | `null` | — | Formatador personalizado para serializar valores de parâmetros; consulte [Formatação personalizada de valores de parâmetros](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | `null` | — | Transformação personalizada aplicada aos valores retornados pelo leitor de dados; consulte [Conversão personalizada de valores lidos](#read-value-conversion) |
| JsonReadMode | `JsonReadMode` | `Binary` | `JsonReadMode` | Define como os dados JSON são retornados: `Binary` (retorna `JsonObject`) ou `String` (retorna a string JSON bruta) |
| JsonWriteMode | `JsonWriteMode` | `String` | `JsonWriteMode` | Define como os dados JSON são enviados: `String` (serializa por meio de `JsonSerializer` e aceita qualquer entrada) ou `Binary` (aceita apenas POCOs registrados com indicações de tipo) |
| MapReadMode | `MapReadMode` | `Dictionary` | `MapReadMode` | Define como os dados `Map(K, V)` são retornados: `Dictionary` (retorna `Dictionary<K, V>`; se uma chave se repetir, mantém apenas seu último valor) ou `KeyValuePairs` (retorna `List<KeyValuePair<K, V>>`, preservando todos os pares). Consulte [Tipo Map](#type-map-reading-map) |
| AllowDuplicateJsonKeys | `bool` | `false` | `AllowDuplicateJsonKeys` | Define como ler uma linha `JSON` em que caminhos sobrepostos contêm valores. `false` gera um erro, pois manter qualquer um dos valores implica descartar o outro; `true` mantém o valor que aparece por último na linha. Consulte [Caminhos sobrepostos](#type-map-reading-json) |

<h3 id="session-management">
  Gerenciamento de sessões
</h3>

| Propriedade | Tipo | Padrão | Chave da string de conexão | Descrição |
| - | - | - | - | - |
| UseSession | `bool` | `false` | `UseSession` | Habilita sessões com estado; processa as requisições em sequência |
| SessionId | `string` | `null` | `SessionId` | ID da sessão; gera automaticamente um GUID se for null e UseSession for true |

<Note>
  A opção `UseSession` mantém a sessão no servidor, permitindo o uso de instruções `SET` e tabelas temporárias. As sessões são redefinidas após 60 segundos de inatividade (tempo limite padrão). É possível prolongar o ciclo de vida da sessão ajustando as configurações de sessão por meio de instruções do ClickHouse ou da configuração do servidor.

  A classe `ClickHouseConnection` normalmente permite a execução paralela (várias threads podem executar consultas ao mesmo tempo). No entanto, habilitar a opção `UseSession` limita a execução a uma consulta ativa por conexão de cada vez (essa é uma limitação do lado do servidor).
</Note>

<h3 id="security">
  Segurança
</h3>

| Propriedade | Tipo | Valor padrão | Chave da string de conexão | Descrição |
| - | - | - | - | - |
| SkipServerCertificateValidation | `bool` | `false` | — | Ignora a validação do certificado HTTPS; **não use em produção** |

<h3 id="http-client-configuration">
  Configuração do cliente HTTP
</h3>

| Propriedade | Tipo | Padrão | Chave da string de conexão | Descrição |
| - | - | - | - | - |
| HttpClient | `HttpClient` | `null` | — | Instância personalizada e pré-configurada de HttpClient |
| HttpClientFactory | `IHttpClientFactory` | `null` | — | Fábrica personalizada para criar instâncias de HttpClient |
| HttpClientName | `string` | `null` | — | Nome usado pela HttpClientFactory para criar um cliente específico |

<h3 id="logging-debugging">
  Logging e depuração
</h3>

| Propriedade | Tipo | Valor padrão | Chave da string de conexão | Descrição |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | Fábrica de loggers para logging de diagnóstico |
| EnableDebugMode | `bool` | `false` | — | Habilita o rastreamento de rede do .NET (requer LoggerFactory configurada com o nível Trace); **impacto significativo no desempenho** |

<h3 id="custom-settings-roles">
  Configurações personalizadas e funções
</h3>

| Propriedade | Tipo | Padrão | Chave da string de conexão | Descrição |
| - | - | - | - | - |
| CustomSettings | `IDictionary<string, object>` | Vazio | Prefixo `set_*` | Configurações do servidor ClickHouse; consulte a nota abaixo |
| Roles | `IReadOnlyList<string>` | Vazio | `Roles` | Funções do ClickHouse separadas por vírgulas (por exemplo, `Roles=admin,reader`) |
| ApplicationInfo | `IReadOnlyDictionary<string, string>` | Vazio | — | Tags de formato livre adicionadas ao cabeçalho HTTP `User-Agent` para atribuir consultas a cada aplicação. |

<Note>
  Ao definir configurações personalizadas por meio de uma string de conexão, use o prefixo `set_`, por exemplo, "set\_max\_threads=4". Ao usar um objeto ClickHouseClientSettings, não use o prefixo `set_`.

  Para ver a lista completa de configurações disponíveis, consulte [esta página](/pt-BR/reference/settings/session-settings).
</Note>

***

<h3 id="connection-string-examples">
  Exemplos de strings de conexão
</h3>

<h4 id="basic-connection">
  Conexão básica
</h4>

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

<h4 id="with-custom-clickhouse-settings">
  Com configurações personalizadas do 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` permite substituir as configurações definidas para o cliente em cada consulta. Todas as propriedades são opcionais e, quando especificadas, substituem os valores padrão do cliente.

| Propriedade | Tipo | Descrição |
| - | - | - |
| QueryId | `string` | Identificador personalizado da consulta para rastreá-la em `system.query_log` ou cancelá-la |
| Database | `string` | Substitui o banco de dados padrão nesta consulta |
| Roles | `IReadOnlyList<string>` | Substitui as funções configuradas para o cliente nesta consulta |
| CustomSettings | `IDictionary<string, object>` | Configurações do servidor ClickHouse para esta consulta (por exemplo, `max_threads`) |
| CustomHeaders | `IDictionary<string, string>` | Cabeçalhos HTTP adicionais para esta consulta |
| UseSession | `bool?` | Substitui o comportamento da sessão nesta consulta |
| SessionId | `string` | ID da sessão para esta consulta (requer `UseSession = true`) |
| BearerToken | `string` | Substitui o token de autenticação nesta consulta |
| ParameterTypeResolver | `IParameterTypeResolver` | Substitui o resolvedor configurado para o cliente no mapeamento de tipos de parâmetros no estilo `@`; consulte [Mapeamento personalizado de tipos de parâmetros](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | Substitui o formatador configurado para o cliente na serialização de valores de parâmetros no estilo `@`; consulte [Formatação personalizada de valores de parâmetros](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | Substitui a transformação configurada para o cliente e aplicada aos valores retornados pelo leitor de dados; consulte [Conversão personalizada de valores lidos](#read-value-conversion) |
| MaxExecutionTime | `TimeSpan?` | Tempo limite da consulta no lado do servidor (passado como configuração `max_execution_time`); o servidor cancela a consulta se esse tempo for excedido |
| AcceptEncoding | `string` | Substitui `Accept-Encoding` nesta consulta (por exemplo, `"br"`, `"identity"`), prevalecendo sobre `ClickHouseClientSettings.AcceptEncoding`; também força `enable_http_compression=1` na URL. Consulte [Compressão de transporte por consulta](#per-query-accept-encoding). |

**Exemplo:**

```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` estende `QueryOptions` com configurações específicas para operações de inserção em massa por meio de `InsertBinaryAsync`.

| Propriedade | Tipo | Padrão | Descrição |
| - | - | - | - |
| BatchSize | `int` | 100,000 | Número de linhas por lote |
| MaxDegreeOfParallelism | `int` | 1 | Número de lotes enviados em paralelo |
| Format | `RowBinaryFormat` | `RowBinary` | Formato binário: `RowBinary` ou `RowBinaryWithDefaults` |
| Compressor | `IClickHouseCompressor` | `ZstdCompressor.Default` | Codec aplicado ao corpo da requisição de inserção (`Content-Encoding`). Com `null`, o corpo é enviado sem compressão. Consulte [Compressão na inserção](#insert-compression) |
| QueryPlacement | `InsertQueryPlacement` | `Body` | Local de envio da instrução `INSERT INTO ... FORMAT ...`: `Body` (antes das linhas) ou `Url` (como parâmetro de URL `query`). Consulte [Posicionamento da consulta de inserção](#insert-query-placement) |
| ColumnTypes | `IReadOnlyDictionary<string, string>` | `null` | Nome da coluna → string do tipo do ClickHouse. Quando definido, dispensa a consulta de verificação do schema. |
| UseSchemaCache | `bool` | `false` | Armazena em cache o schema completo da tabela por (banco de dados, tabela) durante o ciclo de vida do cliente. |

Todas as propriedades de `QueryOptions` também estão disponíveis em `InsertOptions`.

**Exemplo:**

```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">
  Como evitar a consulta de verificação do schema
</h4>

Por padrão, `InsertBinaryAsync` envia uma consulta `SELECT ... WHERE 1=0` antes de cada inserção para identificar os tipos de coluna. Em cenários de alta vazão, você pode eliminar essa sobrecarga de duas maneiras:

**Opção 1: Informar os tipos de coluna explicitamente**

Se você conhece o schema da tabela no momento da compilação, informe os tipos de coluna diretamente por meio de `ColumnTypes`. Assim, nenhuma consulta para verificar o schema é enviada:

```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);
```

**Opção 2: Armazenar o schema em cache**

Se você insere dados repetidamente na mesma tabela, defina `UseSchemaCache = true` para consultar o schema apenas uma vez e reutilizá-lo nas próximas operações de insert na mesma instância de `ClickHouseClient`:

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

// First call fetches schema from the server
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// Second call reuses cached schema — no extra round-trip
await client.InsertBinaryAsync("my_table", columns, batch2, options);
```

<Note>
  * `ColumnTypes` tem prioridade sobre `UseSchemaCache`. Se ambos estiverem definidos, os tipos explícitos serão usados.
  * O cache de schema não detecta alterações feitas por `ALTER TABLE`. Se você alterar o schema da tabela, crie um novo `ClickHouseClient` ou não use `UseSchemaCache` para essa tabela.
  * O cache é específico de cada instância de `ClickHouseClient` e indexado por (database, table). Diferentes subconjuntos de colunas da mesma tabela compartilham o mesmo schema em cache.
</Note>

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

`ClickHouseClient` é a API recomendada para interagir com o ClickHouse. É thread-safe, foi projetada para uso como singleton e gerencia internamente o pool de conexões HTTP.

<h3 id="creating-a-client">
  Criando um cliente
</h3>

Crie um `ClickHouseClient` com uma string de conexão ou um objeto `ClickHouseClientSettings`. Consulte a seção [Configuração](#configuration) para conhecer as opções disponíveis.

Os dados de conexão com seu serviço do ClickHouse Cloud estão disponíveis no console do ClickHouse Cloud.

Selecione um serviço e clique em **Connect**:

<Image img="https://mintcdn.com/private-7c7dfe99-vortex-format/tee0vtC1xMN3cEIV/images/_snippets/cloud-connect-button.webp?fit=max&auto=format&n=tee0vtC1xMN3cEIV&q=85&s=334245222c7a16f54a807110b260b71e" size="md" alt="Botão Connect do serviço ClickHouse Cloud" border width="998" height="932" data-path="images/_snippets/cloud-connect-button.webp" />

Escolha **C#**. Os dados de conexão serão exibidos abaixo.

<Image img="https://mintcdn.com/private-7c7dfe99-vortex-format/tee0vtC1xMN3cEIV/images/_snippets/connection-details-csharp.webp?fit=max&auto=format&n=tee0vtC1xMN3cEIV&q=85&s=60f04e40db829dd87b2407094cf1765d" size="md" alt="Dados de conexão do ClickHouse Cloud para C#" border width="851" height="805" data-path="images/_snippets/connection-details-csharp.webp" />

Se você usa o ClickHouse autogerenciado, os dados de conexão são definidos pelo administrador do ClickHouse.

Usando uma string de conexão:

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

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

Ou com `ClickHouseClientSettings`:

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

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

Em cenários de injeção de dependência, use `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` foi projetado para ser mantido por longos períodos e compartilhado em toda a aplicação. Crie uma única instância (geralmente como um singleton) e reutilize-a em todas as operações de banco de dados. O cliente gerencia internamente o pool de conexões HTTP.
</Note>

***

<h3 id="executing-queries">
  Executando consultas
</h3>

Use `ExecuteNonQueryAsync` para executar instruções que não retornam resultados:

```csharp theme={null}
// Create a table
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// Drop a table
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");
```

Use `ExecuteScalarAsync` para obter um único valor:

```csharp theme={null}
var count = await client.ExecuteScalarAsync("SELECT count() FROM default.my_table");
Console.WriteLine($"Row count: {count}");

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Server version: {version}");
```

***

<h3 id="inserting-data">
  Inserindo dados
</h3>

<h4 id="parameterized-inserts">
  Inserções parametrizadas
</h4>

Insira dados por meio de consultas parametrizadas com `ExecuteNonQueryAsync`. Os tipos dos parâmetros devem ser especificados no SQL com a sintaxe `{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">
  Inserções em massa
</h4>

Use `InsertBinaryAsync` para inserir grandes quantidades de linhas com eficiência. O método transmite os dados em fluxo no formato binário nativo de linhas do ClickHouse, permite o envio paralelo de lotes e evita erros de "URL muito longa" que podem ocorrer com consultas parametrizadas.

```csharp theme={null}
// Prepare data as IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

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

// Basic insert
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");
```

Para grandes volumes de dados, configure o agrupamento em lote e o paralelismo usando `InsertOptions`:

```csharp theme={null}
var options = new InsertOptions
{
    BatchSize = 100_000,           // Rows per batch (default: 100,000)
    MaxDegreeOfParallelism = 4     // Parallel batch uploads (default: 1)
};
```

<Note>
  * Antes de inserir dados, o cliente consulta automaticamente a estrutura da tabela por meio de `SELECT * FROM <table> WHERE 1=0`. Os valores fornecidos devem corresponder aos tipos das colunas de destino. Para evitar essa consulta, use [`InsertOptions.ColumnTypes` ou `InsertOptions.UseSchemaCache`](#skip-schema-query).
  * Quando `MaxDegreeOfParallelism > 1`, os lotes são enviados em paralelo. As sessões não são compatíveis com inserções em paralelo; desative-as ou defina `MaxDegreeOfParallelism = 1`.
  * Use `RowBinaryFormat.RowBinaryWithDefaults` em `InsertOptions.Format` se quiser que o servidor aplique os valores DEFAULT às colunas não fornecidas.
</Note>

<h4 id="poco-insert">
  Inserção com POCO
</h4>

Em vez de criar arrays `object[]`, você pode inserir objetos POCO fortemente tipados diretamente. Registre o tipo uma única vez e depois passe `IEnumerable<T>`:

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

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

// Insert directly — column names are derived from property names
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);
```

Por padrão, todas as propriedades públicas que podem ser lidas são mapeadas para colunas por meio da correspondência exata de nomes para diferenciar maiúsculas de minúsculas. Você pode personalizar esse mapeamento usando atributos:

```csharp theme={null}
public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // Map to a differently-named column
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // Explicit ClickHouse type
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Exclude from insert
    public string InternalTag { get; set; }
}
```

| Atributo | Finalidade |
| - | - |
| `[ClickHouseColumn(Name = "...")]` | Substituir o nome da coluna de destino |
| `[ClickHouseColumn(Type = "...")]` | Declarar explicitamente o tipo do ClickHouse |
| `[ClickHouseNotMapped]` | Excluir a propriedade da inserção |

Quando **todas** as propriedades mapeadas especificam um `Type` explícito, a consulta de verificação do schema é dispensada. Quando apenas algumas propriedades têm tipos explícitos, o driver recorre à consulta de verificação do schema para o conjunto completo de colunas.

`InsertBinaryAsync<T>` aceita as mesmas opções de `InsertOptions` (agrupamento em lote, paralelismo e cache do schema) que a sobrecarga de `object[]`.

<Note>
  Diferentemente da sobrecarga de `object[]`, `InsertBinaryAsync<T>` não aceita uma lista explícita de colunas. As colunas são determinadas pelas propriedades mapeadas do tipo registrado. Para controlar quais colunas serão inseridas, use `[ClickHouseNotMapped]` para excluir propriedades ou `[ClickHouseColumn(Name = "...")]` para renomeá-las.

  Se `ColumnTypes` estiver definido em `InsertOptions`, seus valores terão precedência sobre os atributos do POCO.
</Note>

<h4 id="poco-insert-schema-evolution">
  Evolução do schema
</h4>

As inserções com POCO continuam funcionando normalmente quando novas colunas são adicionadas à tabela de destino após o registro do tipo. Como o driver insere apenas as colunas mapeadas pelo POCO, o servidor preenche automaticamente as novas colunas com `DEFAULT` (ou outras expressões de valor padrão). Não é preciso alterar o código nem registrar o tipo novamente.

<h4 id="insert-query-placement">
  Posicionamento da consulta de inserção
</h4>

Em um insert binário, a instrução `INSERT INTO ... FORMAT ...` é gravada na primeira linha do corpo da requisição, antes das linhas de dados. Como o corpo é comprimido por padrão, os mecanismos de roteamento e logging que inspecionam apenas a URL não conseguem ver a instrução. Defina `InsertOptions.QueryPlacement` como `InsertQueryPlacement.Url` para enviar a instrução como parâmetro `query` da URL, deixando o corpo reservado às linhas de dados:

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

Use essa opção quando um proxy, balanceador de carga ou gateway encaminha ou inspeciona requisições com base no parâmetro `query`, ou quando você quer que a instrução apareça nos logs de acesso e nas ferramentas de observabilidade. Essa opção precisa ser ativada explicitamente porque a instrução passa a contar para o limite de comprimento da URL. O limite efetivo é o menor dos limites impostos pelo runtime do .NET, por um intermediário ou pelo servidor. Do .NET 6 ao .NET 9, `System.Uri` limita a URI completa e codificada da requisição a 65,519 caracteres; se esse limite for excedido, o driver lança uma `InvalidOperationException` que orienta você a usar `InsertQueryPlacement.Body`. Por padrão, o `http_max_uri_size` do ClickHouse é de 1 MiB, mas um intermediário pode impor um limite menor. No modo em que a instrução é enviada no corpo, ela e as linhas não estão sujeitas a esse limite de comprimento da URL; outras opções da requisição ainda podem aparecer na URL.

Essa configuração é independente de `Compressor`: o corpo é codificado da mesma forma em ambos os modos.

***

<h3 id="reading-data">
  Leitura de dados
</h3>

Use `ExecuteReaderAsync` para executar consultas SELECT. O `ClickHouseDataReader` retornado permite acessar as colunas do resultado com tipos definidos, por meio de métodos como `GetInt64()`, `GetString()` e `GetFieldValue<T>()`.

Chame `Read()` para avançar para a próxima linha. O método retorna `false` quando não há mais linhas. Acesse as colunas pelo índice (começando em 0) ou pelo nome.

```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">
  Leitura de POCOs
</h4>

Em vez de ler as colunas pelo índice ou pelo nome, você pode ler os resultados da consulta em fluxo diretamente nas suas classes. Registre o tipo no cliente uma única vez e, depois, use `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">
  Registro
</h5>

`RegisterPocoType<T>()` configura os mapeamentos de inserção e leitura e valida os dois de antemão. `RegisterBinaryInsertType<T>()` permanece inalterado e continua restrito à inserção para manter a compatibilidade com versões anteriores.

Um tipo registrado deve ter:

* Um construtor público sem parâmetros.
* Pelo menos uma propriedade pública com um setter público que não seja `init`. Propriedades `required` são aceitas.

<h5 id="poco-read-column-matching">
  Correspondência de colunas
</h5>

A correspondência de colunas diferencia maiúsculas de minúsculas. Se uma coluna estiver ausente no resultado, a propriedade correspondente mantém seu valor default; colunas adicionais no resultado são ignoradas.

O driver não converte valores para tipos mais amplos ou mais restritos. Exceto pelas representações alternativas listadas abaixo,
o tipo da coluna no framework deve ser atribuível ao tipo da propriedade; caso contrário, é lançada uma
`InvalidOperationException`. Portanto, uma propriedade do tipo `object` aceita qualquer coluna.

<h5 id="poco-read-types">
  Tipos de propriedade compatíveis
</h5>

`QueryAsync<T>` lê cada uma dessas colunas diretamente para uma propriedade do tipo correspondente:

| Coluna do ClickHouse | Tipo(s) de propriedade |
| - | - |
| `Int8`/`Int16`/`Int32`/`Int64` | `sbyte`/`short`/`int`/`long` |
| `UInt8`/`UInt16`/`UInt32`/`UInt64` | `byte`/`ushort`/`uint`/`ulong` |
| `Int128`/`UInt128` | `BigInteger` ou `System.Int128`/`System.UInt128` nativos no .NET 8 e versões posteriores |
| `Int256`/`UInt256` | `BigInteger` |
| `Float32`/`Float64`/`BFloat16` | `float`/`double`/`float` |
| `Bool` | `bool` |
| `Decimal` | `decimal` ou `ClickHouseDecimal` |
| `Date`/`Date32`/`DateTime`/`DateTime64` | `DateTime`, `DateTimeOffset` ou `DateOnly` |
| `Time`/`Time64` | `TimeSpan` |
| `UUID` | `Guid` |
| `IPv4`/`IPv6` | `IPAddress` |
| `Enum8`/`Enum16` | `string` (o rótulo) ou `int` (o valor ordinal na representação wire) |
| `String`/`FixedString` | `string` ou `byte[]` |

Cada linha também aceita a versão anulável do tipo de propriedade (`long?`, `DateOnly?` e assim por diante),
independentemente de a coluna ser `Nullable(...)`. Uma propriedade de tipo valor não anulável associada a uma coluna
`Nullable(T)` é aceita no registro, mas lança uma exceção quando recebe NULL.

Wrappers como `LowCardinality(T)`, `SimpleAggregateFunction(f, T)` e `Object(T)` são mapeados exatamente como `T`.

Colunas compostas também são compatíveis e usam o tipo do framework indicado na
[referência de tipos para leitura](#clickhouse-native-type-map-reading): `Array(T)` para `T[]`, `Tuple(...)`
para `System.Tuple<...>`, `Nested(...)` para `Tuple<...>[]`, `JSON` para `JsonObject` (ou `string`
com [`JsonReadMode=String`](#type-map-reading-json)) e `Variant`/`Dynamic` para `object`.

Uma coluna `Map(K, V)` é um caso especial: uma propriedade `List<KeyValuePair<K, V>>` ou `KeyValuePair<K, V>[]`
é lida sem boxing e preserva a ordem de transmissão e as chaves repetidas, em qualquer um dos modos
[`MapReadMode`](#type-map-reading-map). Uma propriedade `Dictionary<K, V>` funciona apenas no modo
padrão. Os tipos da chave e do valor devem corresponder exatamente; portanto,
`Map(String, Nullable(Int32))` exige `KeyValuePair<string, int?>`.

Quando uma coluna aceita mais de um tipo de propriedade (uma coluna `DateTime` como `DateTime`,
`DateTimeOffset` ou `DateOnly`, uma coluna `String` como `string` ou `byte[]`), o tipo declarado da propriedade determina a representação. Essas representações alternativas
estão disponíveis na leitura de POCOs: `QueryAsync<T>` oferece suporte a elas, mas `MapTo<T>` não.

<h5 id="poco-read-mapto">
  Materialização de uma única linha
</h5>

Ao percorrer manualmente um leitor, use `ClickHouseDataReader.MapTo<T>()` para materializar a linha atual como um POCO registrado sem avançar o leitor:

```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}");
}
```

Use `MapTo<T>` quando precisar controlar o loop de leitura — por exemplo, para combinar o acesso bruto às colunas
com a materialização de POCOs. Como ele lê a linha por meio dos valores com boxing do leitor, não oferece
os tipos alternativos de propriedade mencionados acima e faz mais alocações que `QueryAsync<T>`. Prefira
`QueryAsync<T>` quando precisar apenas das linhas; consulte
[como escolher o caminho de materialização](#perf-read-path) para comparar os números.

<h5 id="poco-read-converters">
  Conversores de valores de leitura
</h5>

Um [conversor de valores de leitura](#read-value-conversion) configurado no nível do cliente ou por consulta se aplica a ambos os caminhos e
não desativa a leitura sem boxing. O driver converte cada coluna usando a sobrecarga correspondente à forma como
ela foi lida: `ConvertValue<T>`, tipada, para uma
coluna sem boxing, e `ConvertValue`, com boxing, para uma coluna composta. Implemente as duas sobrecargas
de forma consistente; caso contrário, a mesma coluna produzirá resultados diferentes dependendo do caminho usado.

<h5 id="poco-read-diagnostics">
  Diagnósticos de registro
</h5>

Quando há um `LoggerFactory` configurado, `RegisterPocoType<T>()` e `RegisterBinaryInsertType<T>()` geram um log de nível `Debug` (categoria `ClickHouse.Driver.Client`) que indica quais propriedades foram mapeadas para cada coluna e quais foram ignoradas, com os respectivos motivos. Consulte [Logging e diagnósticos](#logging-and-diagnostics).

***

<h3 id="sql-parameters">
  Parâmetros SQL
</h3>

No ClickHouse, os parâmetros em consultas SQL seguem o formato padrão `{parameter_name:DataType}`.

**Exemplos:**

```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>
  Os parâmetros de vinculação ('bind') do SQL são enviados como parâmetros de consulta na URI HTTP. Por isso, usar muitos deles pode causar uma exceção de "URL muito longa". Use `InsertBinaryAsync` para inserir grandes volumes de dados e evitar essa limitação.
</Note>

<h4 id="at-style-placeholders">
  Marcadores `@name` no estilo ADO
</h4>

O driver também aceita marcadores `@name`, usados por ORMs como o Dapper. Eles são um
recurso do lado do cliente: antes do envio da requisição, cada um é substituído por
`{name:ResolvedType}`, de modo que o servidor nunca recebe um `@`. Consulte
[resolução de tipos](#parameter-type-mapping) para saber como o tipo é determinado. Sempre que possível, use a forma explícita
`{name:Type}`.

Um `@name` sem parâmetro correspondente é mantido como está, para que o servidor o rejeite. A correspondência
diferencia maiúsculas de minúsculas; portanto, `@ID` não corresponde a um parâmetro chamado `id`.

<Note>
  Para desativar essa substituição, configure a opção AppContext `ClickHouse.Driver.DisableReplacingParameters`
  antes de usar o driver pela primeira vez. Somente a substituição no texto é desativada; os parâmetros continuam sendo enviados, portanto
  as consultas escritas com a sintaxe nativa `{name:Type}` continuam funcionando.
</Note>

<h4 id="identifier-parameters">
  Parâmetros de identificador
</h4>

O tipo de parâmetro `Identifier` permite passar com segurança o nome de um banco de dados, tabela ou coluna, em vez de um literal de string entre aspas. Use-o com a sintaxe `{name:Identifier}` em SQL ou defina `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);
```

O valor é enviado exatamente como foi fornecido, e o servidor o insere como um identificador SQL sem delimitação, fazendo por conta própria a delimitação com backticks e o escape de caracteres. Identificadores com caracteres especiais (inclusive backticks) são preservados com segurança na ida e volta.

***

<h3 id="query-id">
  ID da consulta
</h3>

Cada consulta recebe um `query_id` único, que pode ser usado para buscar dados na tabela `system.query_log` ou cancelar consultas de longa duração. Você pode definir um ID de consulta personalizado usando `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>
  Se você especificar um `QueryId` personalizado, use um valor único para cada chamada. Um GUID aleatório é uma boa opção.
</Tip>

***

<h3 id="parameter-type-mapping">
  Mapeamento personalizado de tipos de parâmetros
</h3>

Ao usar parâmetros no estilo `@` (por exemplo, `WHERE id = @id`), o driver infere automaticamente o tipo do ClickHouse a partir do tipo do valor .NET. Por exemplo, `int` é mapeado para `Int32`.

<Warning>
  **Comportamento dos parâmetros DateTime inferidos**

  Para parâmetros no estilo `@` sem uma indicação `{name:Type}` no SQL e sem `ClickHouseType` definido, valores que representam instantes são inferidos como `DateTime('UTC')`, e não como um `DateTime` sem fuso horário. Valores `DateTime` com `Kind` igual a `Utc` ou `Local`, assim como todos os valores `DateTimeOffset`, são enviados como `DateTime('UTC')`, preservando o instante independentemente do fuso horário do servidor.

  Indicações explícitas (`{name:DateTime}`) têm precedência sobre a inferência e são a forma recomendada de construir consultas.
</Warning>

Para alterar esse comportamento padrão, defina `ParameterTypeResolver` em `ClickHouseClientSettings`. Isso é útil quando você quer que todos os parâmetros `DateTime` usem `DateTime64(3)` para obter precisão de milissegundos, ou que todos os valores decimais usem uma escala específica, sem precisar definir `ClickHouseType` em cada parâmetro.

**Usando `DictionaryParameterTypeResolver` para mapeamentos simples de tipos:**

```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` personalizado para cenários avançados:**

Para uma resolução que leve em conta o valor ou se baseie no nome, implemente diretamente a interface `IParameterTypeResolver`. Retorne `null` para que a inferência padrão seja usada:

```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})";
    }
}
```

Você também pode definir um resolvedor para uma única consulta por meio de `QueryOptions.ParameterTypeResolver`. Nesse caso, ele tem precedência sobre o resolvedor configurado no nível do cliente.

**Ordem de precedência na resolução de tipos:**

O resolvedor faz parte de uma cadeia de precedência. Da maior para a menor prioridade:

1. `ClickHouseType` definido explicitamente no parâmetro
2. Anotação de tipo SQL usando a sintaxe `{name:Type}` na consulta
3. `IParameterTypeResolver` (de `QueryOptions.ParameterTypeResolver` ou, se não estiver definido, de `ClickHouseClientSettings.ParameterTypeResolver`)
4. Inferência de tipos integrada (`TypeConverter.ToClickHouseType`)

O resolvedor também funciona ao usar `ClickHouseConnection` do ADO.NET — as conexões criadas a partir do cliente herdam suas configurações.

***

<h3 id="parameter-value-formatting">
  Formatação personalizada de valores de parâmetros
</h3>

`IParameterFormatter` é um hook que determina como os valores dos parâmetros são serializados. Use-o quando a formatação integrada (por exemplo, a precisão de DateTime, as convenções de formatação de decimais, o escape de strings ou a representação de números) não atender ao que seu schema ou suas ferramentas posteriores esperam.

Defina `ParameterFormatter` em `ClickHouseClientSettings` para aplicar um formatador a todas as consultas parametrizadas. O formatador recebe o valor, o nome do tipo ClickHouse identificado e o nome do parâmetro, e retorna a representação em string enviada ao servidor. Retorne `null` para usar o formatador padrão.

**Como usar `DictionaryParameterFormatter` para formatação simples por tipo 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` personalizado para cenários avançados:**

```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
    }
}
```

Você também pode definir um formatador para cada consulta por meio de `QueryOptions.ParameterFormatter`. Quando definido, ele tem precedência sobre o formatador configurado no nível do cliente.

**Valores compostos:**

O formatador é aplicado tanto aos parâmetros de coleção de nível superior quanto a cada elemento de valores compostos (`Array`, `Tuple`, `Map`, `Nullable`, `LowCardinality`, `Variant`). Por exemplo, um mapeamento de `typeof(int)` formata individualmente cada elemento `Int32` de um `Array(Int32)`.

**Uso de aspas simples em contextos compostos:**

Para tipos do ClickHouse semelhantes a strings (`String`, `FixedString`, `Enum8`, `Enum16`, `IPv4`, `IPv6`, `UUID`) embutidos em um literal composto, o driver coloca a saída do formatador entre aspas simples, mas não aplica escape ao conteúdo. Se a string retornada contiver uma aspa simples ou uma barra invertida sem escape, o literal composto ficará malformado e o servidor rejeitará a consulta.

Parâmetros de string de nível superior (não embutidos em um valor composto) são usados como estão, sem aspas adicionais; portanto, não é necessário aplicar escape nesse caso.

**Precedência dos formatadores:**

1. `IParameterFormatter` (de `QueryOptions.ParameterFormatter` ou, se não estiver definido, de `ClickHouseClientSettings.ParameterFormatter`). Se retornar um valor diferente de `null`, esse valor será usado.
2. Formatação integrada específica para cada tipo em `HttpParameterFormatter`.

O formatador não é chamado para valores `null` ou `DBNull`; eles sempre são serializados como o marcador de valor nulo do ClickHouse (`\N`).

***

<h3 id="read-value-conversion">
  Conversão personalizada de valores na leitura
</h3>

`IReadValueConverter` permite transformar os valores retornados pelo leitor de dados após a desserialização, sem alterar seu tipo CLR. Entre os usos comuns estão definir `DateTime.Kind = Utc` em uma coluna `DateTime` sem fuso horário, remover caracteres das extremidades de strings ou normalizá-las e processar uma coluna JSON antes que seus valores cheguem ao código da aplicação.

Defina `ReadValueConverter` em `ClickHouseClientSettings` para aplicar um conversor a todas as leituras. O conversor é chamado uma vez por coluna, para cada linha, tanto no caminho com boxing (`GetValue`) quanto no caminho genérico (`GetFieldValue<T>`). Se nenhum conversor estiver definido, não há sobrecarga — o leitor retorna os valores diretamente.

**Usando `DictionaryReadValueConverter` para conversões simples por tipo 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);
```

Os valores cujo tipo CLR em tempo de execução não foi registrado com `For<T>` são transmitidos sem alterações. O encaminhamento é feito com base no tipo CLR exato; portanto, registre o tipo que o leitor realmente produz (por exemplo, `For<JsonObject>` para uma coluna JSON no modo `JsonReadMode.Binary`).

**`IReadValueConverter` personalizado para cenários avançados:**

Se precisar fazer o encaminhamento com base na string do tipo do ClickHouse (por exemplo, para distinguir `DateTime` de `DateTime('UTC')` — ambos são representados pelo mesmo tipo CLR), implemente `IReadValueConverter` diretamente:

```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;
    }
}
```

O conversor deve preservar o tipo CLR em tempo de execução; os metadados da coluna (`GetFieldType`, `GetSchemaTable`) não são redirecionados pelo conversor e devem permanecer consistentes com o valor retornado.

Você também pode definir um conversor para cada consulta por meio de `QueryOptions.ReadValueConverter`; nesse caso, ele tem precedência sobre o conversor no nível do cliente.

**Limite do encaminhamento:**

O conversor é invocado uma vez por coluna e recebe o valor completo da célula desserializada; ele **não** percorre recursivamente estruturas compostas. Para uma coluna `Array(Int32)`, o valor recebido é um `int[]`; para `Tuple(Int32, String)`, é um `ITuple`.

**Qual sobrecarga é executada:**

As duas sobrecargas devem produzir resultados consistentes, pois a sobrecarga chamada pelo driver depende de como o código chamador leu a
coluna:

* `ConvertValue<T>` — os métodos de acesso tipados `GetByte`, `GetSByte`, `GetInt16`/`32`/`64`,
  `GetUInt16`/`32`/`64`, `GetFloat`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetIPAddress`,
  `GetBigInteger` e `GetFieldValue<T>`, além de todas as colunas sem encapsulamento em objeto no
  [caminho de leitura de POCO](#poco-read-converters).
* `ConvertValue` (com encapsulamento em objeto) — `GetValue`, `GetValues`, os indexadores, `GetChar`, `GetTuple` e os
  caminhos que fazem coerção de tipo em `GetBoolean`, `GetDecimal` e `GetString`.

`IsDBNull` não executa o conversor: ele lê diretamente o indicador de nulo, portanto um conversor nunca pode
alterar se um valor é considerado nulo. `TryGetEnumOrdinal` também não passa pelo conversor — consulte
[como ler o ordinal de um enum](#ado-net-reader-enum-ordinal).

O conversor funciona no caminho ADO.NET de `ClickHouseConnection` — as conexões criadas a partir do cliente herdam as configurações.

***

<h3 id="raw-streaming">
  Streaming de dados brutos
</h3>

Use `ExecuteRawResultAsync` para transmitir os resultados de uma consulta diretamente no formato desejado, sem passar pelo leitor de dados. Isso é útil para exportar dados para arquivos ou repassá-los a outros sistemas:

```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();
```

Formatos comuns: `JSONEachRow`, `CSV`, `TSV`, `Parquet`, `Native`. Consulte a [documentação sobre formatos](/pt-BR/reference/formats/index) para ver todas as opções.

***

<h3 id="per-query-accept-encoding">
  Compressão de transporte por consulta
</h3>

Por padrão, o cliente negocia `zstd, lz4, gzip, deflate` quando `Compression=true` (o padrão da string de conexão) e decodifica o fluxo de dados automaticamente.

Para exportações brutas (por exemplo, Parquet, Arrow, Native), pode ser útil negociar outro codec (por exemplo, `zstd` ou `lz4`) para economizar largura de banda à custa de mais CPU, sem alterar a configuração da conexão. `QueryOptions.AcceptEncoding` e `ClickHouseCommand.AcceptEncoding` definem o cabeçalho HTTP `Accept-Encoding` para uma única requisição, substituindo qualquer valor padrão já definido, e adicionam `enable_http_compression=1` à URL (exigido pelo ClickHouse para que ele respeite `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">
  Configuração do HttpClient
</h4>

Não é necessário configurar nada: o `HttpClient` criado pelo driver mantém `AutomaticDecompression` em `DecompressionMethods.None`, e o próprio driver decodifica as respostas. Assim, `Content-Encoding` nunca é removido sem que você perceba, e o corpo bruto chega até você exatamente como o servidor o enviou.

<Warning>
  Se você fornecer seu próprio `HttpClient`, também deixe `AutomaticDecompression` desativado. Essa configuração não afeta apenas as respostas: no momento do envio, o handler **adiciona ao `Accept-Encoding` de saída todos os algoritmos presentes em sua máscara que ainda não estejam no cabeçalho**. Assim, um handler com `GZip | Deflate` transforma um `AcceptEncoding = "lz4"` explícito em `lz4, gzip, deflate` e um `"identity"` explícito em `identity, gzip, deflate` na transmissão. Como o ClickHouse interpreta o cabeçalho de acordo com sua própria ordem fixa de preferência por codecs (ignorando a ordem e os valores de q), ele pode responder com um codec que você nunca solicitou. O handler então decodifica a resposta e remove o cabeçalho, sem que você sequer perceba o que aconteceu. Deixar a máscara desativada mantém a oferta exatamente como você a definiu.
</Warning>

<Warning>
  Se `AcceptEncoding` solicitar um codec que o driver não consegue decodificar (`snappy`), apenas `ExecuteRawResultAsync` é seguro. `ExecuteReaderAsync`, `ExecuteScalarAsync` e `ExecuteNonQueryAsync` falham com uma `NotSupportedException` que identifica o codec (antes, interpretavam os bytes comprimidos como o formato do resultado e produziam dados sem sentido).
</Warning>

<h4 id="per-query-accept-encoding-errors">
  Corpos de erro
</h4>

Quando o servidor responde com um código 4xx/5xx e `enable_http_compression=1` está definido, ele comprime o corpo do erro com o mesmo codec que usaria para uma resposta bem-sucedida. O driver decodifica esses corpos com todos os codecs compatíveis (`lz4`, `zstd`, `gzip`, `deflate`, `br`/`brotli`), para que a mensagem de `ClickHouseServerException` seja legível. Para os demais codecs (`snappy`, …), ele retorna um texto substituto que identifica o codec e indica `system.query_log` como o local onde encontrar o texto original do erro.

***

<h3 id="response-decompression">
  Descompressão da resposta
</h3>

`Accept-Encoding` apenas solicita que o servidor comprima a resposta — ela ainda precisa ser decodificada. O driver faz isso automaticamente com base no `Content-Encoding` da resposta. Assim, todas as APIs de leitura habituais (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, Dapper, EF Core, linq2db) funcionam com respostas comprimidas sem precisar de configuração adicional. O driver decodifica `lz4`, `zstd`, `gzip`, `deflate` e `br`; `snappy` não é compatível.

Por padrão, o driver informa que aceita **`zstd, lz4, gzip, deflate`**, e o ClickHouse responde usando `zstd`. Para escolher outro formato, defina `Accept-Encoding` manualmente — para todo o cliente:

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

por consulta, que tem precedência:

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

ou na string de conexão, para quem usa ORM e nunca precisa acessar `ClickHouseClientSettings`:

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

Definir esse valor também força `enable_http_compression=1` na URL, requisito para que o ClickHouse leve o cabeçalho em conta — inclusive quando `UseCompression` é `false`, pois especificar um codec é interpretado como um pedido de compressão. Quando nenhum valor é definido, `UseCompression=false` não envia o cabeçalho `Accept-Encoding`.

`Accept-Encoding` pode ser definido em quatro lugares. Prevalece o primeiro que especificar um codec:

1. `QueryOptions.AcceptEncoding` (ou `ClickHouseCommand.AcceptEncoding`)
2. `CustomHeaders["Accept-Encoding"]` na consulta
3. `CustomHeaders["Accept-Encoding"]` no cliente
4. `ClickHouseClientSettings.AcceptEncoding` ou a palavra-chave `AcceptEncoding` na string de conexão

Se nenhum deles especificar um codec, o driver envia sua lista padrão. Um valor que não especifica nenhum codec (nulo, vazio,
composto apenas por espaços em branco ou por vírgulas) é tratado como não definido, e o próximo lugar é verificado. Para desativar a compressão, use `identity`.

**É o servidor, não o cliente, que escolhe o codec.** O ClickHouse verifica os tokens de `Accept-Encoding` segundo sua própria ordem fixa de preferência — `zstd` > `br` > `lz4` > `snappy` > `gzip` > `deflate` — e ignora tanto a ordem em que você os lista quanto quaisquer valores q. Assim, o cabeçalho indica os codecs aceitos, sem impor a escolha de um deles; a única forma de influenciar essa escolha é omitir tokens. A lista padrão inclui `zstd`, por isso uma consulta com a configuração padrão recebe uma resposta compactada com zstd; os demais tokens servem como alternativas. `br` pode ser decodificado, mas não é anunciado por padrão.

O tamanho da carga útil e o uso de CPU no servidor e no cliente variam entre os codecs conforme os dados, a conexão e o valor de `http_zlib_compression_level` no servidor (valor padrão: 3) — consulte [Ajustar a compressão](#tuning-compression).

* **`http_zlib_compression_level`.** Essa configuração se aplica a todos os codecs HTTP, e seu valor padrão é 3. Ajuste-a de acordo com os dados, a velocidade da conexão e o uso de CPU.
* **Cliente limitado pela CPU em uma conexão rápida.** O driver decodifica o corpo da resposta na thread que fez a chamada; portanto, quando a rede não é o gargalo, a velocidade de decodificação no cliente pode ser o fator limitante.

Solicite um codec diferente por consulta ou para todo o cliente sempre que uma dessas situações se aplicar:

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

Como a decisão se baseia na resposta, o corpo é decodificado sempre que `Content-Encoding` assim indicar, independentemente do que foi solicitado: se o cabeçalho estiver ausente ou tiver o valor `identity`, o corpo passa sem alterações; se indicar um codec compatível, o corpo é decodificado; qualquer outro valor gera um erro que o identifica. Não há risco de decodificação dupla — se o `AutomaticDecompression` de um handler fornecido pelo chamador já tiver decodificado o corpo, ele também remove `Content-Encoding`. Assim, o driver recebe o conteúdo sem compressão e não o altera.

**Resultados brutos não anunciam nenhum codec.** `ExecuteRawResultAsync` (assim como os métodos públicos `PostStreamAsync` / `InsertRawStreamAsync`) entregam o corpo sem alterações. Por isso, a menos que você especifique um codec, eles não solicitam nenhum — como o driver não decodifica esse corpo, anunciar um codec nesse caso transformaria silenciosamente uma exportação em um arquivo comprimido. A regra é simples e independe da configuração de qualquer `HttpClient`: **o corpo chega exatamente como o servidor o enviou, e o servidor envia os dados sem compressão, a menos que você solicite um codec.** Para exportar bytes comprimidos intencionalmente, solicite um codec para todo o cliente ou por consulta.

Um `AcceptEncoding` explícito (em qualquer um dos níveis) continua valendo para requisições brutas, e `ClickHouseRawResult.ReadDecompressedStreamAsync()` decodifica o resultado quando necessário; `ReadAsStreamAsync`, `ReadAsByteArrayAsync`, `ReadAsStringAsync` e `CopyToAsync` sempre retornam os bytes exatamente como chegaram.

```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();
```

Leia o stream retornado até o fim antes que ele saia do escopo, como no exemplo acima. Quando a resposta *está* comprimida, você recebe um decodificador criado com `leaveOpen`, de modo que descartá-lo preserva a resposta; quando ela **não** está comprimida, você recebe o próprio stream do conteúdo HTTP, e descartá-lo encerra o corpo da resposta. Em ambos os casos, o `ClickHouseRawResult` é responsável pela resposta — não use seus outros membros de leitura após descartar o stream. É sempre necessário descartar o `ClickHouseRawResult`, e isso, por si só, é suficiente: ele libera a resposta e qualquer decodificador criado aqui (os decodificadores mantêm buffers de um pool). Assim, o `await using` acima é opcional, mas pode ser mantido com segurança. Chamadas sequenciais repetidas retornam o mesmo stream; não é seguro usar esse tipo simultaneamente.

Consulte [Select\_007\_ResponseCompression.cs](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Http/Select/Select_007_ResponseCompression.cs) para ver um exemplo executável.

<h4 id="insert-compression">
  Compressão de inserts (requisições)
</h4>

Zstd é o codec padrão para inserts: `InsertOptions.Compressor` é inicializado com `ZstdCompressor.Default`,
que usa zstd no nível 3. Defina outro compressor para alterar o codec ou use `null` para enviar o
corpo sem compressão.

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

O driver vem com quatro codecs. Cada um tem uma instância `Default` e um construtor que recebe um nível
e o tamanho do buffer de escrita:

| Compressor | `Content-Encoding` | Construtor | `Default` |
| - | - | - | - |
| `ZstdCompressor` | `zstd` | `(int level = 3, int bufferSize = 262144)` | nível 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>
  *Compartilhe as instâncias dos compressores.* Cada `Default` é uma instância compartilhada, e os quatro compressores
  podem ser usados com segurança por várias threads ao mesmo tempo — como acontece quando
  `InsertOptions.MaxDegreeOfParallelism` é maior que 1, pois cada inserção usa um compressor por
  lote. Nenhum deles implementa `IDisposable`. Crie uma instância própria e reutilize-a, assim
  como se faz com `Default`.
</Note>

<h5 id="custom-compressor">
  Um codec personalizado
</h5>

`IClickHouseCompressor` é público, e sua implementação precisa fornecer apenas dois membros:

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

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

O servidor deve aceitar o `Content-Encoding` especificado. Os demais membros —
`Decompress`, `MethodByte`, `MaxEncodedLength`, `Encode` e `Decode` — têm implementações
padrão que lançam `NotSupportedException`; portanto, sobrescreva apenas os membros de que seu codec precisa.
Além de comprimir as requisições, implemente `Decompress` para decodificar os corpos das respostas. O stream retornado deve lançar
`InvalidDataException` quando o corpo estiver corrompido ou em um formato incorreto.

`InsertOptions.Compressor` controla apenas o insert binário. Os demais corpos das requisições do driver seguem outras regras de compressão e não passam por esse componente:

* **Toda requisição com SQL em texto** (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, `ExecuteRawResultAsync`, a camada ADO.NET) envia a instrução com `Content-Encoding: gzip` sempre que `UseCompression` é `true` — ou seja, por padrão. O codec não é configurável: `AcceptEncoding` controla apenas a resposta, então, para a requisição, as opções são gzip ou nenhuma compressão. `Compression=false` envia a instrução sem compressão. Como as instruções são pequenas, isso raramente exige atenção — mas é útil saber ao monitorar requisições em um proxy ou em uma captura de pacotes.
* **Um corpo multipart** — de uma consulta cujos parâmetros são enviados como dados de formulário (`UseFormDataParameters=true`) — é sempre enviado sem compressão, independentemente do valor de `UseCompression`.
* **Um upload de dados brutos** (`InsertRawStreamAsync`, `PostStreamAsync`) usa um sinalizador próprio para cada chamada e não depende de `UseCompression` nem de `InsertOptions.Compressor`: usa gzip quando o sinalizador está ativado e não usa compressão caso contrário. Observe que o parâmetro `useCompression` de `InsertRawStreamAsync` tem `true` como valor padrão. Portanto, o upload é comprimido com gzip, a menos que você passe `false` — mesmo com `Compression=false` no cliente.

***

<h3 id="tuning-compression">
  Ajustando a compressão
</h3>

A compressão reduz o volume de dados transmitidos, mas exige mais processamento da CPU. Se vale a pena depende quase inteiramente da velocidade da
conexão em comparação com a velocidade de execução do codec. Não existe uma configuração ideal para
todos os casos.

<h4 id="the-one-number-that-decides-it">
  O número que faz a diferença
</h4>

Vale a pena comprimir os dados quando o codec é mais rápido que a rede.

No caminho de leitura, esse limiar é mais baixo do que muitos imaginam, porque o ClickHouse comprime
as respostas HTTP em uma única thread no buffer de saída. Em medições feitas em um serviço ClickHouse Cloud com 16 vCPUs
(`hits`, RowBinary, nível 3), o servidor gera saída comprimida a cerca de 100 a 200 MB/s.

Assim, para um resultado grande, supondo que apenas uma consulta seja processada por vez, a compressão deixa de compensar por volta de 100 MB/s. Um único fluxo HTTPS
dentro da mesma região de nuvem costuma superar essa velocidade, enquanto conexões pela internet pública, por uma VPN ou entre regiões geralmente ficam abaixo dela.

No caminho de inserção, a compressão continua valendo a pena em velocidades de conexão mais altas, porque o cliente comprime os dados em um núcleo de CPU próprio e costuma ser mais rápido que o servidor ao comprimir respostas.

<h4 id="rough-guide-by-deployment">
  Guia geral por ambiente de implantação
</h4>

| Onde seu cliente é executado | Largura de banda típica | Leituras | Insert |
| - | - | - | - |
| Mesmo host / loopback | > 500 MB/s | `identity` | `lz4` é o mais rápido, ou sem compressão |
| Mesma região, mesma nuvem | \~100–500 MB/s | `identity` ou `lz4` | `zstd:1` |
| Entre regiões, mesma nuvem | \~10–100 MB/s | `zstd` | `zstd:3` |
| Internet / VPN / outra nuvem | \< 25 MB/s | `zstd` | `zstd:3` |
| Tráfego tarifado ou conexão muito limitada | \< 5 MB/s | `zstd` | `zstd:5`+ ou `br` |

Há três fatores que esta tabela não considera:

* **Custo do tráfego de saída:** se você paga pela transferência de dados, cada byte tem um custo além do impacto na latência, o que
  favorece uma compressão mais alta independentemente da velocidade da conexão.
* **Resultados pequenos:** as recomendações acima se referem a cargas úteis grandes. Para respostas pequenas, o codec quase não
  faz diferença, e a sobrecarga por requisição predomina.
* **Inserts paralelos elevam os limiares de inserção.** Todos os valores de vazão acima consideram uma *única* thread. O valor padrão de `InsertOptions.MaxDegreeOfParallelism` é `1`, mas aumentá-lo permite comprimir lotes em paralelo. Assim, a taxa total de codificação do cliente cresce aproximadamente conforme o número de núcleos alocados. Por isso, em uma conexão rápida, ainda pode valer a pena
  comprimir uma inserção paralela mesmo em velocidades nas quais a compressão de uma inserção de thread única já não compensa. Considere os valores da coluna de insert como um *limite inferior* e, se você já processa lotes em paralelo, teste novamente antes de concluir que sua conexão é rápida demais para a compressão.

O caminho de leitura só é paralelizado quando há várias consultas.

<h4 id="choosing-a-codec">
  Escolhendo um codec
</h4>

| Codec | Taxa de compressão | Quando usar | Atenção |
| - | - | - | - |
| `lz4` | a mais baixa | Conexões rápidas, quando a CPU é mais escassa que a largura de banda. É, de longe, o que exige menos recursos para decodificar e o mais rápido com resultados pequenos — o codec a escolher quando você quer fugir do `zstd` padrão. | **Não tem codificador de entropia**. Por isso, com dados de distribuição desigual, mas sem repetição (como longas sequências de texto numérico), sua taxa de compressão fica bem abaixo da dos demais. Também é o codec mais afetado pelo aumento de `http_zlib_compression_level`: passar do nível 1 para o 3 exige cerca de 2,7× mais CPU para reduzir o volume de dados em cerca de 29%. |
| `zstd` | alta | A escolha de uso geral quando há uma rede de verdade envolvida. Oferece a melhor taxa de compressão em relação ao uso de CPU na faixa que importa e, no nível 3, supera o `lz4` em volume de dados, uso de CPU do servidor *e* tempo decorrido. | Exige mais recursos para **decodificar** que o `lz4` — 1,6× no nível 3 em nossas medições, embora os dois sejam comparáveis no nível 1 —, e o driver faz a decodificação na thread que executa a chamada. Especificamente com `http_zlib_compression_level=1`, consome um pouco *mais* CPU do servidor que o `lz4`. |
| `gzip` | média | Interoperabilidade — é compreendido por todos os proxies e gateways. | Em nossas medições, perde para `lz4` e `zstd` em todos os aspectos: gera mais dados que `zstd`, exige várias vezes mais CPU para codificar e de 5 a 9 vezes mais para decodificar. Escolha-o pela compatibilidade, não pelo desempenho. |
| `br` | a mais alta em níveis baixos | A largura de banda é realmente o fator limitante, e você pode gastar mais CPU para economizá-la. | Seu custo aumenta drasticamente em níveis mais altos — com `http_zlib_compression_level=6`, medimos um uso de CPU do servidor de 3 a 4 vezes o do `zstd`. Não é anunciado por padrão, pois tem prioridade sobre todos os tokens de fallback da lista padrão. |

<h4 id="levels">
  Níveis
</h4>

A compressão da resposta é controlada por uma única configuração do servidor, `http_zlib_compression_level`, que se aplica a *todos* os codecs HTTP, não apenas ao zlib. O valor padrão é 3.

Não altere essa configuração a menos que as medições justifiquem a mudança. Acima do valor padrão, a redução no tamanho é pequena em comparação com o aumento no uso de CPU (para `zstd`, passar de 3 para 6 praticamente dobra o uso de CPU do servidor para reduzir o volume de dados em cerca de 14%), e o desempenho de `br` piora drasticamente. Abaixo do valor padrão, no nível 1, o cenário muda de fato: `lz4` passa a consumir muito menos CPU, e `zstd` perde a vantagem que tinha sobre ele nesse aspecto. Se necessário, defina o valor por consulta:

```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">
  Medindo o ponto de equilíbrio no seu ambiente
</h4>

A maneira mais rápida de escolher o codec e o nível de compressão ideais é medir o tempo de execução da mesma consulta com diferentes codecs e comparar os resultados.

```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");
}
```

Para ver o mesmo cenário do ponto de vista do servidor, consulte `ProfileEvents` em `system.query_log` — defina
`QueryOptions.QueryId` para localizar a linha:

```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';
```

Uma armadilha ao fazer esse benchmark por conta própria: um simples `LIMIT n` sem `ORDER BY` retorna *linhas diferentes
a cada execução*. Assim, cada repetição comprime dados diferentes, e as razões deixam de ser significativas. Faça a comparação
com um conjunto de resultados fixo.

***

<h3 id="raw-stream-insert">
  Inserção a partir de um fluxo de dados brutos
</h3>

Use `InsertRawStreamAsync` para inserir dados diretamente de fluxos de arquivos ou da memória em formatos como CSV, JSON, Parquet ou qualquer [formato compatível com o ClickHouse](/pt-BR/reference/formats/index).

**Inserir dados de um arquivo 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>
  *O driver assume a posse do stream.* `InsertRawStreamAsync` e `PostStreamAsync` descartam o
  stream fornecido quando a requisição termina, com ou sem sucesso. Não o descarte
  por conta própria nem o reutilize depois — por isso, o exemplo acima não coloca o
  `FileStream` em um bloco `using`.

  Um bloco `using` criado por você é executado depois que o driver já descartou o stream. Para um `FileStream` ou
  `MemoryStream`, essa segunda chamada é inofensiva. Mas, se o `Dispose` do stream devolver um
  buffer ao pool ou reduzir uma contagem de referências, o recurso será liberado duas vezes.

  A posse só é transferida depois que os argumentos são aceitos: se a chamada lançar `ArgumentException` ou
  `ArgumentNullException` por falta de tabela, stream ou formato, o stream ainda é seu.
</Warning>

<Note>
  Consulte a [documentação das configurações de formato](/pt-BR/reference/settings/formats) para conhecer as opções de controle do comportamento da ingestão de dados.
</Note>

***

<h3 id="more-examples">
  Mais exemplos
</h3>

Para ver mais exemplos práticos de uso, consulte o [diretório de exemplos](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples) no repositório do GitHub.

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

A biblioteca oferece suporte completo ao ADO.NET por meio de `ClickHouseConnection`, `ClickHouseCommand` e `ClickHouseDataReader`. Essa API é necessária para a integração com ORMs (Dapper, Linq2db) e para o uso das abstrações padrão de banco de dados do .NET.

<h3 id="ado-net-datasource">
  Gerenciamento do ciclo de vida com ClickHouseDataSource
</h3>

**Sempre crie conexões por meio de um `ClickHouseDataSource`** para garantir o gerenciamento adequado do ciclo de vida e o reaproveitamento das conexões. O DataSource gerencia internamente um único `ClickHouseClient`, e todas as conexões compartilham o pool de conexões HTTP desse cliente.

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

// Create DataSource once (register as singleton in DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Create lightweight connections as needed
await using var connection = await dataSource.OpenConnectionAsync();

// Use the connection
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();
```

Para injeção de dependências:

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

// In your service
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

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

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Use connection...
    }
}
```

<Warning>
  **Não crie `ClickHouseConnection` diretamente** no código de produção. Cada instanciação direta cria um novo cliente HTTP e um novo pool de conexões, o que pode esgotar os sockets sob carga:

  ```csharp theme={null}
  // NÃO FAÇA ISSO - cria um novo pool de conexões a cada instanciação
  using var conn = new ClickHouseConnection("Host=localhost");
  await conn.OpenAsync();
  ```

  Em vez disso, use sempre `ClickHouseDataSource` ou compartilhe uma única instância de `ClickHouseClient`.
</Warning>

***

<h3 id="ado-net-command">
  Usando ClickHouseCommand
</h3>

Crie comandos usando uma conexão para executar SQL:

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

// Create command with SQL
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// Execute and read results
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}
```

Métodos de comando:

* `ExecuteNonQueryAsync()` - Para instruções INSERT, UPDATE, DELETE e DDL
* `ExecuteScalarAsync()` - Retorna o valor da primeira coluna da primeira linha
* `ExecuteReaderAsync()` - Retorna um `ClickHouseDataReader` para percorrer os resultados

***

<h3 id="ado-net-reader">
  Usando ClickHouseDataReader
</h3>

O `ClickHouseDataReader` oferece acesso tipado aos resultados das consultas:

```csharp theme={null}
await using var reader = await command.ExecuteReaderAsync();

while (reader.Read())
{
    // Access by column index
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // Access by column name
    var email = reader.GetString("email");

    // Generic access
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // Check for null
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}
```

<h4 id="ado-net-reader-enum-ordinal">
  Lendo o valor ordinal de um enum
</h4>

Uma coluna `Enum8` ou `Enum16` é materializada como seu rótulo: `GetFieldType` informa `string`, e
`GetString`, `GetValue` e `GetFieldValue<string>` retornam o rótulo. Os acessores numéricos
lançam `InvalidCastException` ao acessar uma coluna enum, pois o valor armazenado é uma string.

Use `TryGetEnumOrdinal` para obter o número associado ao rótulo:

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

Retorna `true` e atribui um valor a `value` para uma coluna `Enum8`/`Enum16` e para uma coluna `Nullable(Enum...)`
cuja célula não seja NULL. Retorna `false`, com `value` definido como `0`, para uma célula NULL ou para
qualquer coluna que não seja do tipo enum. O ordinal é o valor com sinal recebido no formato
wire; por isso, pode ser negativo, e o ordinal de um `Enum16` pode ser maior que um
byte.

<h2 id="best-practices">
  Boas práticas
</h2>

<h3 id="best-practices-connection-lifetime">
  Ciclo de vida das conexões e pool de conexões
</h3>

`ClickHouse.Driver` usa `System.Net.Http.HttpClient` internamente. `HttpClient` mantém um pool de conexões para cada endpoint. Por isso:

* As sessões do banco de dados compartilham as conexões HTTP gerenciadas pelo pool.
* As conexões HTTP são recicladas automaticamente pelo pool.
* As conexões podem permanecer ativas mesmo após o descarte dos objetos `ClickHouseClient` ou `ClickHouseConnection`.

**Práticas recomendadas:**

| Cenário | Abordagem recomendada |
| - | - |
| Uso geral | Use uma única instância compartilhada de `ClickHouseClient` |
| ADO.NET / ORMs | Use `ClickHouseDataSource` (cria conexões que compartilham o mesmo pool) |
| Ambientes com injeção de dependências | Registre `ClickHouseClient` ou `ClickHouseDataSource` como singleton com `IHttpClientFactory` |

<Warning>
  Ao usar um `HttpClient` ou `HttpClientFactory` personalizado, configure `PooledConnectionIdleTimeout` com um valor menor que o `keep_alive_timeout` do servidor para evitar erros causados por conexões encerradas apenas de um lado. O valor padrão de `keep_alive_timeout` nas implantações no Cloud é de 10 segundos.
</Warning>

<Warning>
  Evite criar várias instâncias de `ClickHouseClient` ou instâncias independentes de `ClickHouseConnection` sem um `HttpClient` compartilhado. Cada instância cria seu próprio pool de conexões.
</Warning>

***

<h3 id="best-practice-datetime">
  Tratamento de DateTime
</h3>

1. **Use UTC sempre que possível.** Armazene os timestamps em colunas `DateTime('UTC')` e use `DateTimeKind.Utc` no código. Isso elimina ambiguidades de fuso horário.

2. **Use `DateTimeOffset` para lidar explicitamente com fusos horários.** Ele sempre representa um instante específico e inclui a informação de deslocamento em relação ao UTC.

3. **Especifique o fuso horário nas indicações de tipo SQL.** Ao usar parâmetros com valores DateTime `Unspecified` para colunas que não usam UTC, inclua o fuso horário no 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">
  Inserções assíncronas
</h3>

[Inserções assíncronas](/pt-BR/concepts/features/operations/insert/asyncinserts) transferem do cliente para o servidor a responsabilidade pelo agrupamento em lote. Em vez de exigir que o cliente agrupe os dados em lotes, o servidor armazena temporariamente os dados recebidos e os grava no armazenamento quando atingem limiares configuráveis. Isso é útil em cenários de alta concorrência, como cargas de trabalho de observabilidade em que muitos agentes enviam pequenas cargas úteis.

Ative as inserções assíncronas por meio de `CustomSettings` ou da string de conexão:

```csharp theme={null}
// Using CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Recommended: wait for flush acknowledgment

// Or via connection string
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"
```

**Dois modos** (controlados por `wait_for_async_insert`):

| Modo | Comportamento | Caso de uso |
| - | - | - |
| `wait_for_async_insert=1` | A inserção retorna após a gravação dos dados em disco. Os erros são retornados ao cliente. | **Recomendado** para a maioria das cargas de trabalho |
| `wait_for_async_insert=0` | A inserção retorna assim que os dados são armazenados em buffer. Não há garantia de que os dados serão persistidos. | Apenas quando a perda de dados é aceitável |

<Warning>
  Com `wait_for_async_insert=0`, os erros só aparecem durante a gravação dos dados do buffer e não podem ser associados à inserção original. O cliente também não aplica contrapressão, o que pode sobrecarregar o servidor.
</Warning>

**Principais configurações:**

| Configuração | Descrição |
| - | - |
| `async_insert_max_data_size` | Grava os dados do buffer quando ele atinge este tamanho (bytes) |
| `async_insert_busy_timeout_ms` | Grava os dados do buffer após este tempo limite (milissegundos) |
| `async_insert_max_query_number` | Grava os dados do buffer quando este número de consultas se acumula |

***

<h3 id="best-practices-sessions">
  Sessões
</h3>

Habilite as sessões apenas quando precisar de funcionalidades que mantêm estado no lado do servidor, como:

* Tabelas temporárias (`CREATE TEMPORARY TABLE`)
* Manutenção do contexto das consultas entre várias instruções
* Configurações no nível da sessão (`SET max_threads = 4`)

Quando as sessões estão habilitadas, as requisições são processadas em sequência para impedir o uso simultâneo da mesma sessão. Isso gera sobrecarga para cargas de trabalho que não precisam manter o estado da sessão.

```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)"
);
```

**Uso do ADO.NET (para compatibilidade com ORMs):**

```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">
  Tipos de dados compatíveis
</h2>

`ClickHouse.Driver` oferece suporte a todos os tipos de dados do ClickHouse. As tabelas abaixo mostram a correspondência entre os tipos do ClickHouse e os tipos nativos do .NET na leitura de dados do banco de dados.

<h3 id="clickhouse-native-type-map-reading">
  Mapeamento de tipos: leitura de dados do ClickHouse
</h3>

<h4 id="type-map-reading-integer">
  Tipos de números inteiros
</h4>

| Tipo do ClickHouse | Tipo do .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">
  Tipos de ponto flutuante
</h4>

| Tipo do ClickHouse | Tipo do .NET |
| - | - |
| Float32 | `float` |
| Float64 | `double` |
| BFloat16 | `float` |

***

<h4 id="type-map-reading-decimal">
  Tipos decimais
</h4>

| Tipo do ClickHouse | Tipo do .NET |
| - | - |
| Decimal(P, S) | `decimal` / `ClickHouseDecimal` |
| Decimal32(S) | `decimal` / `ClickHouseDecimal` |
| Decimal64(S) | `decimal` / `ClickHouseDecimal` |
| Decimal128(S) | `decimal` / `ClickHouseDecimal` |
| Decimal256(S) | `decimal` / `ClickHouseDecimal` |

<Note>
  A conversão de tipos decimais é controlada pela configuração UseCustomDecimals.
</Note>

***

<h4 id="type-map-reading-boolean">
  Tipo booleano
</h4>

| Tipo do ClickHouse | Tipo .NET |
| - | - |
| Bool | `bool` |

***

<h4 id="type-map-reading-strings">
  Tipos de string
</h4>

| Tipo do ClickHouse | Tipo do .NET |
| - | - |
| String | `string` |
| FixedString(N) | `string` |

<Note>
  Por padrão, as colunas `String` e `FixedString(N)` são retornadas como `string`. Defina `ReadStringsAsByteArrays=true` na string de conexão para lê-las como `byte[]`. Isso é útil para armazenar dados binários que talvez não sejam UTF-8 válido.

  A configuração também se aplica a strings aninhadas em outros tipos: `Array(String)` é lido como `byte[][]`
  e `Map(String, String)` como `Dictionary<byte[], byte[]>` — inclusive as chaves. A única exceção é uma
  coluna `JSON`, cujos valores de string são sempre tratados como texto; consulte [tipo JSON](#type-map-reading-json).
</Note>

***

<h4 id="type-map-reading-datetime">
  Tipos de data e hora
</h4>

| Tipo do ClickHouse | Tipo do .NET |
| - | - |
| Date | `DateTime` |
| Date32 | `DateTime` |
| DateTime | `DateTime` |
| DateTime32 | `DateTime` |
| DateTime64 | `DateTime` |
| Time | `TimeSpan` |
| Time64 | `TimeSpan` |

O ClickHouse armazena internamente os valores `DateTime` e `DateTime64` como timestamps Unix (segundos ou frações de segundo decorridos desde a epoch). Embora o armazenamento seja sempre em UTC, as colunas podem ter um fuso horário associado, que afeta a exibição e a interpretação dos valores.

Ao ler valores `DateTime`, a propriedade `DateTime.Kind` é definida de acordo com o fuso horário da coluna:

| Definição da coluna | DateTime.Kind retornado | Observações |
| - | - | - |
| `DateTime('UTC')` | `Utc` | Fuso horário UTC explícito |
| `DateTime('Europe/Amsterdam')` | `Unspecified` | Deslocamento aplicado |
| `DateTime` | `Unspecified` | Hora local preservada sem alterações |

Para colunas com fuso horário diferente de UTC, o `DateTime` retornado representa a hora local nesse fuso horário. Use `ClickHouseDataReader.GetDateTimeOffset()` para obter um `DateTimeOffset` com o deslocamento correto:

```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)
```

Para colunas **sem** um fuso horário explícito (ou seja, `DateTime` em vez de `DateTime('Europe/Amsterdam')`), o driver retorna um `DateTime` com `Kind=Unspecified`. Isso preserva a hora local exatamente como foi armazenada, sem presumir um fuso horário.

Se você precisar considerar o fuso horário em colunas sem um fuso horário explícito, escolha uma das opções:

1. Especifique o fuso horário na definição da coluna: `DateTime('UTC')` ou `DateTime('Europe/Amsterdam')`
2. Aplique o fuso horário após a leitura.

***

<h4 id="type-map-reading-json">
  Tipo JSON
</h4>

| Tipo do ClickHouse | Tipo do .NET | Observações |
| - | - | - |
| Json | `JsonObject` | Padrão (`JsonReadMode=Binary`) |
| Json | `string` | Quando `JsonReadMode=String` |

O tipo de retorno das colunas JSON é determinado pela configuração `JsonReadMode`:

* **`Binary` (padrão)**: Retorna `System.Text.Json.Nodes.JsonObject`. Permite acessar os dados JSON de forma estruturada, mas tipos específicos do ClickHouse (como endereços IP, UUIDs e números decimais grandes) são convertidos em representações em string dentro da estrutura JSON.

* **`String`**: Retorna o JSON bruto como `string`. Preserva a representação exata do JSON fornecida pelo ClickHouse, o que é útil para repassar o JSON sem fazer o parsing ou para cuidar da desserialização por conta própria.

```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` é um terceiro modo. Ele lê os dados exatamente como `Binary`, mas não envia nenhuma configuração do servidor junto com a
consulta — use esse modo em uma conexão sem permissão para definir configurações do servidor.

<h5 id="type-map-reading-json-nulls">
  Caminhos tipados e valores nulos
</h5>

Um caminho declarado no tipo da coluna é um **caminho tipado**; qualquer outro caminho no documento é um
**caminho dinâmico**. A diferença entre eles aparece quando um valor é nulo.

Um caminho tipado sempre aparece no `JsonObject`. Quando declarado como `Nullable(T)` ou `Dynamic`, ele é retornado
como null em JSON tanto se o valor armazenado for nulo quanto se o documento não contiver esse caminho — não é possível
distinguir os dois casos:

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

Quando declarado com um tipo que não aceita valores nulos, um caminho ausente assume o valor padrão desse tipo — `JSON(x String)`
produz `{"x":""}` e `JSON(x Int64)` produz `{"x":0}`.

Um caminho dinâmico com valor nulo é removido por completo do objeto; por isso, `ContainsKey` retorna
false para esse caminho. Ao ler `{"x":null}` de uma coluna `JSON` comum, o resultado é `{}`.

Caminhos tipados aninhados criam os níveis superiores, de modo que `JSON(a.b Nullable(Int64))` produz `{"a":{"b":null}}`
mesmo para um documento vazio.

<Note>
  É assim que o próprio servidor renderiza o resultado; portanto, os modos `Binary` e `String` agora se comportam da mesma forma. Antes da versão 1.4.0, um
  caminho tipado com valor nulo era removido do `JsonObject`, fazendo com que `{"x":null}` fosse lido como
  `{}` — e, para um caminho aninhado como `JSON(a.b Nullable(Int64))`, toda a subárvore `a` desaparecia.
</Note>

<h5 id="type-map-reading-json-strings">
  Strings em uma coluna JSON
</h5>

Os valores de string nas folhas de uma coluna `JSON` são sempre retornados como texto, seja qual for
o valor de `ReadStringsAsByteArrays` — `JsonValue` não tem representação como array de bytes; por isso, um `byte[]` seria
representado em Base64. Isso vale para `String`, `FixedString` e para esses tipos quando encapsulados em
`LowCardinality`, `Nullable` ou `SimpleAggregateFunction`, além de strings dentro de `Array` e `Map`,
inclusive as chaves de map.

<Note>
  Um array de bytes cujo tipo o leitor de JSON não consegue identificar ainda é representado em Base64: um
  caminho tipado como `Variant` ou `Dynamic` contém um valor cujo tipo só é conhecido em cada linha. Por isso, uma string
  em `Variant(Array(UInt8), String)` é retornada codificada em Base64. Isso vale para ambas as configurações.

  Um tipo de chave de map JSON que não seja exatamente `String` — `Map(LowCardinality(String), String)`, por
  exemplo — lança `NotSupportedException`.
</Note>

<h5 id="overlapping-paths">
  Caminhos sobrepostos
</h5>

O ClickHouse aceita uma coluna que declara um caminho tanto como valor quanto como pai de outro
caminho, por exemplo, `JSON(a Int64, a.b Int64)`. Ambos os caminhos estão presentes em todas as linhas, então o servidor
gera a linha com uma chave duplicada: `{"a":0,"a":{"b":7}}`. Um `JsonObject` não pode conter dois valores
para a mesma chave. Por isso, `JsonReadMode.Binary` lança uma `SerializationException` que identifica os dois caminhos. O
mesmo acontece quando o valor é um `Map`, como em `JSON(a Map(String, Int64))`, lido de uma linha que
também contém um `a.b` dinâmico.

Isso só se aplica quando ambos os caminhos contêm um valor na linha. Se um deles não contiver nada — um valor nulo,
um objeto vazio ou uma subárvore cujos valores são todos nulos —, prevalece o caminho que contém dados,
independentemente de qual dos dois o servidor envia primeiro. Assim, quando a sobreposição é declarada com tipos `Nullable`, apenas um dos caminhos é preenchido por linha, e a leitura ocorre sem
erro: `JSON(a Nullable(Int64), a.b Nullable(Int64))` produz `{"a":5}` e `{"a":{"b":7}}`, como
esperado.

Leia essa coluna com `JsonReadMode.String` para obter o texto JSON enviado pelo servidor sem alterações, inclusive a
chave duplicada.

Defina `AllowDuplicateJsonKeys` para continuar lendo a coluna como um `JsonObject` sem lançar uma exceção. O
driver mantém o último dos dois valores na linha e descarta o outro. Portanto, há
perda de dados: `JSON(a Int64, a.b Int64)` contendo `{"a.b":7}` é lido como `{"a":0}`. Se um caminho
contiver um valor e seu pai contiver um escalar ou um array, uma exceção ainda será lançada, pois uma subárvore não pode ser
inserida em nenhum dos dois.

```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">
  Tipo Map
</h4>

| Tipo do ClickHouse | Tipo do .NET | Observações |
| - | - | - |
| Map(K, V) | `Dictionary<K, V>` | Padrão (`MapReadMode=Dictionary`) |
| Map(K, V) | `List<KeyValuePair<K, V>>` | Quando `MapReadMode=KeyValuePairs` |

Um `Map(K, V)` do ClickHouse é armazenado fisicamente como um `Array(Tuple(K, V))` e pode conter vários pares com a mesma chave. Um `Dictionary` não permite isso; portanto, no modo padrão, apenas o último valor de cada chave repetida é mantido, e os pares anteriores são descartados. A configuração `MapReadMode` determina a representação:

* **`Dictionary` (padrão)**: Retorna `Dictionary<K, V>`.

* **`KeyValuePairs`**: Retorna `List<KeyValuePair<K, V>>` na ordem em que o servidor enviou os pares, preservando todos eles, inclusive os que têm a mesma chave.

```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"
```

O modo determina o tipo usado pelo framework para uma coluna `Map`. Por isso, também afeta `GetFieldValue<T>`, os tipos de schema informados pelo driver e o mapeamento de propriedades de POCO. Isso vale sempre que um map aparecer na hierarquia de tipos de uma coluna — inclusive em `Array(Map(...))`, `Map(K, Map(...))`, `Tuple(..., Map(...))` e `Dynamic`.

Ambas as representações são aceitas para gravação em qualquer um dos modos — consulte [gravação de maps](#type-map-writing-other).

***

<h4 id="type-map-reading-other">
  Outros tipos
</h4>

| Tipo do ClickHouse | Tipo do .NET |
| - | - |
| UUID | `Guid` |
| IPv4 | `IPAddress` |
| IPv6 | `IPAddress` |
| Nothing | `DBNull` |
| Dynamic | Consulte a nota |
| Array(T) | `T[]` (`Array(Array(T))` aninhado é lido como um array de arrays `T[][]`; use `reader.GetFieldValue<T[,]>(ordinal)` para materializar dados retangulares em um array CLR multidimensional) |
| Tuple(T1, T2, ...) | `Tuple<T1, T2, ...>` / `LargeTuple` |
| Map(K, V) | `Dictionary<K, V>` ou `List<KeyValuePair<K, V>>` quando `MapReadMode=KeyValuePairs` — consulte [Tipo Map](#type-map-reading-map) |
| Nullable(T) | `T?` |
| Enum8 | `string` |
| Enum16 | `string` |
| LowCardinality(T) | O mesmo que T |
| SimpleAggregateFunction | O mesmo que o tipo subjacente |
| Nested(...) | `Tuple[]` |
| Variant(T1, T2, ...) | Consulte a nota |
| QBit(T, dimension) | `T[]` |

<Note>
  Os tipos Dynamic e Variant são convertidos, em cada linha, para o tipo correspondente ao valor subjacente.
</Note>

***

<h4 id="type-map-reading-geometry">
  Tipos geométricos
</h4>

| Tipo do ClickHouse | Tipo do .NET |
| - | - |
| Point | `Tuple<double, double>` |
| Ring | `Tuple<double, double>[]` |
| LineString | `Tuple<double, double>[]` |
| Polygon | `Ring[]` |
| MultiLineString | `LineString[]` |
| MultiPolygon | `Polygon[]` |
| Geometry | Consulte a observação |

<Note>
  O tipo Geometry é um tipo Variant que pode armazenar qualquer um dos tipos geométricos. O valor será convertido para o tipo correspondente.
</Note>

***

<h3 id="clickhouse-native-type-map-writing">
  Mapeamento de tipos: gravação no ClickHouse
</h3>

Ao inserir dados, o driver converte os tipos .NET para os tipos correspondentes do ClickHouse. As tabelas abaixo mostram quais tipos .NET são aceitos para cada tipo de coluna do ClickHouse.

<h4 id="type-map-writing-integer">
  Tipos inteiros
</h4>

| Tipo do ClickHouse | Tipos .NET aceitos | Observações |
| - | - | - |
| Int8 | `sbyte`, qualquer tipo compatível com `Convert.ToSByte()` | |
| UInt8 | `byte`, qualquer tipo compatível com `Convert.ToByte()` | |
| Int16 | `short`, qualquer tipo compatível com `Convert.ToInt16()` | |
| UInt16 | `ushort`, qualquer tipo compatível com `Convert.ToUInt16()` | |
| Int32 | `int`, qualquer tipo compatível com `Convert.ToInt32()` | |
| UInt32 | `uint`, qualquer tipo compatível com `Convert.ToUInt32()` | |
| Int64 | `long`, qualquer tipo compatível com `Convert.ToInt64()` | |
| UInt64 | `ulong`, qualquer tipo compatível com `Convert.ToUInt64()` | |
| Int128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, qualquer tipo compatível com `Convert.ToInt64()` | |
| UInt128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, qualquer tipo compatível com `Convert.ToInt64()` | |
| Int256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, qualquer tipo compatível com `Convert.ToInt64()` | |
| UInt256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, qualquer tipo compatível com `Convert.ToInt64()` | |

***

<h4 id="type-map-writing-floating-point">
  Tipos de ponto flutuante
</h4>

| Tipo do ClickHouse | Tipos .NET aceitos | Observações |
| - | - | - |
| Float32 | `float`, qualquer tipo compatível com `Convert.ToSingle()` | |
| Float64 | `double`, qualquer tipo compatível com `Convert.ToDouble()` | |
| BFloat16 | `float`, qualquer tipo compatível com `Convert.ToSingle()` | Trunca para o formato brain float de 16 bits |

***

<h4 id="type-map-writing-boolean">
  Tipo booleano
</h4>

| Tipo do ClickHouse | Tipos .NET aceitos | Observações |
| - | - | - |
| Bool | `bool` | |

***

<h4 id="type-map-writing-strings">
  Tipos de string
</h4>

| Tipo do ClickHouse | Tipos .NET aceitos | Observações |
| - | - | - |
| String | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | Tipos binários gravados diretamente; os fluxos podem permitir ou não o reposicionamento da leitura |
| FixedString(N) | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | A string é codificada em UTF-8 e preenchida até atingir o comprimento definido; os tipos binários devem ter exatamente N bytes |

***

<h4 id="type-map-writing-datetime">
  Tipos de data e hora
</h4>

| Tipo do ClickHouse | Tipos .NET aceitos | Observações |
| - | - | - |
| Date | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos do NodaTime | Convertido em dias Unix como UInt16; intervalo aceito `[1970-01-01, 2149-06-06]` |
| Date32 | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos do NodaTime | Convertido em dias Unix como Int32; intervalo aceito `[1900-01-01, 2299-12-31]` |
| DateTime | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos do NodaTime | Veja os detalhes abaixo; intervalo aceito `[1970-01-01, 2106-02-07 06:28:15]` UTC |
| DateTime32 | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos do NodaTime | Igual a DateTime |
| DateTime64 | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos do NodaTime | Precisão determinada pelo parâmetro Scale |
| Time | `TimeSpan`, `TimeOnly`, `int` | Limitado a ±999:59:59; `int` interpretado como segundos |
| Time64 | `TimeSpan`, `TimeOnly`, `decimal`, `double`, `float`, `int`, `long`, `string` | `string` interpretada no formato `[-]HHH:MM:SS[.fraction]`; limitado a ±999:59:59.999999999 |

<Note>
  **Valores fora do intervalo**

  Na gravação binária, valores de `Date`, `Date32`, `DateTime` e `DateTime32` fora dos respectivos intervalos aceitos geram `ArgumentOutOfRangeException` no momento da chamada a `Write`. A exceção informa o tipo da coluna e o intervalo aceito. Antes, valores fora do intervalo podiam ser truncados silenciosamente ao passar por um inteiro de 32 bits e reinterpretados pelo servidor, produzindo timestamps válidos, mas incorretos.
</Note>

O driver respeita `DateTime.Kind` ao gravar valores:

| DateTime.Kind | Parâmetros HTTP | Em lote |
| - | - | - |
| Utc | Instante preservado | Instante preservado |
| Local | Instante preservado | Instante preservado |
| Unspecified | Interpretado como horário civil no fuso horário do tipo do parâmetro (UTC por padrão) | Interpretado como horário civil no fuso horário da coluna |

Os valores de `DateTimeOffset` sempre preservam o instante exato.

**Exemplo: DateTime em UTC (instante preservado)**

```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
```

**Exemplo: DateTime não especificado (hora local)**

```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
```

**Recomendação:** para ter um comportamento mais simples e previsível, use `DateTimeKind.Utc` ou `DateTimeOffset` em todas as operações com DateTime. Assim, seu código funcionará de forma consistente, independentemente dos fusos horários do servidor, do cliente e da coluna.

<h4 id="datetime-http-param-vs-bulkcopy">
  Parâmetros HTTP vs cópia em massa
</h4>

Há uma diferença importante entre a vinculação de parâmetros HTTP e a cópia em massa ao gravar valores DateTime `Unspecified`:

**A cópia em massa** conhece o fuso horário da coluna de destino e interpreta corretamente os valores `Unspecified` nesse fuso horário.

**Os parâmetros HTTP** não identificam automaticamente o fuso horário da coluna. Você precisa especificá-lo na anotação de tipo SQL:

```csharp theme={null}
// CORRECT: Timezone in SQL type hint - type is extracted automatically
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// INCORRECT: Without timezone hint, interpreted as UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// String value "2024-01-15 14:30:00" interpreted as UTC, not Amsterdam time!
```

| `DateTime.Kind` | Coluna de destino | Parâmetro HTTP (com informação de fuso horário) | Parâmetro HTTP (sem informação de fuso horário) | Cópia em massa |
| - | - | - | - | - |
| `Utc` | UTC | Instante preservado | Instante preservado | Instante preservado |
| `Utc` | Europe/Amsterdam | Instante preservado | Instante preservado | Instante preservado |
| `Local` | Qualquer | Instante preservado | Instante preservado | Instante preservado |
| `Unspecified` | UTC | Tratado como UTC | Tratado como UTC | Tratado como UTC |
| `Unspecified` | Europe/Amsterdam | Tratado como horário de Amsterdã | **Tratado como UTC** | Tratado como horário de Amsterdã |

***

<h4 id="type-map-writing-decimal">
  Tipos decimais
</h4>

| Tipo do ClickHouse | Tipos .NET aceitos | Observações |
| - | - | - |
| Decimal(P,S) | `decimal`, `ClickHouseDecimal`, qualquer tipo compatível com `Convert.ToDecimal()` | Lança `OverflowException` se a precisão for excedida |
| Decimal32 | `decimal`, `ClickHouseDecimal`, qualquer tipo compatível com `Convert.ToDecimal()` | Precisão máxima de 9 |
| Decimal64 | `decimal`, `ClickHouseDecimal`, qualquer tipo compatível com `Convert.ToDecimal()` | Precisão máxima de 18 |
| Decimal128 | `decimal`, `ClickHouseDecimal`, qualquer tipo compatível com `Convert.ToDecimal()` | Precisão máxima de 38 |
| Decimal256 | `decimal`, `ClickHouseDecimal`, qualquer tipo compatível com `Convert.ToDecimal()` | Precisão máxima de 76 |

***

<h4 id="type-map-writing-json">
  Tipo JSON
</h4>

| Tipo do ClickHouse | Tipos .NET aceitos | Observações |
| - | - | - |
| Json | `string`, `JsonObject`, `JsonNode`, qualquer objeto | O comportamento depende da configuração `JsonWriteMode` |

O comportamento da gravação de JSON é controlado pela configuração `JsonWriteMode`:

| Tipo de entrada | `JsonWriteMode.String` (padrão) | `JsonWriteMode.Binary` |
| - | - | - |
| `string` | Enviado sem alterações | Lança `ArgumentException` |
| `JsonObject` | Serializado com `ToJsonString()` | Lança `ArgumentException` |
| `JsonNode` | Serializado com `ToJsonString()` | Lança `ArgumentException` |
| POCO registrado | Serializado com `JsonSerializer.Serialize()` | Codificação binária com dicas de tipo; oferece suporte a atributos de caminho personalizados |
| POCO não registrado / objeto anônimo | Serializado com `JsonSerializer.Serialize()` | Lança `ClickHouseJsonSerializationException` |

* **`String` (padrão)**: Aceita `string`, `JsonObject`, `JsonNode` ou qualquer objeto. Todas as entradas são serializadas com `System.Text.Json.JsonSerializer` e enviadas como strings JSON para parsing no lado do servidor. É o modo mais flexível e funciona sem registro de tipos.

* **`Binary`**: Aceita apenas tipos POCO registrados. Os dados são convertidos no lado do cliente para o formato JSON binário do ClickHouse, com suporte completo a dicas de tipo. É necessário chamar `connection.RegisterJsonSerializationType<T>()` antes de usar esse modo. A gravação de valores `string` ou `JsonNode` nesse modo lança `ArgumentException`.

```csharp theme={null}
// Default String mode works with any input
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// Binary mode requires explicit opt-in and type registration
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
```

<h5 id="json-typed-columns">
  Colunas JSON tipadas
</h5>

Quando uma coluna JSON tem dicas de tipo (por exemplo, `JSON(id UInt64, price Decimal128(2))`), o driver as utiliza para serializar os valores sem perder informações de tipo. Isso evita a perda de precisão em tipos como `UInt64`, `Decimal`, `UUID` e `DateTime64`, que ocorreria se fossem serializados como JSON genérico.

<h5 id="json-poco-serialization">
  Serialização de POCOs
</h5>

Os POCOs podem ser gravados em colunas JSON de duas maneiras, dependendo do `JsonWriteMode`:

**Modo String (padrão)**: os POCOs são serializados por meio de `System.Text.Json.JsonSerializer`. Não é necessário registrar os tipos. Essa é a abordagem mais simples e funciona com objetos anônimos.

**Modo Binary**: os POCOs são serializados no formato JSON binário do driver, com suporte completo a indicações de tipo. Os tipos devem ser registrados com `connection.RegisterJsonSerializationType<T>()` antes do uso. Esse modo permite mapear caminhos JSON personalizados por meio de atributos:

* **`[ClickHouseJsonPath("path")]`**: mapeia uma propriedade para um caminho JSON personalizado. Útil para estruturas aninhadas ou quando o nome da propriedade é diferente da chave JSON desejada. **Funciona apenas no modo Binary.**

* **`[ClickHouseJsonIgnore]`**: exclui uma propriedade da serialização. **Funciona apenas no modo Binary.**

```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; }  // Not serialized
}

// For Binary mode: Register the type and enable Binary mode
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Insert POCO - serialized to JSON with nested structure via custom path attributes
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// Resulting JSON: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}
```

A correspondência entre nomes de propriedades e indicações de tipo da coluna diferencia maiúsculas de minúsculas. A propriedade `UserId` só corresponde a uma indicação definida como `UserId`, não como `userid`. Esse é o comportamento do ClickHouse, que permite que caminhos como `userName` e `UserName` coexistam como campos distintos.

**Limitações (apenas no modo Binary):**

* Os tipos POCO devem ser registrados na conexão com `connection.RegisterJsonSerializationType<T>()` antes da serialização. A tentativa de serializar um tipo não registrado lança `ClickHouseJsonSerializationException`.
* Propriedades do tipo dicionário ou array/lista precisam de indicações de tipo na definição da coluna para que sejam serializadas corretamente. Sem essas indicações, use o modo String.
* Valores nulos em propriedades POCO só são gravados quando o caminho tem uma indicação de tipo `Nullable(T)` na definição da coluna. O ClickHouse não permite tipos `Nullable` em caminhos JSON dinâmicos; por isso, propriedades nulas sem indicação de tipo são ignoradas.
* Os atributos `ClickHouseJsonPath` e `ClickHouseJsonIgnore` são ignorados no modo String (funcionam apenas no modo Binary).

***

<h4 id="type-map-writing-other">
  Outros tipos
</h4>

| Tipo do ClickHouse | Tipos .NET aceitos | Observações |
| - | - | - |
| UUID | `Guid`, `string` | A string é interpretada como Guid |
| IPv4 | `IPAddress`, `string` | Deve ser IPv4; a string é interpretada por `IPAddress.Parse()` |
| IPv6 | `IPAddress`, `string` | Deve ser IPv6; a string é interpretada por `IPAddress.Parse()` |
| Nothing | Qualquer tipo | Não grava nada (operação sem efeito) |
| Dynamic | — | **Não há suporte** (lança `NotImplementedException`) |
| Array(T) | `IList`, `null` | Um valor nulo grava um array vazio. Para tipos aninhados (`Array(Array(T))` e níveis mais profundos), são aceitos tanto arrays irregulares (`T[][]`, `List<List<T>>`) quanto arrays CLR multidimensionais retangulares (`T[,]`, `T[,,]`, …); o número de dimensões do array CLR deve corresponder à profundidade de aninhamento no ClickHouse. |
| Tuple(T1, T2, ...) | `ITuple`, `IList` | O número de elementos deve corresponder à aridade da tupla. Consulte a [ressalva sobre ValueTuple](#valuetuple-caveat) para mais de 7 elementos. |
| Map(K, V) | `IDictionary`, `IEnumerable<KeyValuePair<K, V>>` | Uma sequência de pares (por exemplo, o `List<KeyValuePair<K, V>>` produzido por `MapReadMode=KeyValuePairs`) é aceita em qualquer modo de leitura e pode conter chaves repetidas. Aplica-se a inserts binários e a parâmetros de consulta |
| Nullable(T) | `null`, `DBNull` ou tipos aceitos por T | Grava o byte indicador de nulo antes do valor |
| Enum8 | `string`, `sbyte`, tipos numéricos | A string é buscada no dicionário do enum |
| Enum16 | `string`, `short`, tipos numéricos | A string é buscada no dicionário do enum |
| LowCardinality(T) | Tipos aceitos por T | Delega ao tipo subjacente |
| SimpleAggregateFunction | Tipos aceitos pelo tipo subjacente | Delega ao tipo subjacente |
| Nested(...) | `IList` de tuplas | O número de elementos deve corresponder ao número de campos |
| Variant(T1, T2, ...) | Valor correspondente a um dos tipos T1, T2, ... | Lança `ArgumentException` se não houver correspondência de tipo |
| QBit(T, dim) | `IList` | Delega a Array; a dimensão é apenas um metadado |

***

<h4 id="type-map-writing-geometry">
  Tipos geométricos
</h4>

| Tipo do ClickHouse | Tipos .NET aceitos | Observações |
| - | - | - |
| Point | `System.Drawing.Point`, `ITuple`, `IList` (2 elementos) | |
| Ring | `IList` de Point | |
| LineString | `IList` de Point | |
| Polygon | `IList` de Ring | |
| MultiLineString | `IList` de LineString | |
| MultiPolygon | `IList` de Polygon | |
| Geometry | Qualquer um dos tipos geométricos acima | Variante de todos os tipos geométricos |

***

<h4 id="type-map-writing-not-supported">
  Sem suporte para gravação
</h4>

| Tipo do ClickHouse | Observações |
| - | - |
| Dynamic | Lança `NotImplementedException` |
| AggregateFunction | Lança `AggregateFunctionException` |

***

<h3 id="nested-type-handling">
  Tratamento de tipos aninhados
</h3>

Os tipos aninhados do ClickHouse (`Nested(...)`) podem ser lidos e gravados com a semântica de arrays.

```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">
  Logging e diagnóstico
</h2>

O cliente .NET do ClickHouse usa as abstrações de `Microsoft.Extensions.Logging` para oferecer logging leve, ativado apenas quando configurado. Quando habilitado, o driver emite mensagens estruturadas sobre eventos do ciclo de vida da conexão, execução de comandos, operações de transporte e operações de insert em massa. O logging é totalmente opcional: aplicações que não configuram um logger continuam funcionando sem sobrecarga adicional.

<h3 id="logging-quick-start">
  Início rápido
</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">
  Usando appsettings.json
</h4>

Você pode configurar os níveis de logging com a configuração padrão do .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">
  Usando uma configuração em memória
</h4>

Você também pode definir no código o nível de detalhamento do logging para cada categoria:

```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">
  Categorias e emissores
</h3>

O driver usa categorias específicas para que você possa ajustar os níveis de log de cada componente:

| Categoria | Origem | Destaques |
| - | - | - |
| `ClickHouse.Driver.Connection` | `ClickHouseConnection` | Ciclo de vida da conexão, seleção da fábrica de clientes HTTP, abertura e fechamento de conexões e gerenciamento de sessões. |
| `ClickHouse.Driver.Command` | `ClickHouseCommand` | Início e conclusão da execução de consultas, tempos de execução, IDs das consultas, estatísticas do servidor e detalhes de erros. |
| `ClickHouse.Driver.Transport` | `ClickHouseConnection` | Requisições de streaming HTTP de baixo nível, flags de compressão, códigos de status das respostas e falhas de transporte. |
| `ClickHouse.Driver.Client` | `ClickHouseClient` | Inserts binários, consultas e outras operações |
| `ClickHouse.Driver.NetTrace` | `TraceHelper` | Rastreamento de rede, apenas quando o modo de depuração está habilitado |

<h4 id="logging-config-example">
  Exemplo: Como diagnosticar problemas de conexão
</h4>

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

Isso será registrado nos logs:

* Seleção da fábrica de clientes HTTP (pool padrão ou conexão única)
* Configuração do handler HTTP (SocketsHttpHandler ou HttpClientHandler)
* Configurações do pool de conexões (MaxConnectionsPerServer, PooledConnectionLifetime etc.)
* Configurações de tempo limite (ConnectTimeout, Expect100ContinueTimeout etc.)
* Configuração de SSL/TLS
* Eventos de abertura e fechamento de conexões
* Rastreamento do ID da sessão

<h3 id="logging-debugmode">
  Modo de depuração: rastreamento de rede e diagnóstico
</h3>

Para facilitar o diagnóstico de problemas de rede, a biblioteca do driver inclui um processo auxiliar que permite rastrear, em baixo nível, os componentes internos de rede do .NET. Para habilitar esse recurso, você deve passar uma LoggerFactory com o nível definido como Trace e definir EnableDebugMode como true (ou habilitá-lo manualmente por meio da classe `ClickHouse.Driver.Diagnostic.TraceHelper`). Os eventos serão registrados na categoria `ClickHouse.Driver.NetTrace`. Aviso: isso gerará logs extremamente detalhados e afetará o desempenho. Não é recomendável habilitar o modo de depuração em produção.

```csharp theme={null}
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Must be Trace level to see network events
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Enable low-level network tracing
};
```

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

O driver oferece suporte nativo ao rastreamento distribuído com OpenTelemetry por meio da API [`System.Diagnostics.Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing) do .NET. Quando habilitado, o driver gera spans para operações de banco de dados, que podem ser exportados para plataformas de observabilidade como o Jaeger ou o próprio ClickHouse (por meio do [OpenTelemetry Collector](/pt-BR/guides/use-cases/observability/build-your-own/integrating-opentelemetry)).

<h3 id="opentelemetry-enabling">
  Habilitando o rastreamento
</h3>

Em aplicações ASP.NET Core, adicione o `ActivitySource` do driver do ClickHouse à configuração do OpenTelemetry:

```csharp theme={null}
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Subscribe to ClickHouse driver spans
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // Or AddJaegerExporter(), etc.
```

Para aplicativos de console, testes ou configuração manual:

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

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

<h3 id="opentelemetry-attributes">
  Atributos dos spans
</h3>

Cada span inclui atributos padrão do OpenTelemetry para bancos de dados, além de estatísticas de consulta específicas do ClickHouse que podem ajudar na depuração.

| Atributo | Descrição |
| - | - |
| `db.system` | Sempre `"clickhouse"` |
| `db.name` | Nome do banco de dados |
| `db.user` | Nome de usuário |
| `db.statement` | Consulta SQL (se habilitada) |
| `db.clickhouse.read_rows` | Linhas lidas pela consulta |
| `db.clickhouse.read_bytes` | Bytes lidos pela consulta |
| `db.clickhouse.written_rows` | Linhas gravadas pela consulta |
| `db.clickhouse.written_bytes` | Bytes gravados pela consulta |
| `db.clickhouse.elapsed_ns` | Tempo de execução no lado do servidor, em nanossegundos |

<h3 id="opentelemetry-configuration">
  Opções de configuração
</h3>

Configure o comportamento do rastreamento com `ClickHouseDiagnosticsOptions`:

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

// Include SQL statements in spans (default: false for security)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Truncate long SQL statements (default: 1000 characters)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;
```

<Warning>
  Ativar `IncludeSqlInActivityTags` pode expor dados confidenciais em seus traces. Use essa opção com cautela em ambientes de produção.
</Warning>

<h2 id="tls-configuration">
  Configuração de TLS
</h2>

Ao se conectar ao ClickHouse via HTTPS, você pode configurar o funcionamento do TLS/SSL de diferentes maneiras.

<h3 id="custom-certificate-validation">
  Validação personalizada de certificados
</h3>

Em ambientes de produção que exigem uma lógica personalizada para validar certificados, forneça uma instância própria de `HttpClient` com o handler `ServerCertificateCustomValidationCallback` configurado:

```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>
  Considerações importantes ao fornecer um HttpClient personalizado

  * **Descompressão automática**: mantenha `AutomaticDecompression` desativado. O driver descomprime as respostas por conta própria, portanto essa opção não é necessária — e ativá-la pode causar problemas no envio de requisições: ao enviar uma requisição, o handler *também adiciona* todos os algoritmos de sua máscara ao `Accept-Encoding` de saída, ampliando a lista anunciada pelo driver. Com isso, o ClickHouse pode responder usando um codec que você não solicitou. Consulte [Descompressão de respostas](#response-decompression).
  * **Tempo limite de inatividade**: configure `PooledConnectionIdleTimeout` com um valor menor que o `keep_alive_timeout` do servidor (10 segundos no ClickHouse Cloud) para evitar erros causados por conexões semiabertas.
</Note>

<h2 id="performance-tuning">
  Ajuste de desempenho
</h2>

Esta seção explica como usar o cliente para obter o melhor desempenho possível e quais opções ajustar para otimizar seu desempenho no seu caso de uso.

<h3 id="perf-at-a-glance">
  Visão geral
</h3>

\| Se você | Faça isto |
\|---|---|---|
\| Lê linhas como POCOs | Use [`QueryAsync<T>`](#perf-read-path), não `MapTo<T>` |
\| Insere grandes volumes de dados | Aumente [`InsertOptions.BatchSize`](#perf-insert-batching) |
\| Executa um aplicativo de console ou worker que faz muitas inserções | Ative o [Server GC](#perf-gc) |
\| Lê grandes volumes de resultados pela rede | Mantenha a compressão da resposta ativada (como é o padrão) |
\| Insere dados por uma conexão rápida | Experimente [`InsertOptions.Compressor = null`](#perf-compression) |
\| Insere dados repetidamente na mesma tabela | Use [`UseSchemaCache` ou `ColumnTypes`](#skip-schema-query) |
\| Lê volumes muito grandes de resultados | Aumente [`ReadBufferSize`](#perf-buffers) |

***

<h3 id="perf-read-path">
  Leitura: escolha o caminho de materialização
</h3>

Há três maneiras de obter uma linha de um resultado, e elas têm custos diferentes. Alguns caminhos encapsulam os valores em objetos, aumentando as alocações e reduzindo o desempenho.

| Como os dados são lidos | Encapsula cada valor | Observações |
| - | - | - |
| `QueryAsync<T>` | **Não** | Lê diretamente do fluxo para as propriedades. É o caminho mais rápido. |
| Acessores tipados do leitor (`GetInt32`, `GetInt64`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetFieldValue<T>`) | **Não** | Lê valores de um armazenamento tipado sem encapsulá-los em objetos. |
| `MapTo<T>` | Sim | Primeiro materializa a linha e depois copia seus valores. |
| `GetValue` e `GetValues` | Sim | Retornam `object`, por isso o valor precisa ser encapsulado em um objeto. |

Para a leitura de 1,000,000 linhas e 105 colunas do conjunto de dados *hits*:

| API | Alocado |
| - | -: |
| `QueryAsync<T>` | **1,372 MB** |
| `MapTo<T>` | 3,133 MB |

```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>
  *Os ORMs usam o caminho otimizado quando empregam acessores tipados.* O linq2db registra `GetInt64`,
  `GetDouble` e `GetDateTime` para cada coluna, permitindo a leitura sem boxing. O código que lê por meio de
  `GetValue` (inclusive resultados `dynamic` do Dapper) faz boxing de cada valor. Se uma consulta de ORM for executada com frequência
  e usar `GetValue` para leitura, use `QueryAsync<T>` para essa consulta específica.
</Note>

***

<h3 id="perf-insert-batching">
  Inserção: tamanho do lote e paralelismo
</h3>

O tamanho do lote é o fator que mais afeta a vazão de inserção. O valor padrão de `InsertOptions.BatchSize` é
100.000 linhas.

**Use lotes grandes.** Em uma inserção de 1.000.000 de linhas, aumentar o tamanho de cada lote de 10.000 para 100.000 linhas
produziu os seguintes resultados:

| Inserção | 10.000 linhas/lote | 100.000 linhas/lote | |
| - | -: | -: | -: |
| POCO | 15.308 ms | 7.853 ms | −49% |
| `object[]` | 17.027 ms | 10.671 ms | −37% |

Se não for possível controlar o tamanho do lote (por exemplo, quando vários produtores enviam pequenas quantidades de linhas de forma independente), use [insert assíncrono](#async-inserts) e deixe que o servidor agrupe as linhas em lotes.

**Envios em paralelo.** O valor padrão de `InsertOptions.MaxDegreeOfParallelism` é `1`. Aumente-o para enviar
lotes simultaneamente. Isso é especialmente útil quando a compressão está ativada, pois cada lote é comprimido
em uma thread própria. As sessões não são compatíveis com inserts paralelos: desative-as ou mantenha
`MaxDegreeOfParallelism = 1`.

**Elimine a consulta preliminar ao schema.** Cada chamada a `InsertBinaryAsync` envia primeiro uma consulta `SELECT ... WHERE 1=0`
para identificar os tipos de coluna. Consulte [Como dispensar a consulta preliminar ao schema](#skip-schema-query) para evitar essa
ida e volta com `ColumnTypes` ou `UseSchemaCache`.

<Note>
  O caminho de inserção sem boxing se aplica ao formato padrão `RowBinary`. `RowBinaryWithDefaults` precisa
  examinar cada valor para encontrar o marcador `DBDefault` e, por isso, usa o caminho mais lento.
</Note>

***

<h3 id="perf-compression">
  Compressão: os resultados variam conforme a direção
</h3>

A compressão reduz a quantidade de bytes transferidos em troca de maior uso de CPU. Se vale a pena depende da direção da
transferência, da largura de banda da conexão com o servidor ClickHouse, do desempenho do algoritmo de compressão escolhido com seus dados e de você pagar ou não por byte transferido.

**Leituras:** mantenha a compressão ativada, a menos que o servidor esteja na mesma máquina. Essa é a configuração padrão. Em comparação com a transferência sem compressão, `zstd` no nível 1
apresentou:

| Do cliente para o servidor | Efeito da compressão |
| - | - |
| Mesmo host (loopback) | Custo 8% |
| Mesma região de nuvem | **Economia de 16%** |
| A uma região de distância | **Economia de 33%** |

**Inserts:** meça o desempenho antes de ativar a compressão. A economia pode não ser suficiente para justificar seu uso. Lembre-se também de que a descompressão aumenta a carga no servidor; esse aumento é moderado com Zstd e LZ4, mas pode ser elevado com outros algoritmos (por exemplo, Brotli).

Para desativar a compressão na inserção:

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

Para saber mais sobre a escolha do codec, os níveis de compressão e como identificar o ponto de virada no seu caso, consulte
[Ajuste da compressão](#tuning-compression).

***

<h3 id="perf-buffers">
  Buffers
</h3>

`ReadBufferSize` define o tamanho do buffer usado para ler respostas HTTP. O valor padrão é 64 KiB.

O driver obtém esse buffer de um pool compartilhado e o devolve ao liberar o leitor. Assim, não há
uma alocação para cada consulta. Aumente esse valor para reduzir o número de vezes que o buffer precisa
ser reabastecido ao processar resultados grandes. O driver mantém um buffer para cada leitor aberto
simultaneamente; por isso, o uso de memória aumenta com o tamanho do buffer e o número de leitores simultâneos.

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

<Warning>
  *Sempre libere os leitores.* Ao liberar um leitor, ele devolve o buffer ao pool e libera a conexão HTTP.
  Abandonar um leitor não devolve o buffer ao pool e pode deixar a conexão HTTP
  indisponível; a coleta de lixo convencional não substitui a liberação do leitor.
</Warning>

***

<h3 id="perf-gc">
  Ambiente de execução e coleta de lixo
</h3>

**Ative o Server GC para aplicações que realizam muitas operações de insert.** Com o mesmo código e a mesma quantidade de
bytes alocados, o Workstation GC foi até 97% mais lento nas operações de insert do que o Server GC.

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

Os projetos ASP.NET Core já usam essa configuração. Aplicativos de console, serviços worker e a maioria das imagens
de contêiner não a usam.

A causa está no tamanho do orçamento da geração 0. O Workstation GC usa um orçamento pequeno, por isso os
buffers de curta duração criados por uma inserção não são liberados na geração 0. Em vez disso, passam para a geração 1,
o que aumenta as promoções e gera muito mais trabalho na geração 2. Em um teste de inserção,
houve 4.000 coletas da geração 2 a cada 1.000 operações com o Server GC e 73.000 com o
Workstation GC.

<Note>
  O Server GC é uma configuração voltada à vazão, não à latência. Nas mesmas medições, o Server GC passou
  menos da metade do tempo total em pausas, mas cada pausa durou mais
  (114,6 ms no 95º percentil, contra 61,9 ms). Se o seu serviço for sensível à latência das requisições mais lentas, avalie
  os dois modos antes de escolher.
</Note>

***

<h3 id="perf-latency">
  Latência: reutilize conexões
</h3>

Estabelecer uma nova conexão TCP e realizar o handshake TLS leva um tempo considerável.
Reutilizar conexões reduz significativamente a latência das consultas.

* Não crie um cliente para cada requisição. Cada novo cliente com seu próprio `HttpClient` cria um novo
  pool de conexões e precisa realizar o handshake novamente. Use um único `ClickHouseClient` durante todo o ciclo de vida da aplicação. Ele é thread-safe e foi projetado para
  ser usado como singleton.
* Para ADO.NET e ORMs, use `ClickHouseDataSource` para que todas as conexões compartilhem o mesmo pool.

Para conhecer todos os padrões, consulte
[Ciclo de vida das conexões e pooling](#best-practices-connection-lifetime).

***

<h3 id="perf-measuring">
  Meça o desempenho por conta própria
</h3>

Em muitos casos, o desempenho dependerá da estrutura dos seus dados, da velocidade da conexão com o servidor,
de como você distribui o uso de CPU entre o cliente e o servidor, das limitações do seu hardware etc.
Por isso, recomendamos que você meça o desempenho no seu ambiente, usando seus próprios dados.

Para verificar a parte do trabalho realizada pelo servidor, defina `QueryOptions.QueryId` e consulte os contadores:

```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">
  Suporte a ORMs
</h2>

Os ORMs exigem a API ADO.NET (`ClickHouseConnection`). Para gerenciar corretamente o ciclo de vida das conexões, crie-as a partir de um `ClickHouseDataSource`:

```csharp theme={null}
// Register DataSource as singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// Create connections for ORM use
await using var connection = await dataSource.OpenConnectionAsync();
// Pass connection to your ORM...
```

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

`ClickHouse.Driver` é compatível com o Dapper. O driver converte automaticamente a sintaxe `@parameter` do Dapper para a sintaxe nativa `{parameter:Type}` do ClickHouse, inferindo os tipos a partir dos valores do .NET.

Use `ClickHouseDataSource` para gerenciar corretamente o ciclo de vida das conexões:

```csharp theme={null}
var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Register as singleton in DI

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

<h4 id="dapper-parameter-passing">
  Formas de passar parâmetros
</h4>

O Dapper oferece suporte a todas as formas padrão de passar parâmetros:

**Objetos anônimos:**

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

**Classes 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);
```

**Dicionário:**

```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` (de um dicionário ou de um objeto anônimo):**

```csharp theme={null}
var dynParams = new DynamicParameters(new { Id = 1 });
// or: 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">
  Consultas com resultados em POCOs
</h4>

O Dapper mapeia as colunas para as propriedades pelo nome (sem diferenciar maiúsculas de minúsculas):

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

// From a table
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// From a literal
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();
```

<h4 id="dapper-clickhouse-param-syntax">
  Sintaxe nativa de parâmetros do ClickHouse
</h4>

Quando precisar controlar explicitamente os tipos, use a sintaxe `{param:Type}` do ClickHouse diretamente no SQL, com um `Dictionary<string, object>` para os valores dos parâmetros. Não combine as sintaxes `@param` e `{param:Type}` para o mesmo parâmetro.

```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>

**A expansão nativa de IN do Dapper funciona:**

```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 } });
```

O Dapper reescreve isso como `WHERE id IN (@Ids1, @Ids2, @Ids3)`, e o driver converte cada parâmetro expandido.

**A função `has()` do ClickHouse também funciona com um parâmetro 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">
  Handlers de tipo personalizados
</h4>

Alguns tipos do ClickHouse, como `ITuple`, `BigInteger` e `ClickHouseDecimal`, precisam ter seus handlers registrados na inicialização:

```csharp theme={null}
// ClickHouseDecimal (for Decimal64/128/256 columns)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger (for Int128/Int256/UInt128/UInt256 columns)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (for IPv4/IPv6 columns)
SqlMapper.AddTypeHandler(new IpAddressHandler());
```

Consulte o [exemplo do Dapper](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Http/ORM/ORM_001_Dapper.cs) para ver como implementar um handler de tipo.

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

`GetAll<T>()` e `Get<T>(id)` funcionam. Já `Insert<T>()` não funciona: gera sintaxe específica do SQL Server (`SCOPE_IDENTITY`, `[]`). Em vez dele, recomenda-se usar o método nativo `InsertBinaryAsync` de `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);
```

Os nomes das propriedades devem corresponder exatamente aos nomes das colunas do ClickHouse (inclusive quanto ao uso de maiúsculas e minúsculas).

<h4 id="dapper-limitations">
  Limitações
</h4>

| Item | Status | Detalhes |
| - | - | - |
| Tupla como **resultado** | Funciona | Requer o registro de `SqlMapper.TypeHandler<ITuple>` |
| Tupla como **parâmetro** | Não há suporte | O Dapper não consegue serializar `ITuple`/`Tuple<>` como valor de `DbParameter` |
| Tipos aninhados como parâmetro | Não há suporte | Pelo mesmo motivo: o Dapper rejeita tipos complexos como valores de parâmetro |
| Tipos geoespaciais como parâmetro | Não há suporte | Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon |
| `Dapper.Contrib.Insert<T>()` | Não há suporte | Gera sintaxe específica do SQL Server |
| Tipo `Nothing` | Não há suporte | Não há uma representação equivalente em .NET |

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

Este driver é compatível com o [linq2db](https://github.com/linq2db/linq2db), um ORM leve e provedor LINQ para .NET. Consulte o site do projeto para obter a documentação completa.

**Exemplo de uso:**

Crie uma `DataConnection` com o provedor do 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);
```

Os mapeamentos de tabelas podem ser definidos por meio de atributos ou de uma configuração fluente. Se os nomes das classes e propriedades corresponderem exatamente aos nomes das tabelas e colunas, não será necessária nenhuma configuração:

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

**Consultas:**

```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();
```

**Cópia em massa:**

Use `BulkCopyAsync` para realizar inserções em massa com eficiência.

```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>

O provedor oficial de Entity Framework Core para ClickHouse. Mapeie classes C# para tabelas do ClickHouse, faça consultas com LINQ e insira dados usando `SaveChanges` — tudo com os recursos familiares do EF Core.

* **NuGet**: [`ClickHouse.EntityFrameworkCore`](https://www.nuget.org/packages/ClickHouse.EntityFrameworkCore)
* **Código-fonte**: [GitHub](https://github.com/ClickHouse/ClickHouse.EntityFrameworkCore)

<Note>
  Este provedor está em desenvolvimento ativo. O lançamento atual oferece suporte a consultas LINQ (incluindo junções, subconsultas e operações de conjunto), `INSERT` via `SaveChanges` / `BulkInsertAsync`, migrações com DDL completo (CREATE / ALTER / DROP) e configuração de motores de tabela específicos do ClickHouse. `UPDATE` / `DELETE` não têm suporte.
</Note>

<h4 id="ef-core-installation">
  Instalação
</h4>

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

Requer .NET 10.0 e EF Core 10.

<h4 id="ef-core-quick-start">
  Início rápido
</h4>

Defina sua entidade e o `DbContext` e faça consultas com 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");
}

// Query
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">
  Tipos compatíveis
</h4>

| Categoria | Tipos do ClickHouse | Tipos do CLR |
| - | - | - |
| **Inteiros** | `Int8`–`Int64`, `UInt8`–`UInt64` | `sbyte`, `short`, `int`, `long`, `byte`, `ushort`, `uint`, `ulong` |
| **Inteiros de alta precisão** | `Int128`, `Int256`, `UInt128`, `UInt256` | `BigInteger` |
| **Números de ponto flutuante** | `Float32`, `Float64`, `BFloat16` | `float`, `double` |
| **Decimais** | `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` | `decimal` ou `ClickHouseDecimal` |
| **Booleanos** | `Bool` | `bool` |
| **Strings** | `String`, `FixedString(N)` | `string` |
| **Enumerações** | `Enum8(...)`, `Enum16(...)` | `string` ou `enum` do C# |
| **Data e hora** | `Date`, `Date32`, `DateTime`, `DateTime64(P, 'TZ')` | `DateOnly`, `DateTime` |
| **Hora** | `Time`, `Time64(N)` | `TimeSpan` |
| **UUID** | `UUID` | `Guid` |
| **Rede** | `IPv4`, `IPv6` | `IPAddress` |
| **Arrays** | `Array(T)` | `T[]`, `List<T>`, `IList<T>`, `ICollection<T>`, `IReadOnlyList<T>`, `IReadOnlyCollection<T>`, `IEnumerable<T>` |
| **Mapas** | `Map(K, V)` | `Dictionary<K,V>` |
| **Tuplas** | `Tuple(T1, ...)` | `Tuple<...>` ou `ValueTuple<...>` |
| **Variantes** | `Variant(T1, T2, ...)` | `object` |
| **Tipos dinâmicos** | `Dynamic` | `object` |
| **JSON** | `Json` | `JsonNode` ou `string` |
| **Tipos geográficos** | `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` | `Tuple<double,double>` e arrays desse tipo; `object` para Geometry |
| **Tipos de encapsulamento** | `Nullable(T)`, `LowCardinality(T)` | Desencapsulados automaticamente |

Use `ClickHouseDecimal` (de `ClickHouse.Driver.Numerics`) em vez de `decimal` quando precisar da precisão total das colunas `Decimal128`/`Decimal256` — o tipo `decimal` do .NET comporta apenas 28 a 29 dígitos significativos.

<h4 id="ef-core-linq">
  Operações LINQ compatíveis
</h4>

**Consultas:** `Where`, `OrderBy`, `Take`, `Skip`, `Select`, `First`, `Single`, `Any`, `All`, `Count`, `Distinct`, `AsNoTracking`

**GROUP BY e agregações:** `GroupBy` com `Count`, `LongCount`, `Sum`, `Average`, `Min`, `Max` — incluindo `HAVING` (`.Where()` após `.GroupBy()`), várias agregações em uma única projeção e `OrderBy` aplicado aos resultados das agregações.

**Junções:** `Join` (INNER), padrões `GroupJoin`/`SelectMany` (LEFT e CROSS). LEFT JOIN retorna `null` de fato para linhas sem correspondência (veja [semântica de valores nulos em LEFT JOIN](#ef-core-join-nulls) abaixo).

**Subconsultas:** `Contains` / `IN` correlacionados, `Any` / `EXISTS`, `All` e subconsultas escalares em projeções.

**Operações de conjunto:** `Concat` (→ `UNION ALL`), `Union` (→ `UNION DISTINCT`), `Intersect`, `Except`.

**Coleções locais embutidas:** junções e `Contains` com coleções em memória (`int[]`, `List<T>` etc.) são convertidos em uma sequência de operações UNION.

**Métodos de string:** `Contains`, `StartsWith`, `EndsWith`, `IndexOf`, `Replace`, `Substring`, `Trim`/`TrimStart`/`TrimEnd`, `ToLower`, `ToUpper`, `Length`, `IsNullOrEmpty`, `Concat` (e o operador `+`).

**Funções matemáticas:** métodos padrão de `Math` e `MathF` convertidos para funções equivalentes no ClickHouse — aritméticas, logarítmicas, trigonométricas e utilitárias.

<h5 id="ef-core-join-nulls">
  Semântica de valores nulos em LEFT JOIN
</h5>

O provedor inclui automaticamente `set_join_use_nulls=1` em todos os caminhos de conexão para que o comportamento de JOIN corresponda ao esperado pelo Entity Framework.

Se o servidor ClickHouse ou o perfil não permitir alterar essa configuração (por exemplo, um perfil `readonly=1`), desative esse comportamento com:

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

Com a opção de desativação habilitada, LEFT JOIN retorna os valores padrão das colunas do ClickHouse, e a detecção de navegação do EF baseada em valores nulos deixa de funcionar como esperado. Use comparações explícitas com `0` / `""` em vez de `== null`.

<h4 id="ef-core-insert">
  Inserção de dados
</h4>

`SaveChanges` usa a API nativa `InsertBinaryAsync` do driver, com codificação RowBinary e corpo da requisição comprimido — uma abordagem muito mais eficiente do que SQL parametrizado:

```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();
```

As entidades passam de `Added` para `Unchanged` após serem salvas, como ocorre com qualquer outro provedor do EF Core.

O **tamanho do lote** é configurável (o padrão é 1000):

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

<h4 id="ef-core-bulk-insert">
  Inserção em lote
</h4>

Para cargas de alta vazão, use `BulkInsertAsync` em vez de `SaveChanges`. Esse método de extensão de `DbContext` dispensa totalmente o rastreamento de alterações, a resolução de identidade e o gerenciamento de estado do EF Core — ele chama diretamente o `InsertBinaryAsync` do driver, usando a codificação RowBinary e um corpo da requisição comprimido.

Por isso, ele é indicado para carregar grandes conjuntos de dados quando não é necessário rastrear as entidades após a inserção:

```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);
```

A entrada pode ser qualquer `IEnumerable<T>` — as entidades são processadas uma a uma, sem que seja necessário carregar todas na memória. O valor retornado é o número de linhas inseridas. Após a inserção, as entidades **não** ficam vinculadas ao `DbContext`, portanto não há transição de estado de `Added` → `Unchanged`.

<h4 id="ef-core-enums">
  Enums
</h4>

As colunas `Enum8`/`Enum16` do ClickHouse podem ser mapeadas como propriedades do tipo `string` ou como tipos `enum` do C#. Ao usar enums do C#, o provedor converte automaticamente os valores entre o enum e sua representação em string:

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

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

// Query with enum values
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();
```

<h4 id="ef-core-value-converters">
  Conversões de tipos personalizados
</h4>

O sistema `ValueConverter` do EF Core permite mapear tipos personalizados para tipos já aceitos pelo provedor. O provedor nunca recebe o tipo personalizado — o EF Core faz a conversão antes que ele chegue ao provedor.

**Conversão por propriedade:**

```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; }
}

// In 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");
```

**Classe de conversor reutilizável:**

```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] };
    }
}

// Apply to a single property:
.HasConversion<MoneyConverter>()

// Or apply to all properties of a type via conventions:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}
```

<h4 id="ef-core-column-types">
  Anotações de tipo de coluna
</h4>

Para tipos escalares como `string`, `int`, `DateTime` etc., o provedor infere automaticamente o tipo do ClickHouse. Para tipos parametrizados e wrappers, é preciso especificar o tipo do ClickHouse explicitamente.

**Usando anotações de dados (atributos):**

```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; }
}
```

**Usando a API fluente em `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)");
});
```

Encapsulamentos aninhados, como `Array(Nullable(Int32))` e `LowCardinality(Nullable(String))`, são compatíveis — o provedor remove automaticamente as camadas `Nullable` e `LowCardinality` em todos os níveis de aninhamento.

<h4 id="ef-core-variant-dynamic">
  Colunas Variant e Dynamic
</h4>

As colunas `Variant(T1, T2, ...)` e `Dynamic` do ClickHouse são mapeadas para `object` no .NET. Como `object` é genérico demais para a inferência automática de tipos, é necessário declarar explicitamente o tipo de armazenamento com `.HasColumnType()`:

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

// In OnModelCreating:
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// or:
entity.Property(e => e.Payload).HasColumnType("Dynamic");
```

Ao ler o valor, ele é desserializado automaticamente para o tipo .NET correspondente ao discriminador armazenado (por exemplo, `string`, `ulong`, `ulong[]`).

<h4 id="ef-core-json">
  Colunas JSON
</h4>

O provedor oferece suporte ao tipo de coluna `Json` do ClickHouse, que é mapeado para `System.Text.Json.Nodes.JsonNode` (principal) ou `string` (por meio de um `ValueConverter` automático):

```csharp theme={null}
using System.Text.Json.Nodes;

public class Event
{
    public long Id { get; set; }
    public JsonNode? Data { get; set; }
}

// In OnModelCreating:
entity.Property(e => e.Data).HasColumnType("Json");
```

A leitura e a gravação de JSON funcionam com `SaveChanges` e `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"
```

Se preferir strings JSON sem processamento, mapeie a propriedade como `string` com uma coluna do tipo `Json` — o provedor aplicará um `ValueConverter` automaticamente:

```csharp theme={null}
public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // raw JSON string
}

entity.Property(e => e.Data).HasColumnType("Json");
```

<Note>
  * **Sem tradução de caminhos JSON** — `entity.Data["name"]` em LINQ não é convertido para a sintaxe SQL `data.name` do ClickHouse. Filtre por colunas que não sejam JSON e examine o JSON na memória.
  * **Semântica de NULL** — O tipo JSON do ClickHouse retorna `{}` (um objeto vazio) para valores NULL, em vez de retornar NULL do SQL.
  * **Precisão dos inteiros** — O ClickHouse armazena todos os inteiros em JSON como `Int64`. Ao ler com `JsonNode`, use `GetValue<long>()` em vez de `GetValue<int>()`.
</Note>

<h4 id="ef-core-engines">
  Motores de tabela
</h4>

Configure os motores de tabela do ClickHouse e as cláusulas específicas de cada motor por meio da API fluente `ToTable(name, t => ...)`. Se nenhum motor for configurado, o provedor usará `MergeTree` por padrão, com `ORDER BY` definido a partir da chave primária da entidade.

```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"));
});
```

Famílias de motores compatíveis:

| Motor | Método fluente | Observações |
| - | - | - |
| `MergeTree` | `HasMergeTreeEngine()` | Usado por padrão quando nenhum motor é configurado |
| `ReplacingMergeTree` | `HasReplacingMergeTreeEngine("Version", "IsDeleted")` ou `HasReplacingMergeTreeEngine<T>(e => e.Version)` | As colunas Version / IsDeleted são opcionais |
| `SummingMergeTree` | `HasSummingMergeTreeEngine(…)` ou `HasSummingMergeTreeEngine<T>(e => new { … })` | As colunas a somar são opcionais |
| `AggregatingMergeTree` | `HasAggregatingMergeTreeEngine()` | — |
| `CollapsingMergeTree` | `HasCollapsingMergeTreeEngine("Sign")` ou `HasCollapsingMergeTreeEngine<T>(e => e.Sign)` | A coluna `Sign` deve ser do tipo `Int8` |
| `VersionedCollapsingMergeTree` | `HasVersionedCollapsingMergeTreeEngine("Sign", "Version")` ou `<T>(e => e.Sign, e => e.Version)` | — |
| `GraphiteMergeTree` | `HasGraphiteMergeTreeEngine("config_section")` | — |
| `Log`, `TinyLog`, `StripeLog`, `Memory` | `HasLogEngine()`, `HasTinyLogEngine()`, `HasStripeLogEngine()`, `HasMemoryEngine()` | Sem ORDER BY / PARTITION BY |

**Cláusulas do motor:** `WithOrderBy`, `WithPartitionBy`, `WithPrimaryKey`, `WithSampleBy`, `WithTtl`, `WithSettings`. Todas são aplicadas ao construtor do motor retornado por `HasXxxEngine()`.

**Recursos em nível de coluna:** `HasCodec`, `HasTtl`, `HasComment`, `HasDefault` — todos são incluídos nas migrações.

**Índices de salto de dados** — por meio de `HasIndex(...).HasSkippingIndexType(...)`:

```csharp theme={null}
modelBuilder.Entity<Event>()
    .HasIndex(e => e.UserId)
    .HasSkippingIndexType("minmax")
    .HasGranularity(4);

// Index with parameters (e.g. bloom_filter, tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);
```

Os índices padrão (que não são do tipo skipping) são ignorados sem aviso, pois não há equivalente no ClickHouse. Os índices únicos geram uma exceção, pois o ClickHouse não exige unicidade.

<h4 id="ef-core-migrations">
  Migrações
</h4>

Fluxo de trabalho padrão das migrações do EF Core:

```bash theme={null}
dotnet ef migrations add InitialCreate
dotnet ef database update
```

Operações compatíveis:

| Operação | Gera |
| - | - |
| `CREATE TABLE` | Inclui cláusula do motor, ORDER BY, PARTITION BY, SETTINGS, codecs, TTL, comentários e valores padrão das colunas |
| `ALTER TABLE ADD COLUMN` | — |
| `ALTER TABLE DROP COLUMN` | — |
| `ALTER TABLE MODIFY COLUMN` | Trata alterações de tipo e a adição ou remoção de anotações (CODEC, TTL, COMMENT, DEFAULT) |
| `ALTER TABLE RENAME COLUMN` | — |
| `RENAME TABLE` | — |
| `ALTER TABLE ADD INDEX` / `DROP INDEX` | Apenas índices de salto de dados |
| `CREATE DATABASE` / `DROP DATABASE` | Por meio de `EnsureCreated` / `EnsureDeleted` e migrações |

<h4 id="ef-core-limitations">
  Limitações das migrações
</h4>

| Funcionalidade | Motivo |
| - | - |
| Chaves estrangeiras | O ClickHouse não impõe chaves estrangeiras. As migrações rejeitam `AddForeignKey`; o validador emite um aviso durante a construção do modelo. |
| Restrições de unicidade / índices únicos | O ClickHouse não impõe unicidade. Índices únicos geram uma exceção durante a migração. |
| Valores gerados pelo servidor (autoincremento / `IDENTITY`) | Não há equivalente no ClickHouse. |
| Colunas `Nested(…)` | Ainda não há suporte ao mapeamento para um tipo CLR. |
| Entidades de propriedade como JSON (`.ToJson()`) | O mapeamento estrutural de JSON para entidades de propriedade ainda não foi implementado. Em vez disso, use `JsonNode` / `string` em uma coluna `Json` (consulte [Colunas JSON](#ef-core-json)). |

Além das migrações, o provedor ainda não oferece suporte a:

* **`UPDATE` / `DELETE`**
* **Transações**: `BeginTransaction` não tem efeito. Não há suporte a transações ACID no ClickHouse.
* **Tradução de consultas com caminho JSON**: `entity.Data["key"]` em LINQ não é traduzido para a sintaxe SQL `data.key` do ClickHouse. Filtre por colunas que não sejam JSON e examine o JSON na memória.

<h2 id="limitations">
  Limitações
</h2>

<h3 id="valuetuple-caveat">
  Tuplas com 8 ou mais elementos e uma tupla aninhada na última posição
</h3>

Os tipos `ValueTuple` de C# com mais de 7 elementos usam uma estrutura de aninhamento gerada pelo compilador: o 8º argumento genérico (`TRest`) é um `ValueTuple` que contém os elementos restantes. Por exemplo, `(int, int, int, int, int, int, int, string, string)` é compilado como `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

Isso gera uma ambiguidade quando a coluna do ClickHouse é uma tupla de 8 elementos cujo último elemento também é uma tupla — por exemplo, `Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String))`. O driver não consegue distinguir entre:

* Uma **tupla plana de 9 elementos** (com aninhamento de `TRest` gerado pelo compilador)
* Uma **tupla de 8 elementos** cujo último elemento é uma `Tuple(String, String)` aninhada

Ambas produzem o mesmo tipo .NET: `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

O driver interpreta o 8º argumento como `TRest` (ou seja, desfaz o aninhamento), fazendo com que a tupla de 8 elementos com uma tupla aninhada seja serializada incorretamente.

Isso afeta tanto `System.Tuple` quanto `ValueTuple`, pois ambos usam o aninhamento de `TRest` para tuplas com mais de 7 elementos. Tuplas com 7 elementos ou menos, ou cujo último elemento não é uma tupla, não são afetadas.

**Solução alternativa:** Envolva a tupla interna em uma camada adicional para que o driver consiga distingui-la do aninhamento de `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">
  Colunas do tipo AggregateFunction
</h3>

Não é possível consultar colunas do tipo `AggregateFunction(...)` nem inserir dados nelas diretamente.

Para inserir dados:

```sql theme={null}
INSERT INTO t VALUES (uniqState(1));
```

Para selecionar:

```sql theme={null}
SELECT uniqMerge(c) FROM t;
```

***
