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

> El cliente oficial de C# para conectarse a ClickHouse.

# Cliente C# de 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>;
};

El cliente oficial de C# para conectarse a ClickHouse.
El código fuente del cliente está disponible en el [repositorio de GitHub](https://github.com/ClickHouse/clickhouse-cs).
Desarrollado originalmente por [Oleg V. Kozlyuk](https://github.com/DarkWanderer).

La biblioteca ofrece dos API principales:

* **`ClickHouseClient`** (recomendado): un cliente de alto nivel, seguro para subprocesos, diseñado para usarse como singleton. Ofrece una API asíncrona sencilla para consultas e inserciones masivas. Es la mejor opción para la mayoría de las aplicaciones.

* **ADO.NET** (`ClickHouseDataSource`, `ClickHouseConnection`, `ClickHouseCommand`): abstracciones estándar de base de datos de .NET. Son necesarias para la integración con ORM (Dapper, Linq2db) y cuando necesita compatibilidad con ADO.NET. `ClickHouseBulkCopy` es una clase auxiliar para insertar datos de forma eficiente mediante una conexión ADO.NET. `ClickHouseBulkCopy` está obsoleto y se eliminará en una versión futura; en su lugar, use `ClickHouseClient.InsertBinaryAsync`.

Ambas API comparten el mismo pool de conexiones HTTP y pueden usarse juntas en la misma aplicación.

<h2 id="migration-guide">
  Guía de migración
</h2>

1. Actualiza tu archivo `.csproj` con el nuevo nombre del paquete `ClickHouse.Driver` y [la versión más reciente en NuGet](https://www.nuget.org/packages/ClickHouse.Driver).
2. Actualiza en tu código todas las referencias de `ClickHouse.Client` a `ClickHouse.Driver`.

***

<h2 id="supported-net-versions">
  Versiones de .NET compatibles
</h2>

`ClickHouse.Driver` es compatible con las siguientes versiones de .NET:

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

<h2 id="supported-clickhouse-versions">
  Versiones de ClickHouse compatibles
</h2>

El Client admite oficialmente las 3 versiones más recientes, además de las 2 últimas versiones LTS.

<h2 id="installation">
  Instalación
</h2>

Instale el paquete desde NuGet:

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

O bien, usa el Administrador de paquetes NuGet:

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

<h2 id="quick-start">
  Inicio rápido
</h2>

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

// Crear un cliente (normalmente como singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// Ejecutar una consulta
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);
```

<h2 id="configuration">
  Configuración
</h2>

Hay dos formas de configurar su conexión a ClickHouse:

* **Cadena de conexión:** pares clave/valor separados por punto y coma que especifican el host, las credenciales de autenticación y otras opciones de conexión.
* **Objeto `ClickHouseClientSettings`:** objeto de configuración fuertemente tipado que puede cargarse desde archivos de configuración o establecerse en el código.

A continuación se muestra una lista completa de todas las opciones de configuración, sus valores predeterminados y sus efectos.

<h3 id="connection-settings">
  Configuración de la conexión
</h3>

| Propiedad | Tipo | Predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| Host | `string` | `"localhost"` | `Host` | Nombre de host o dirección IP del servidor de ClickHouse |
| Port | `ushort` | 8123 (HTTP) / 8443 (HTTPS) | `Port` | Número de puerto; los valores predeterminados dependen del protocolo |
| Username | `string` | `"default"` | `Username` | Nombre de usuario para la autenticación |
| Password | `string` | `""` | `Password` | Contraseña de autenticación |
| Database | `string` | `""` | `Database` | Base de datos predeterminada; si está vacía, se usa la predeterminada del servidor o del usuario |
| Protocol | `string` | `"http"` | `Protocol` | Protocolo de conexión: `"http"` o `"https"` |
| Path | `string` | `null` | `Path` | Ruta de la URL para entornos con proxy inverso (p. ej., `/clickhouse`) |
| Timeout | `TimeSpan` | 2 minutos | `Timeout` | Tiempo de espera de la operación (se almacena como segundos en la cadena de conexión) |

<h3 id="data-format-serialization">
  Formato de datos y serialización
</h3>

| Propiedad | Tipo | Predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| UseCompression | `bool` | `true` | `Compression` | Controla la compresión de transporte en ambos sentidos para una consulta ordinaria: solicita al servidor que comprima la respuesta (`enable_http_compression`; consulta `AcceptEncoding` para el códec, que un valor explícito puede solicitar incluso con esta opción desactivada) **y** comprime con gzip el cuerpo de la solicitud, salvo con `UseFormDataParameters`, cuyo cuerpo multiparte siempre se envía sin comprimir. Los insert binarios nunca la consultan; usan `InsertOptions.Compressor` — consulta [compresión de inserciones](#insert-compression) |
| AcceptEncoding | `string` | `null` | `AcceptEncoding` | `Accept-Encoding` enviado con cada solicitud, que reemplaza los codecs que el driver anuncia de forma predeterminada (`zstd, lz4, gzip, deflate`). Lo que el servidor responda se decodifica de forma transparente. Consulta [Descompresión de respuestas](#response-decompression) |
| UseCustomDecimals | `bool` | `true` | `UseCustomDecimals` | Usa `ClickHouseDecimal` para precisión arbitraria; si es `false`, usa `decimal` de .NET (límite de 128 bits) |
| ReadStringsAsByteArrays | `bool` | `false` | `ReadStringsAsByteArrays` | Lee las columnas `String` y `FixedString` como `byte[]` en lugar de `string`; útil para datos binarios |
| UseFormDataParameters | `bool` | `false` | `UseFormDataParameters` | Envía los parámetros como datos de formulario en lugar de la cadena de consulta de la URL |
| ReadBufferSize | `int` | `65536` (64 KiB) | `ReadBufferSize` | Tamaño en bytes del búfer que lee las respuestas de consultas HTTP. El driver toma prestado el búfer de un grupo compartido y lo devuelve cuando libera el lector, por lo que no supone una asignación por cada consulta. Increméntalo para reducir las recargas del búfer en conjuntos de resultados grandes. El driver mantiene un búfer por cada lector concurrente, así que el uso de memoria aumenta con el tamaño del búfer y el número de lectores concurrentes. Consulta [Búferes](#perf-buffers). |
| ParameterTypeResolver | `IParameterTypeResolver` | `null` | — | Resolver personalizado para la correspondencia de tipos de parámetros con estilo `@`; consulta [Correspondencia personalizada de tipos de parámetros](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | `null` | — | Formateador personalizado para la serialización de valores de parámetros; consulta [Formato personalizado de valores de parámetros](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | `null` | — | Transformación personalizada aplicada a los valores devueltos por el lector de datos; consulta [Conversión personalizada de valores leídos](#read-value-conversion) |
| JsonReadMode | `JsonReadMode` | `Binary` | `JsonReadMode` | Cómo se devuelven los datos JSON: `Binary` (devuelve `JsonObject`) o `String` (devuelve la cadena JSON sin procesar) |
| JsonWriteMode | `JsonWriteMode` | `String` | `JsonWriteMode` | Cómo se envían los datos JSON: `String` (serializa mediante `JsonSerializer`, acepta cualquier entrada) o `Binary` (solo POCO registrados con indicaciones de tipo) |
| MapReadMode | `MapReadMode` | `Dictionary` | `MapReadMode` | Cómo se devuelven los datos `Map(K, V)`: `Dictionary` (devuelve `Dictionary<K, V>`; una clave repetida conserva solo su último valor) o `KeyValuePairs` (devuelve `List<KeyValuePair<K, V>>`, conservando todos los pares). Consulta [Tipo Map](#type-map-reading-map) |
| AllowDuplicateJsonKeys | `bool` | `false` | `AllowDuplicateJsonKeys` | Cómo leer una fila `JSON` en la que ambas rutas superpuestas contienen un valor. `false` lanza una excepción, porque conservar uno de los valores implica descartar el otro; `true` conserva el que la fila lleve en último lugar. Consulta [Rutas superpuestas](#type-map-reading-json) |

<h3 id="session-management">
  Gestión de sesiones
</h3>

| Propiedad | Tipo | Predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| UseSession | `bool` | `false` | `UseSession` | Habilita sesiones con estado; serializa las solicitudes |
| SessionId | `string` | `null` | `SessionId` | ID de sesión; genera automáticamente un GUID si es `null` y `UseSession` es `true` |

<Note>
  El indicador `UseSession` habilita la persistencia de la sesión del servidor, lo que permite usar sentencias `SET` y tablas temporales. Las sesiones se reinician tras 60 segundos de inactividad (timeout predeterminado). La duración de la sesión puede ampliarse configurando ajustes de sesión mediante sentencias de ClickHouse o la configuración del servidor.

  La clase `ClickHouseConnection` normalmente permite operaciones en paralelo (varios hilos pueden ejecutar consultas de forma concurrente). Sin embargo, habilitar el indicador `UseSession` lo limita a una sola consulta activa por conexión en un momento dado (esta es una limitación del lado del servidor).
</Note>

<h3 id="security">
  Seguridad
</h3>

| Propiedad | Tipo | Predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| SkipServerCertificateValidation | `bool` | `false` | — | Omite la validación del certificado HTTPS; **no debe usarse en producción** |

<h3 id="http-client-configuration">
  Configuración del cliente HTTP
</h3>

| Propiedad | Tipo | Valor predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| HttpClient | `HttpClient` | `null` | — | Instancia personalizada de HttpClient ya configurada |
| HttpClientFactory | `IHttpClientFactory` | `null` | — | Fábrica personalizada para crear instancias de HttpClient |
| HttpClientName | `string` | `null` | — | Nombre para que HttpClientFactory cree un cliente concreto |

<h3 id="logging-debugging">
  Registro y depuración
</h3>

| Propiedad | Tipo | Predeterminado | Clave de cadena de conexión | Descripción |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | Fábrica de registradores para el registro de diagnósticos |
| EnableDebugMode | `bool` | `false` | — | Activa las trazas de red de .NET (requiere LoggerFactory con el nivel configurado en `Trace`); **impacto significativo en el rendimiento** |

<h3 id="custom-settings-roles">
  Ajustes personalizados y roles
</h3>

| Propiedad | Tipo | Predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| CustomSettings | `IDictionary<string, object>` | Vacío | prefijo `set_*` | ajustes del servidor de ClickHouse; consulta la nota a continuación |
| Roles | `IReadOnlyList<string>` | Vacío | `Roles` | roles de ClickHouse separados por comas (p. ej., `Roles=admin,reader`) |
| ApplicationInfo | `IReadOnlyDictionary<string, string>` | Vacío | — | Etiquetas de formato libre añadidas al encabezado HTTP `User-Agent` para atribuir consultas por aplicación. |

<Note>
  Al usar una cadena de conexión para establecer ajustes personalizados, usa el prefijo `set_`, p. ej., "set\_max\_threads=4". Al usar un objeto ClickHouseClientSettings, no uses el prefijo `set_`.

  Para ver la lista completa de ajustes disponibles, consulta [aquí](/es/reference/settings/session-settings).
</Note>

***

<h3 id="connection-string-examples">
  Ejemplos de cadenas de conexión
</h3>

<h4 id="basic-connection">
  Conexión básica
</h4>

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

<h4 id="with-custom-clickhouse-settings">
  Con ajustes personalizados de 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 anular la configuración del cliente para una consulta concreta. Todas las propiedades son opcionales y solo anulan los valores predeterminados del cliente cuando se especifican.

| Propiedad | Tipo | Descripción |
| - | - | - |
| QueryId | `string` | Identificador de consulta personalizado para el seguimiento en `system.query_log` o para cancelarla |
| Database | `string` | Anula la base de datos predeterminada para esta consulta |
| Roles | `IReadOnlyList<string>` | Anula los roles del cliente para esta consulta |
| CustomSettings | `IDictionary<string, object>` | Ajustes del servidor de ClickHouse para esta consulta (por ejemplo, `max_threads`) |
| CustomHeaders | `IDictionary<string, string>` | Encabezados HTTP adicionales para esta consulta |
| UseSession | `bool?` | Anula el comportamiento de la sesión para esta consulta |
| SessionId | `string` | ID de sesión para esta consulta (requiere `UseSession = true`) |
| BearerToken | `string` | Anula el token de autenticación para esta consulta |
| ParameterTypeResolver | `IParameterTypeResolver` | Anula el resolver a nivel de client para la correspondencia de tipos de parámetros con estilo `@`; consulte [Correspondencia personalizada de tipos de parámetros](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | Anula el formateador a nivel de client para la serialización de valores de parámetros con estilo `@`; consulte [Formato personalizado de valores de parámetros](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | Anula la transformación a nivel de client aplicada a los valores devueltos por el lector de datos; consulte [Conversión personalizada de valores leídos](#read-value-conversion) |
| MaxExecutionTime | `TimeSpan?` | Timeout de la consulta en el servidor (se pasa como la configuración `max_execution_time`); el servidor cancela la consulta si se supera |
| AcceptEncoding | `string` | Anulación por consulta de `Accept-Encoding` (por ejemplo, `"br"`, `"identity"`), que tiene prioridad sobre `ClickHouseClientSettings.AcceptEncoding`; también fuerza `enable_http_compression=1` en la URL. Consulte [Compresión de transporte por consulta](#per-query-accept-encoding). |

**Ejemplo:**

```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` amplía `QueryOptions` con opciones específicas para operaciones de inserción masiva mediante `InsertBinaryAsync`.

| Propiedad | Type | Predeterminado | Descripción |
| - | - | - | - |
| BatchSize | `int` | 100,000 | Número de filas por lote |
| MaxDegreeOfParallelism | `int` | 1 | Número de cargas de lotes en paralelo |
| Format | `RowBinaryFormat` | `RowBinary` | Formato binario: `RowBinary` o `RowBinaryWithDefaults` |
| Compressor | `IClickHouseCompressor` | `ZstdCompressor.Default` | Códec aplicado al cuerpo de la inserción (`Content-Encoding`). `null` lo envía sin comprimir. Consulte [Compresión de inserciones](#insert-compression) |
| QueryPlacement | `InsertQueryPlacement` | `Body` | Dónde se envía la sentencia `INSERT INTO ... FORMAT ...`: `Body` (antes de las filas) o `Url` (como el parámetro de URL `query`). Consulte [Posición de la consulta de inserción](#insert-query-placement) |
| ColumnTypes | `IReadOnlyDictionary<string, string>` | `null` | Nombre de columna → cadena de tipo de ClickHouse. Omite la consulta de sondeo del esquema cuando se especifica. |
| UseSchemaCache | `bool` | `false` | Almacena en caché el esquema completo de la tabla para cada par (base de datos, tabla) durante la vida útil del cliente. |

Todas las propiedades de `QueryOptions` también están disponibles en `InsertOptions`.

**Ejemplo:**

```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">
  Omitir la consulta de sondeo del esquema
</h4>

De forma predeterminada, `InsertBinaryAsync` envía una consulta `SELECT ... WHERE 1=0` antes de cada inserción para detectar los tipos de columna. Para escenarios de alto rendimiento, puedes eliminar esta sobrecarga con dos opciones:

**Opción 1: Proporcionar los tipos de columna explícitamente**

Cuando conoces el esquema de la tabla en tiempo de compilación, pásalo directamente mediante `ColumnTypes`. No se envía ninguna consulta de esquema en absoluto:

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

**Opción 2: Almacenar en caché el esquema**

Cuando realices inserciones repetidas en la misma tabla, establece `UseSchemaCache = true` para consultar el esquema una sola vez y reutilizarlo en las inserciones posteriores de la misma instancia de `ClickHouseClient`:

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

// La primera llamada obtiene el esquema del servidor
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// La segunda llamada reutiliza el esquema en caché — sin ida y vuelta adicional
await client.InsertBinaryAsync("my_table", columns, batch2, options);
```

<Note>
  * `ColumnTypes` tiene prioridad sobre `UseSchemaCache`. Si se configuran ambos, se usan los tipos explícitos.
  * La caché de esquema no detecta cambios realizados con `ALTER TABLE`. Si modifica el esquema de la tabla, cree un nuevo `ClickHouseClient` o evite `UseSchemaCache` para esa tabla.
  * La caché se limita a la instancia de `ClickHouseClient` y se indexa por (database, table). Los distintos subconjuntos de columnas de una misma tabla comparten un único esquema en caché.
</Note>

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

`ClickHouseClient` es la API recomendada para interactuar con ClickHouse. Es seguro para subprocesos, está diseñado para usarse como singleton y gestiona internamente el pool de conexiones HTTP.

<h3 id="creating-a-client">
  Crear un Client
</h3>

Cree un `ClickHouseClient` con una cadena de conexión o un objeto `ClickHouseClientSettings`. Consulte la sección [Configuración](#configuration) para ver las opciones disponibles.

Los detalles de su servicio de ClickHouse Cloud están disponibles en la consola de ClickHouse Cloud.

Seleccione un servicio y haga clic en **Connect**:

<Image img="https://mintcdn.com/private-7c7dfe99-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/cloud-connect-button.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=d059c1bbcc7317ff8df85b20189e65f4" size="md" alt="Botón Connect del servicio de ClickHouse Cloud" border width="998" height="932" data-path="images/_snippets/cloud-connect-button.webp" />

Elija **C#**. Los detalles de la conexión se muestran a continuación.

<Image img="https://mintcdn.com/private-7c7dfe99-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/connection-details-csharp.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=487b14816a8a8711d46ae022d82d74ef" size="md" alt="Detalles de conexión de C# para ClickHouse Cloud" border width="851" height="805" data-path="images/_snippets/connection-details-csharp.webp" />

Si utiliza ClickHouse autogestionado, los detalles de la conexión los establece el administrador de ClickHouse.

Usar una cadena de conexión:

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

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

O bien, usando `ClickHouseClientSettings`:

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

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

Para escenarios con inyección de dependencias, 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` está diseñado para ser de larga duración y para compartirse en toda la aplicación. Créelo una sola vez (normalmente como un singleton) y reutilícelo para todas las operaciones de base de datos. El Client administra internamente el pool de conexiones HTTP.
</Note>

***

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

Use `ExecuteNonQueryAsync` para las sentencias que no devuelven resultados:

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

// Eliminar una tabla
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");
```

Usa `ExecuteScalarAsync` para obtener un único valor:

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

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Versión del servidor: {version}");
```

***

<h3 id="inserting-data">
  Insertar datos
</h3>

<h4 id="parameterized-inserts">
  Inserciones parametrizadas
</h4>

Inserta datos mediante consultas parametrizadas con `ExecuteNonQueryAsync`. Los tipos de los parámetros deben especificarse en el SQL usando la sintaxis `{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">
  Inserciones masivas
</h4>

Use `InsertBinaryAsync` para insertar un gran número de filas de forma eficiente. Transmite los datos mediante el formato binario nativo de filas de ClickHouse, admite envíos por lotes en paralelo y evita los errores de "URL too long" que pueden producirse con consultas parametrizadas.

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

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

// Inserción básica
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");
```

Para grandes volúmenes de datos, configure el procesamiento por lotes y el paralelismo con `InsertOptions`:

```csharp theme={null}
var options = new InsertOptions
{
    BatchSize = 100_000,           // Filas por lote (predeterminado: 100,000)
    MaxDegreeOfParallelism = 4     // Cargas de lotes en paralelo (predeterminado: 1)
};
```

<Note>
  * El cliente obtiene automáticamente la estructura de la tabla mediante `SELECT * FROM <table> WHERE 1=0` antes de insertar. Los valores proporcionados deben coincidir con los tipos de las columnas de destino. Para omitir esta consulta, use [`InsertOptions.ColumnTypes` o `InsertOptions.UseSchemaCache`](#skip-schema-query).
  * Cuando `MaxDegreeOfParallelism > 1`, los batches se cargan en paralelo. Las sesiones no son compatibles con la inserción en paralelo; desactive las sesiones o establezca `MaxDegreeOfParallelism = 1`.
  * Use `RowBinaryFormat.RowBinaryWithDefaults` en `InsertOptions.Format` si desea que el servidor aplique valores DEFAULT a las columnas no proporcionadas.
</Note>

<h4 id="poco-insert">
  Inserciones con POCO
</h4>

En lugar de construir arrays `object[]`, puede insertar directamente objetos POCO fuertemente tipados. Registre el tipo una sola vez y luego pase `IEnumerable<T>`:

```csharp theme={null}
// Defina un POCO que se corresponda con las columnas de su tabla
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// Registre el tipo (una vez durante la vida útil del cliente)
client.RegisterBinaryInsertType<SensorReading>();

// Inserte directamente: los nombres de las columnas se obtienen de los nombres de las propiedades
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);
```

De forma predeterminada, todas las propiedades públicas de lectura se asignan a columnas mediante una coincidencia estricta de nombres que distingue entre mayúsculas y minúsculas. Puede personalizar la asignación con atributos:

```csharp theme={null}
public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // Mapear a una columna con nombre distinto
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // Tipo ClickHouse explícito
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Excluir del insert
    public string InternalTag { get; set; }
}
```

| Atributo | Propósito |
| - | - |
| `[ClickHouseColumn(Name = "...")]` | Sobrescribe el nombre de la columna de destino |
| `[ClickHouseColumn(Type = "...")]` | Declara explícitamente el tipo de ClickHouse |
| `[ClickHouseNotMapped]` | Excluye la propiedad de la inserción |

Cuando **todas** las propiedades mapeadas especifican un `Type` explícito, la consulta de sondeo del esquema se omite por completo. Cuando solo algunas propiedades tienen tipos explícitos, el driver recurre a la consulta de sondeo del esquema para el conjunto completo de columnas.

`InsertBinaryAsync<T>` admite las mismas `InsertOptions` (agrupación en lotes, paralelismo, almacenamiento en caché del esquema) que la sobrecarga `object[]`.

<Note>
  A diferencia de la sobrecarga `object[]`, `InsertBinaryAsync<T>` no acepta una lista explícita de columnas. Las columnas se determinan a partir de las propiedades mapeadas del tipo registrado. Para controlar qué columnas se insertan, use `[ClickHouseNotMapped]` para excluir propiedades o `[ClickHouseColumn(Name = "...")]` para cambiarles el nombre.

  Si se establece `ColumnTypes` en `InsertOptions`, sobrescribirá los atributos del POCO.
</Note>

<h4 id="poco-insert-schema-evolution">
  Evolución del esquema
</h4>

Las inserciones de POCO funcionan sin problemas cuando se añaden columnas a la tabla de destino después de registrar el tipo. Como el driver solo inserta las columnas asignadas por el POCO, cualquier columna nueva con `DEFAULT` (u otras expresiones predeterminadas) la rellena automáticamente el servidor. No se requieren cambios en el código ni volver a registrar nada.

<h4 id="insert-query-placement">
  Posición de la consulta de inserción
</h4>

Un insert binario escribe su sentencia `INSERT INTO ... FORMAT ...` como primera línea del request body, antes de las filas. El body se comprime de forma predeterminada, por lo que el enrutamiento y el logging que solo inspeccionan la URL no ven la sentencia. Establezca `InsertOptions.QueryPlacement` en `InsertQueryPlacement.Url` para enviar la sentencia como el URL parameter `query`, dejando el body solo para las filas:

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

Úselo cuando un proxy, un balanceador de carga o un gateway enrute o inspeccione el parámetro `query`, o cuando desee que la sentencia aparezca en los logs de acceso y en las herramientas de observabilidad. Es opcional porque, en ese caso, la sentencia cuenta para la longitud de la URL. El límite efectivo es el más bajo de los impuestos por el runtime de .NET, un intermediario y el server. Desde .NET 6 hasta .NET 9, `System.Uri` limita el URI de solicitud completo codificado a 65.519 caracteres; el driver lanza una `InvalidOperationException` que lo remite de nuevo a `InsertQueryPlacement.Body` cuando se supera este límite. En ClickHouse, `http_max_uri_size` es de 1 MiB de forma predeterminada, aunque un intermediario puede imponer un límite inferior. En el modo body, la sentencia y las filas no están sujetos a ese límite de longitud de URL; otras opciones de la solicitud sí pueden aparecer en la URL.

Esta configuración es independiente de `Compressor`: el body se codifica de la misma manera en ambos modos.

***

<h3 id="reading-data">
  Lectura de datos
</h3>

Use `ExecuteReaderAsync` para ejecutar consultas SELECT. El `ClickHouseDataReader` devuelto proporciona acceso tipado a las columnas del resultado mediante métodos como `GetInt64()`, `GetString()` y `GetFieldValue<T>()`.

Llame a `Read()` para avanzar a la fila siguiente. Devuelve `false` cuando ya no quedan más filas. Acceda a las columnas por índice (empezando en 0) o por nombre de columna.

```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">
  Lectura de POCO
</h4>

En lugar de leer columnas por índice o por nombre, puedes enviar los resultados de la consulta directamente a tus propias clases. Registra el tipo una sola vez con el Client y luego usa `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 tanto los mapeos de inserción como los de lectura y valida ambos desde el principio. `RegisterBinaryInsertType<T>()` no cambia y sigue siendo solo para inserción por compatibilidad con versiones anteriores.

Un tipo registrado debe tener:

* Un constructor público sin parámetros.
* Al menos una propiedad pública con un setter público que no sea `init`. Se admiten propiedades `required`.

<h5 id="poco-read-column-matching">
  Coincidencia de columnas
</h5>

La coincidencia de columnas es sensible a mayúsculas y minúsculas. Las columnas de resultado que faltan dejan las propiedades con su valor predeterminado; las columnas de resultado adicionales se ignoran.

El driver no amplía ni reduce los valores. Salvo por las representaciones alternativas que se enumeran a continuación, el tipo de framework de la columna debe ser asignable al tipo de la propiedad, y una incompatibilidad produce `InvalidOperationException`. Por lo tanto, una propiedad `object` acepta cualquier columna.

<h5 id="poco-read-types">
  Tipos de propiedad admitidos
</h5>

`QueryAsync<T>` lee cada una de estas columnas directamente en una propiedad coincidente:

| Columna de ClickHouse | Tipo(s) de propiedad |
| - | - |
| `Int8`/`Int16`/`Int32`/`Int64` | `sbyte`/`short`/`int`/`long` |
| `UInt8`/`UInt16`/`UInt32`/`UInt64` | `byte`/`ushort`/`uint`/`ulong` |
| `Int128`/`UInt128` | `BigInteger`, o los tipos nativos `System.Int128`/`System.UInt128` en .NET 8 y posteriores |
| `Int256`/`UInt256` | `BigInteger` |
| `Float32`/`Float64`/`BFloat16` | `float`/`double`/`float` |
| `Bool` | `bool` |
| `Decimal` | `decimal` o `ClickHouseDecimal` |
| `Date`/`Date32`/`DateTime`/`DateTime64` | `DateTime`, `DateTimeOffset` o `DateOnly` |
| `Time`/`Time64` | `TimeSpan` |
| `UUID` | `Guid` |
| `IPv4`/`IPv6` | `IPAddress` |
| `Enum8`/`Enum16` | `string` (la etiqueta) o `int` (el ordinal en el flujo binario) |
| `String`/`FixedString` | `string` o `byte[]` |

Cada fila admite también la forma anulable de su tipo de propiedad (`long?`, `DateOnly?`, etc.),
sea o no la columna `Nullable(...)`. Una propiedad de tipo de valor no anulable sobre una columna
`Nullable(T)` se acepta en el registro, pero lanza una excepción cuando llega un NULL.

Las envolturas como `LowCardinality(T)`, `SimpleAggregateFunction(f, T)` y `Object(T)` se corresponden exactamente con `T`.

También se admiten las columnas compuestas, que adoptan el tipo del framework indicado en la
[referencia de tipos de lectura](#clickhouse-native-type-map-reading): `Array(T)` a `T[]`, `Tuple(...)`
a `System.Tuple<...>`, `Nested(...)` a `Tuple<...>[]`, `JSON` a `JsonObject` (o `string`
con [`JsonReadMode=String`](#type-map-reading-json)), y `Variant`/`Dynamic` a `object`.

Una columna `Map(K, V)` es un caso especial: una propiedad `List<KeyValuePair<K, V>>` o `KeyValuePair<K, V>[]`
se lee por la ruta sin boxing y conserva el orden del flujo binario y las claves repetidas, en cualquiera de los
[`MapReadMode`](#type-map-reading-map). Una propiedad `Dictionary<K, V>` solo funciona en el modo predeterminado.
Los tipos de clave y valor deben coincidir exactamente, por lo que
`Map(String, Nullable(Int32))` requiere `KeyValuePair<string, int?>`.

Cuando una columna ofrece más de un tipo de propiedad (una columna `DateTime` como `DateTime`,
`DateTimeOffset` o `DateOnly`, o una columna `String` como `string` o `byte[]`), el tipo de propiedad declarado determina la representación. Estas representaciones alternativas
pertenecen a la ruta POCO, por lo que `QueryAsync<T>` dispone de ellas y `MapTo<T>` no.

<h5 id="poco-read-mapto">
  Materialización de una sola fila
</h5>

Al iterar manualmente sobre un lector, use `ClickHouseDataReader.MapTo<T>()` para materializar la fila actual en un POCO registrado sin hacer avanzar el lector:

```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>` cuando necesite controlar usted mismo el bucle del lector; por ejemplo, para combinar el acceso directo a columnas
con la materialización de POCO. Lee la fila a través de los valores encajonados (boxed) del lector, por lo que no ofrece
los tipos de propiedad alternativos indicados arriba y realiza más asignaciones de memoria que `QueryAsync<T>`. Es preferible usar
`QueryAsync<T>` cuando solo necesite las filas; consulte
[elegir la ruta de materialización](#perf-read-path) para ver las cifras.

<h5 id="poco-read-converters">
  Convertidores de valores de lectura
</h5>

Un [convertidor de valores de lectura](#read-value-conversion) a nivel de client o por consulta se aplica a ambas rutas y
no desactiva la lectura sin boxing. El driver convierte cada columna mediante la sobrecarga que corresponde a la forma en que
se leyó la columna: el `ConvertValue<T>` tipado para una columna
sin boxing, y el `ConvertValue` con boxing para una columna compuesta. Implemente ambas sobrecargas
de forma coherente; de lo contrario, una misma columna dará resultados distintos según la ruta.

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

Cuando se configura un `LoggerFactory`, `RegisterPocoType<T>()` y `RegisterBinaryInsertType<T>()` emiten un registro de nivel `Debug` (categoría `ClickHouse.Driver.Client`) en el que se indica qué propiedades se asignaron a qué columnas y cuáles se omitieron, además del motivo. Consulte [Registro y diagnóstico](#logging-and-diagnostics).

***

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

En ClickHouse, el formato estándar de los parámetros en las consultas SQL es `{parameter_name:DataType}`.

**Ejemplos:**

```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>
  Los parámetros SQL 'bind' se pasan como parámetros de consulta en la URI HTTP, por lo que usar demasiados puede provocar una excepción de "URL demasiado larga". Use `InsertBinaryAsync` para la inserción masiva de datos y así evitar esta limitación.
</Note>

<h4 id="at-style-placeholders">
  Marcadores de posición `@name` de estilo ADO
</h4>

El driver también acepta marcadores de posición `@name`, como los que emiten ORM del tipo Dapper. Son una
comodidad del lado del cliente: antes de enviarse la petición, cada uno se reescribe como
`{name:ResolvedType}`, de modo que el servidor nunca llega a ver una `@`. Consulta
[la resolución de tipos](#parameter-type-mapping) para saber cómo se elige el tipo. Usa la forma explícita
`{name:Type}` siempre que puedas.

Un `@name` sin ningún parámetro coincidente se deja intacto para que el servidor lo rechace. La coincidencia es
sensible a mayúsculas y minúsculas, por lo que `@ID` no vincula un parámetro llamado `id`.

<Note>
  Para desactivar la reescritura, activa el interruptor de AppContext `ClickHouse.Driver.DisableReplacingParameters`
  antes del primer uso del driver. Solo se detiene la reescritura del texto; los parámetros se siguen enviando, por lo que
  las consultas escritas con la sintaxis nativa `{name:Type}` siguen funcionando.
</Note>

<h4 id="identifier-parameters">
  Parámetros de Identifier
</h4>

El tipo de parámetro `Identifier` le permite especificar de forma segura el nombre de una base de datos, tabla o columna en lugar de un literal de cadena entre comillas. Úselo mediante la sintaxis `{name:Identifier}` en SQL, o estableciendo `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);
```

El valor se envía literalmente, y el servidor lo sustituye como un identificador SQL sin comillas, aplicando su propio entrecomillado con comillas invertidas y su propio escape. Los identificadores que contienen caracteres especiales (incluidas las comillas invertidas) se conservan de forma segura al hacer un recorrido de ida y vuelta.

***

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

A cada consulta se le asigna un `query_id` único que puede usarse para obtener datos de la tabla `system.query_log` o cancelar consultas de larga duración. Puedes especificar un ID de consulta personalizado mediante `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>
  Si especificas un `QueryId` personalizado, asegúrate de que sea único en cada llamada. Un GUID aleatorio es una buena opción.
</Tip>

***

<h3 id="parameter-type-mapping">
  Correspondencia personalizada de tipos de parámetros
</h3>

Al usar parámetros con el estilo `@` (por ejemplo, `WHERE id = @id`), el driver infiere automáticamente el tipo de ClickHouse a partir del tipo de valor de .NET. Por ejemplo, `int` se corresponde con `Int32`.

<Warning>
  **Comportamiento de los parámetros `DateTime` inferidos**

  Para los parámetros con el estilo `@` sin indicación `{name:Type}` en el SQL y sin `ClickHouseType` configurado, los valores que representan un instante se infieren como `DateTime('UTC')` en lugar de un `DateTime` simple. Los valores `DateTime` con `Kind` `Utc` o `Local`, y todos los valores `DateTimeOffset`, se envían como `DateTime('UTC')`, preservando el instante independientemente de la zona horaria del servidor.

  Las indicaciones explícitas (`{name:DateTime}`) tienen prioridad sobre la inferencia y son la forma recomendada de crear consultas.
</Warning>

Para anular estos valores predeterminados, configure `ParameterTypeResolver` en `ClickHouseClientSettings`. Esto resulta útil cuando desea que todos los parámetros `DateTime` usen `DateTime64(3)` con precisión de milisegundos, o que todos los decimales usen una escala específica, sin tener que establecer `ClickHouseType` en cada parámetro individual.

**Uso de `DictionaryParameterTypeResolver` para correspondencias de tipos simples:**

```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 casos avanzados:**

Para una resolución basada en el valor o en el nombre, implemente directamente la interfaz `IParameterTypeResolver`. Devuelva `null` para que se aplique la inferencia predeterminada:

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

También puede configurar un resolver para una sola consulta mediante `QueryOptions.ParameterTypeResolver`. Cuando se establece, tiene prioridad sobre el resolver a nivel de cliente.

**Precedencia de la resolución de tipos:**

El resolver es un paso dentro de una cadena de precedencia. De mayor a menor prioridad:

1. `ClickHouseType` explícito establecido en el parámetro
2. Indicación de tipo SQL de la sintaxis `{name:Type}` en la consulta
3. `IParameterTypeResolver` (de `QueryOptions.ParameterTypeResolver`, con fallback a `ClickHouseClientSettings.ParameterTypeResolver`)
4. Inferencia de tipos integrada (`TypeConverter.ToClickHouseType`)

El resolver también funciona con la vía `ClickHouseConnection` de ADO.NET: las conexiones creadas desde el cliente heredan la configuración.

***

<h3 id="parameter-value-formatting">
  Formato personalizado de valores de parámetros
</h3>

`IParameterFormatter` es un hook que determina cómo se serializan los valores de los parámetros. Úselo cuando el formato integrado (por ejemplo, la precision de DateTime, la configuración regional de los decimales, el escape de cadenas o la representación de números) no coincida con lo que espera su schema o sus herramientas de destino.

Configure `ParameterFormatter` en `ClickHouseClientSettings` para instalar un formateador para todas las consultas parametrizadas. El formateador recibe el valor, el type name de ClickHouse resuelto y el nombre del parámetro, y devuelve la string representation que se envía al server. Devuelva `null` para que se use el formateador predeterminado.

**Uso de `DictionaryParameterFormatter` para un formato sencillo por tipo de 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 casos avanzados:**

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

También puede establecer un formateador por consulta mediante `QueryOptions.ParameterFormatter`. Cuando se establece, tiene prioridad sobre el formateador a nivel de client.

**Valores compuestos:**

El formateador se ejecuta tanto para los parámetros de collection de nivel superior como para cada elemento dentro de valores compuestos (`Array`, `Tuple`, `Map`, `Nullable`, `LowCardinality`, `Variant`). Por ejemplo, una correspondencia de `typeof(int)` formatea individualmente cada elemento `Int32` de un `Array(Int32)`.

**Comillas simples en contextos compuestos:**

Para los tipos de ClickHouse similares a cadenas (`String`, `FixedString`, `Enum8`, `Enum16`, `IPv4`, `IPv6`, `UUID`) incrustados dentro de un literal compuesto, el driver encierra la salida del formateador entre comillas simples, pero no escapa su contenido. Si la cadena devuelta contiene una comilla simple o una barra invertida sin escape, el literal compuesto quedará mal formado y el server rechazará la consulta.

Los parámetros de cadena de nivel superior (no incrustados en un compuesto) se usan textualmente, sin comillas adicionales, por lo que no es necesario aplicar escaping en ese caso.

**Prioridad del formateador:**

1. `IParameterFormatter` (de `QueryOptions.ParameterFormatter`, con respaldo en `ClickHouseClientSettings.ParameterFormatter`). Si devuelve un valor no nulo, se usa ese valor.
2. Formato integrado específico del tipo en `HttpParameterFormatter`.

No se consulta al formateador para valores `null` o `DBNull`; esos siempre se serializan como el centinela nulo de ClickHouse (`\N`).

***

<h3 id="read-value-conversion">
  Conversión personalizada de valores leídos
</h3>

`IReadValueConverter` permite transformar los valores devueltos por el lector de datos después de la deserialización, sin cambiar su tipo CLR. Usos habituales: establecer `DateTime.Kind = Utc` en una columna `DateTime` que no tiene zona horaria, recortar o normalizar cadenas, o posprocesar una columna JSON antes de que llegue al código de la aplicación.

Configure `ReadValueConverter` en `ClickHouseClientSettings` para instalar un convertidor para todas las operaciones de lectura. El convertidor se invoca una vez por columna y por fila, tanto en la variante boxed (`GetValue`) como en la genérica (`GetFieldValue<T>`). Si no se configura ningún convertidor, no hay sobrecarga: el lector devuelve los valores directamente.

**Uso de `DictionaryReadValueConverter` para una conversión sencilla 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);
```

Los valores cuyo tipo CLR en tiempo de ejecución no esté registrado con `For<T>` se devuelven sin cambios. La resolución se basa en el tipo CLR exacto, así que registre el tipo real que produce el lector (p. ej., `For<JsonObject>` para una columna JSON en `JsonReadMode.Binary`).

**`IReadValueConverter` personalizado para escenarios avanzados:**

Si necesita resolver según la cadena de tipo del lado de ClickHouse (por ejemplo, para distinguir `DateTime` de `DateTime('UTC')` — ambos aparecen como el mismo tipo CLR), implemente `IReadValueConverter` directamente:

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

El convertidor debe preservar el tipo CLR en tiempo de ejecución; los metadatos de columna (`GetFieldType`, `GetSchemaTable`) no se redirigen a través de él y deben seguir siendo coherentes con lo que se devuelve.

También puedes establecer un convertidor por consulta mediante `QueryOptions.ReadValueConverter`; cuando se establece, tiene prioridad sobre el convertidor a nivel de Client.

**Límite de despacho:**

El convertidor se invoca una vez por columna con el valor completo de la celda deserializado; **no** desciende recursivamente a contenedores compuestos. Para una columna `Array(Int32)`, el valor que se pasa es un `int[]`; para `Tuple(Int32, String)`, es un `ITuple`.

**Qué sobrecarga se ejecuta:**

Ambas sobrecargas deben ser coherentes entre sí, porque la que invoca el driver depende de cómo el llamador haya leído la
columna:

* `ConvertValue<T>`: los accessors tipados `GetByte`, `GetSByte`, `GetInt16`/`32`/`64`,
  `GetUInt16`/`32`/`64`, `GetFloat`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetIPAddress`,
  `GetBigInteger` y `GetFieldValue<T>`, además de todas las columnas sin boxing de la
  [ruta de lectura POCO](#poco-read-converters).
* `ConvertValue` (boxed): `GetValue`, `GetValues`, los indexadores, `GetChar`, `GetTuple` y las
  rutas de coerción en `GetBoolean`, `GetDecimal` y `GetString`.

`IsDBNull` no ejecuta ningún convertidor: lee el indicador de nulo directamente, por lo que un convertidor nunca puede
cambiar si un valor cuenta como nulo. `TryGetEnumOrdinal` también lo omite; consulta
[lectura del ordinal de un enum](#ado-net-reader-enum-ordinal).

El convertidor funciona con la ruta de ADO.NET `ClickHouseConnection`: la configuración se hereda en las conexiones creadas desde el Client.

***

<h3 id="raw-streaming">
  Transmisión sin procesar
</h3>

Use `ExecuteRawResultAsync` para transmitir directamente los resultados de una consulta en un formato específico, omitiendo el lector de datos. Esto resulta útil para exportar datos a archivos o enviarlos a otros 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 comunes: `JSONEachRow`, `CSV`, `TSV`, `Parquet`, `Native`. Consulta la [documentación sobre formatos](/es/reference/formats/index) para conocer todas las opciones.

***

<h3 id="per-query-accept-encoding">
  Compresión de transporte por consulta
</h3>

De forma predeterminada, el client negocia `zstd, lz4, gzip, deflate` cuando `Compression=true` (el valor predeterminado de la cadena de conexión) y descodifica el flujo por sí mismo, de forma transparente.

Para exportaciones sin procesar (p. ej., Parquet, Arrow, Native), es posible que quiera negociar un códec distinto (p. ej., `zstd` o `lz4`) para intercambiar CPU por ancho de banda sin cambiar la configuración de toda la conexión. `QueryOptions.AcceptEncoding` y `ClickHouseCommand.AcceptEncoding` establecen la cabecera HTTP `Accept-Encoding` para una sola solicitud, reemplazan cualquier valor predeterminado asociado y fuerzan `enable_http_compression=1` en la URL (que es lo que ClickHouse requiere antes de respetar `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">
  Configuración de HttpClient
</h4>

No hay nada que configurar: el `HttpClient` que construye el driver deja `AutomaticDecompression` en `DecompressionMethods.None` y es el propio driver quien decodifica las respuestas, de modo que `Content-Encoding` nunca se elimina a tus espaldas y el cuerpo sin procesar te llega exactamente como lo envió el servidor.

<Warning>
  Si proporcionas tu propio `HttpClient`, deja también `AutomaticDecompression` desactivado. No es solo una opción del lado de la respuesta: al enviar, el handler **añade todos los algoritmos de su máscara que falten en el `Accept-Encoding` saliente**. Así, un handler con `GZip | Deflate` convierte un `AcceptEncoding = "lz4"` explícito en `lz4, gzip, deflate`, y un `"identity"` explícito en `identity, gzip, deflate` on the wire; y, dado que ClickHouse resuelve el header según su propia preferencia fija de códecs (ignorando el orden y los valores q), puede responder con un códec que nunca pediste, que el handler decodifica y elimina, de modo que ni siquiera llegas a enterarte. Dejar la máscara desactivada hace que la oferta sea exactamente la que elegiste.
</Warning>

<Warning>
  Si `AcceptEncoding` solicita un códec que el driver no puede decodificar (`snappy`), solo `ExecuteRawResultAsync` es seguro. `ExecuteReaderAsync`, `ExecuteScalarAsync` y `ExecuteNonQueryAsync` fallan con una `NotSupportedException` que indica el códec (antes interpretaban los compressed bytes como el format del resultado y producían garbage).
</Warning>

<h4 id="per-query-accept-encoding-errors">
  Cuerpos de error
</h4>

Cuando el servidor responde con un 4xx/5xx y se ha establecido `enable_http_compression=1`, comprime el cuerpo del error con el mismo códec que habría usado para una respuesta satisfactoria. El driver los decodifica en el caso de todos los códecs que admite (`lz4`, `zstd`, `gzip`, `deflate`, `br`/`brotli`), de modo que el mensaje en `ClickHouseServerException` sea legible. Para cualquier otro caso (`snappy`, …) devuelve un mensaje provisional que indica el códec y remite a `system.query_log` para ver el texto original del error.

***

<h3 id="response-decompression">
  Descompresión de la respuesta
</h3>

`Accept-Encoding` solo le pide al servidor que comprima la respuesta; alguien tiene que decodificarla. De eso se encarga el propio driver, a partir del `Content-Encoding` de la respuesta, de modo que todas las API de lectura habituales (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, Dapper, EF Core, linq2db) funcionan sobre una respuesta comprimida sin necesidad de configurar nada. Decodifica `lz4`, `zstd`, `gzip`, `deflate` y `br`; `snappy` no es compatible.

De forma predeterminada, el driver anuncia **`zstd, lz4, gzip, deflate`**, y ClickHouse responde con `zstd`. Para elegir otra opción, define `Accept-Encoding` tú mismo, a nivel de client:

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

por consulta, que tiene prioridad:

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

o en la cadena de conexión, para usuarios de ORM que nunca utilizan `ClickHouseClientSettings`:

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

Definirlo también fuerza `enable_http_compression=1` en la URL, algo que ClickHouse exige para tener en cuenta el header siquiera —incluso cuando `UseCompression` es `false`, ya que nombrar un códec de forma explícita se interpreta como una solicitud de compresión—. Si no se define ningún valor, `UseCompression=false` no envía ningún `Accept-Encoding`.

`Accept-Encoding` puede definirse en cuatro lugares. Gana el primero de ellos que nombre un códec:

1. `QueryOptions.AcceptEncoding` (o `ClickHouseCommand.AcceptEncoding`)
2. `CustomHeaders["Accept-Encoding"]` en la consulta
3. `CustomHeaders["Accept-Encoding"]` en el client
4. `ClickHouseClientSettings.AcceptEncoding`, o el keyword `AcceptEncoding` de la cadena de connection

Si ninguno lo hace, el driver envía su lista predeterminada. Un valor que no nombre ningún códec (null, vacío,
espacios en blanco o solo comas) se considera no definido y pasa al siguiente lugar. Para desactivar la compresión, use `identity`.

**Es el servidor, no el client, quien elige el códec.** ClickHouse analiza `Accept-Encoding` en busca de tokens siguiendo su propio orden de preferencia fijo —`zstd` > `br` > `lz4` > `snappy` > `gzip` > `deflate`— e ignora tanto el orden en que los enumere como cualquier valor q. Por tanto, el header es un anuncio de capacidades más que una exigencia, y la única forma de dirigir la elección es omitir determinados tokens. La lista predeterminada incluye `zstd`, de modo que una consulta predeterminada se responde con zstd; los tokens restantes actúan como fallback. `br` puede decodificarse, pero no se anuncia de forma predeterminada.

Cómo se comparan los códecs en cuanto a tamaño del payload, CPU del servidor y CPU del client depende de sus datos, de su enlace y del valor de `http_zlib_compression_level` del servidor (valor predeterminado de fábrica: 3) — consulte [Ajuste de la compresión](#tuning-compression).

* **`http_zlib_compression_level`.** Ese SETTING se aplica a todos los códecs HTTP y su valor predeterminado es 3. Conviene ajustarlo en función de sus datos, la velocidad del enlace y el uso de CPU.
* **Un client limitado por CPU en un enlace rápido.** El driver decodifica el cuerpo de la respuesta en el hilo que realiza la llamada, por lo que, cuando la red no es el cuello de botella, la velocidad de decodificación del lado del client puede convertirse en el factor limitante.

Solicite un códec distinto por consulta, o para todo el client, siempre que se dé alguno de estos casos:

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

Dado que la decisión se toma a partir de la respuesta, el cuerpo se decodifica siempre que su `Content-Encoding` así lo indique, con independencia de lo que se haya solicitado: si está ausente o es `identity`, pasa sin modificarse; si se trata de un códec compatible, se decodifica; y en cualquier otro caso se lanza un error que lo nombra. No existe riesgo de doble decodificación: si el `AutomaticDecompression` de un handler proporcionado por el llamador ya ha decodificado un cuerpo, también elimina el `Content-Encoding`, de modo que el driver ve plaintext y lo deja intacto.

**Los resultados sin procesar no anuncian ningún códec.** `ExecuteRawResultAsync` (y los métodos públicos `PostStreamAsync` / `InsertRawStreamAsync`) entregan su cuerpo tal cual, de modo que, salvo que se indique explícitamente un códec, no solicitan ninguno: nada en el driver decodifica un cuerpo así, por lo que ofrecer un códec ahí convertiría silenciosamente una exportación en un archivo comprimido. Por tanto, la regla es sencilla e independiente de cómo esté configurado el `HttpClient`: **un cuerpo literal llega exactamente como lo envió el servidor, y el servidor envía plaintext salvo que se haya solicitado un códec.** Solicitarlo (a nivel de client o por consulta) es la manera de exportar bytes comprimidos de forma intencionada.

Un `AcceptEncoding` explícito (en cualquiera de los dos niveles) sigue aplicándose a las peticiones sin procesar, y `ClickHouseRawResult.ReadDecompressedStreamAsync()` decodifica el resultado cuando así se desee; `ReadAsStreamAsync`, `ReadAsByteArrayAsync`, `ReadAsStringAsync` y `CopyToAsync` siempre devuelven los bytes exactamente como llegaron.

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

Lee por completo el stream devuelto antes de que salga de ámbito, como se muestra arriba. Cuando la respuesta *sí* está comprimida, obtienes un decodificador creado con `leaveOpen`, de modo que liberarlo deja intacta la respuesta; cuando **no** está comprimida, obtienes el propio stream de contenido HTTP, así que liberarlo cierra el cuerpo. En cualquier caso, `ClickHouseRawResult` es el propietario de la respuesta: no llames a sus otros miembros de lectura después de haber liberado el stream. Liberar el `ClickHouseRawResult` siempre es obligatorio y, por sí solo, suficiente: libera tanto la respuesta como cualquier decodificador insertado aquí (los decodificadores retienen búferes agrupados). Por lo tanto, el `await using` anterior es opcional y puede conservarse sin riesgo. Las llamadas secuenciales repetidas devuelven el mismo stream; el tipo no es seguro para usarse de forma concurrente.

Consulta [Select\_007\_ResponseCompression.cs](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Select/Select_007_ResponseCompression.cs) para ver un ejemplo ejecutable.

<h4 id="insert-compression">
  Compresión de inserciones (solicitudes)
</h4>

Zstd es el códec predeterminado para las inserciones: `InsertOptions.Compressor` toma inicialmente el valor `ZstdCompressor.Default`,
es decir, zstd de nivel 3. Asígnele otro compresor para cambiar el códec, o `null` para enviar el
cuerpo sin comprimir.

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

El driver incluye cuatro códecs. Cada uno cuenta con una instancia `Default` y con un constructor que recibe un nivel
y el tamaño del write buffer:

| Compresor | `Content-Encoding` | Constructor | `Default` |
| - | - | - | - |
| `ZstdCompressor` | `zstd` | `(int level = 3, int bufferSize = 262144)` | nivel 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>
  *Comparta las instancias de compresor.* Cada `Default` es una única instancia compartida, y los cuatro compresores
  pueden usarse de forma segura desde varios hilos a la vez, que es justo lo que ocurre cuando
  `InsertOptions.MaxDegreeOfParallelism` es mayor que 1, ya que cada insert usa un compresor por
  batch. Ninguno de ellos implementa `IDisposable`. Cree su propia instancia una sola vez y reutilícela, del
  mismo modo en que se usa `Default`.
</Note>

<h5 id="custom-compressor">
  Un códec personalizado
</h5>

`IClickHouseCompressor` es público y una implementación solo debe proporcionar dos miembros:

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

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

El servidor debe aceptar el `Content-Encoding` que indiques. Los demás miembros —
`Decompress`, `MethodByte`, `MaxEncodedLength`, `Encode` y `Decode`— tienen implementaciones
predeterminadas que lanzan `NotSupportedException`, así que sobrescribe solo los que necesite tu códec.
Implementa `Decompress` para decodificar los cuerpos de respuesta además de comprimir las solicitudes, y lanza
`InvalidDataException` desde el stream que devuelve cuando un cuerpo esté corrupto o tenga un formato incorrecto.

`InsertOptions.Compressor` solo rige los insert binarios. Los demás cuerpos de solicitud del driver se comprimen según reglas distintas y ninguno pasa por él:

* **Toda solicitud de texto SQL** (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, `ExecuteRawResultAsync`, la capa ADO.NET) envía su statement con `Content-Encoding: gzip` siempre que `UseCompression` sea `true`, es decir, de forma predeterminada. El códec no es configurable: `AcceptEncoding` solo afecta a la respuesta, así que la elección se reduce a gzip o nada. `Compression=false` envía el statement sin comprimir. Los statements son pequeños, por lo que rara vez merece la pena preocuparse por esto, pero conviene saberlo cuando estés inspeccionando solicitudes en un proxy o en una captura de packets.
* **Un cuerpo multiparte** —una consulta cuyos parámetros se envían como form data (`UseFormDataParameters=true`)— siempre se envía sin comprimir, diga lo que diga `UseCompression`.
* **Una carga sin procesar** (`InsertRawStreamAsync`, `PostStreamAsync`) usa su propio indicador por llamada y no consulta ni `UseCompression` ni `InsertOptions.Compressor`: gzip cuando el indicador está activado y sin comprimir en caso contrario. Ten en cuenta que el parámetro `useCompression` de `InsertRawStreamAsync` es `true` de forma predeterminada, por lo que una carga sin procesar se comprime con gzip a menos que pases `false`, incluso con `Compression=false` en el client.

***

<h3 id="tuning-compression">
  Ajustar la compresión
</h3>

La compresión sacrifica CPU a cambio de bytes. Que compense o no depende casi por completo de la velocidad de su
enlace en relación con la rapidez con la que se ejecuta el códec. No existe una SETTING que sirva para
todos los casos.

<h4 id="the-one-number-that-decides-it">
  La única cifra que lo decide
</h4>

Comprimir merece la pena siempre que el códec sea más rápido que la red.

Ese umbral es más bajo de lo que la mayoría espera en la ruta de lectura, porque ClickHouse comprime
las respuestas HTTP en un solo hilo dentro del búfer de salida. Medido en un servicio de ClickHouse Cloud
de 16 vCPU (`hits`, RowBinary, nivel 3), el servidor genera salida comprimida a aproximadamente 100-200 MB/s.

Así pues, para un resultado grande, y suponiendo que se procesa una sola consulta a la vez, la compresión deja de compensar en torno a los 100 MB/s. Un único flujo HTTPS
dentro de una misma región de la nube suele superar esa cifra, mientras que todo lo que atraviesa internet pública, una VPN o el límite de una región normalmente queda por debajo.

La ruta de inserción tolera la compresión hasta velocidades de enlace más altas, porque tu cliente comprime en un núcleo propio y suele ser más rápido que la compresión de respuestas del servidor.

<h4 id="rough-guide-by-deployment">
  Guía aproximada por implementación
</h4>

| Dónde se ejecuta tu cliente | Ancho de banda típico | Lecturas | Inserciones |
| - | - | - | - |
| Mismo host / loopback | > 500 MB/s | `identity` | `lz4` es lo más rápido, o ninguna |
| Misma región, misma nube | \~100–500 MB/s | `identity` o `lz4` | `zstd:1` |
| Entre regiones, misma nube | \~10–100 MB/s | `zstd` | `zstd:3` |
| Internet / VPN / nube distinta | \< 25 MB/s | `zstd` | `zstd:3` |
| Conexión tarificada o muy limitada | \< 5 MB/s | `zstd` | `zstd:5`+ o `br` |

Hay tres aspectos que esta tabla no refleja:

* **Coste de salida:** si te facturan la transferencia de datos, los bytes tienen un precio más allá de la latencia, lo
  que inclina la balanza hacia una mayor compresión al margen de la velocidad del enlace.
* **Resultados pequeños:** todo lo anterior se refiere a payloads grandes. En respuestas pequeñas el códec apenas
  importa y lo que domina es el overhead por petición.
* **Las inserciones en paralelo elevan los umbrales de inserción.** Todas las cifras de throughput anteriores corresponden a un *único* hilo. `InsertOptions.MaxDegreeOfParallelism` es `1` de forma predeterminada, pero al aumentarlo los batches se comprimen de forma concurrente, con lo que la tasa de codificación agregada del cliente escala aproximadamente con los núcleos que le asignes. Así, en un enlace rápido, comprimir una inserción en paralelo puede seguir compensando muy por encima de la velocidad a la que deja de compensar en una de un solo hilo. Trata las filas de inserción de la tabla como un *mínimo* y, si ya agrupas en paralelo, vuelve a hacer pruebas antes de concluir que tu enlace es demasiado rápido para la compresión.

La ruta de lectura solo se paraleliza entre varias consultas.

<h4 id="choosing-a-codec">
  Elegir un códec
</h4>

| Codec | Ratio | Úsalo cuando | Ten cuidado con |
| - | - | - | - |
| `lz4` | el más bajo | Enlaces rápidos; la CPU escasea más que el ancho de banda. Es con diferencia el más barato de decodificar y el más rápido con resultados pequeños, lo que lo convierte en el códec que conviene indicar cuando quieres apartarte del valor predeterminado zstd. | **No tiene codificador entrópico**, así que con datos sesgados pero no repetitivos (largas secuencias de texto numérico, por ejemplo) su ratio queda muy por detrás del resto. También es el códec más penalizado al subir `http_zlib_compression_level`: pasar del nivel 1 al 3 le cuesta \~2,7× más de CPU a cambio de \~29 % menos bytes. |
| `zstd` | alto | Es la opción de propósito general siempre que haya una red real de por medio. Ofrece el mejor ratio por CPU en el rango que importa y, en el nivel 3, supera a `lz4` en bytes *y* en CPU del servidor *y* en tiempo de reloj. | es más caro de **decodificar** que `lz4` (1,6× en el nivel 3 según nuestras mediciones, aunque en el nivel 1 ambos son comparables), y el driver decodifica en el hilo que hace la llamada. En concreto, con `http_zlib_compression_level=1` consume algo *más* de CPU del servidor que `lz4`. |
| `gzip` | medio | Interoperabilidad: lo entienden universalmente los proxies y los gateways. | En nuestras mediciones queda por detrás de `lz4` y `zstd` en todos los ejes: genera más bytes que `zstd` y, además, cuesta varias veces más CPU codificar y de 5 a 9× más decodificar. Elígelo por compatibilidad, no por rendimiento. |
| `br` | el más alto en niveles bajos | El ancho de banda es realmente la restricción determinante y puedes permitirte gastar CPU en ello. | Se desploma en los niveles altos: con `http_zlib_compression_level=6` medimos entre 3 y 4× la CPU del servidor que consume `zstd`. No se anuncia de forma predeterminada, porque tiene prioridad sobre todos los token de fallback de la lista predeterminada. |

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

La compresión de las respuestas se controla mediante un único SETTING de servidor, `http_zlib_compression_level`, que se aplica a *todos* los códecs HTTP, no solo a zlib. Su valor predeterminado es 3.

No lo modifique salvo que tenga mediciones que lo justifiquen. Por encima del valor predeterminado apenas reduce el tamaño a costa de mucha CPU (para `zstd`, pasar de 3 a 6 duplica aproximadamente la CPU del servidor a cambio de un \~14% menos de bytes), y `br` se vuelve patológico. Por debajo, en el nivel 1, el panorama sí cambia de verdad: `lz4` resulta mucho más barato y `zstd` pierde su ventaja de CPU frente a él. Configúrelo por consulta si lo necesita:

```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">
  Medir su propio punto de cruce
</h4>

La forma más rápida de optimizar la elección del códec y del nivel de compresión es cronometrar la misma consulta con varios códecs y comparar los 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 este mismo panorama desde el lado del servidor, lee los `ProfileEvents` de `system.query_log`: establece
`QueryOptions.QueryId` para poder localizar la fila:

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

Una trampa si realiza el benchmark por su cuenta: un simple `LIMIT n` sin `ORDER BY` devuelve *filas distintas
en cada ejecución*, por lo que cada repetición comprime datos diferentes y los ratios se vuelven ruido. Compare
siempre contra un conjunto de resultados fijo.

***

<h3 id="raw-stream-insert">
  Inserción con stream sin procesar
</h3>

Utilice `InsertRawStreamAsync` para insertar datos directamente desde archivos o streams en memoria en formatos como CSV, JSON, Parquet o cualquier [formato compatible con ClickHouse](/es/reference/formats/index).

**Insertar desde un archivo 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>
  *El driver toma posesión del stream.* `InsertRawStreamAsync` y `PostStreamAsync` liberan el
  stream que les proporcionas una vez que finaliza la solicitud, tanto si tuvo éxito como si falló. No lo liberes
  tú mismo ni lo reutilices después: por eso el ejemplo anterior no envuelve el
  `FileStream` en un `using`.

  Un `using` propio se ejecuta cuando el driver ya ha liberado el stream. En el caso de un `FileStream` o
  un `MemoryStream`, esa segunda llamada es inofensiva, pero en un stream cuyo `Dispose` devuelve un búfer
  a un grupo o reduce un contador de referencias, el recurso se libera dos veces.

  La posesión se transfiere solo una vez aceptados los argumentos: si la llamada lanza `ArgumentException` o
  `ArgumentNullException` porque falta la tabla, el stream o el format, el stream sigue siendo tuyo.
</Warning>

<Note>
  Consulta la [documentación de configuración de formats](/es/reference/settings/formats) para conocer las opciones que controlan el comportamiento de la ingestión de datos.
</Note>

***

<h3 id="more-examples">
  Más ejemplos
</h3>

Para ver más ejemplos prácticos de uso, consulta el [directorio de ejemplos](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples) en el repositorio de GitHub.

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

La biblioteca ofrece compatibilidad completa con ADO.NET mediante `ClickHouseConnection`, `ClickHouseCommand` y `ClickHouseDataReader`. Esta API es necesaria para la integración con ORM (Dapper, Linq2db) y cuando necesita las abstracciones estándar de bases de datos de .NET.

<h3 id="ado-net-datasource">
  Gestión del ciclo de vida con ClickHouseDataSource
</h3>

**Cree siempre conexiones desde un `ClickHouseDataSource`** para garantizar una gestión correcta del ciclo de vida y del pool de conexiones. El DataSource administra internamente un único `ClickHouseClient`, y todas las conexiones comparten su pool de conexiones HTTP.

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

// Crear DataSource una vez (registrar como singleton en DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Crear conexiones ligeras según sea necesario
await using var connection = await dataSource.OpenConnectionAsync();

// Usar la conexión
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();
```

Para la inyección de dependencias:

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

// En el servicio
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

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

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Usa la conexión...
    }
}
```

<Warning>
  **No cree `ClickHouseConnection` directamente** en producción. Cada instanciación directa crea un nuevo cliente HTTP y un nuevo pool de conexiones, lo que puede provocar agotamiento de sockets con carga elevada:

  ```csharp theme={null}
  // NO HAGA ESTO: crea un pool de conexiones nuevo cada vez
  using var conn = new ClickHouseConnection("Host=localhost");
  await conn.OpenAsync();
  ```

  En su lugar, use siempre `ClickHouseDataSource` o comparta una sola instancia de `ClickHouseClient`.
</Warning>

***

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

Cree comandos a partir de una conexión para ejecutar SQL:

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

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

// Ejecutar y leer resultados
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}
```

Métodos del comando:

* `ExecuteNonQueryAsync()` - Para INSERT, UPDATE, DELETE y sentencias DDL
* `ExecuteScalarAsync()` - Devuelve la primera columna de la primera fila
* `ExecuteReaderAsync()` - Devuelve un `ClickHouseDataReader` para recorrer los resultados

***

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

`ClickHouseDataReader` proporciona acceso tipado a los resultados de la consulta:

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

while (reader.Read())
{
    // Acceso por índice de columna
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // Acceso por nombre de columna
    var email = reader.GetString("email");

    // Acceso genérico
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // Comprobar si es NULL
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}
```

<h4 id="ado-net-reader-enum-ordinal">
  Lectura del ordinal de un enum
</h4>

Una columna `Enum8` o `Enum16` se materializa como su etiqueta: `GetFieldType` devuelve `string`, y tanto
`GetString` como `GetValue` y `GetFieldValue<string>` proporcionan la etiqueta. Los accesores numéricos
lanzan `InvalidCastException` sobre una columna de enum, porque el valor almacenado es una cadena.

Utilice `TryGetEnumOrdinal` para obtener el número que hay detrás de la etiqueta:

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

Devuelve `true` y establece `value` para una columna `Enum8`/`Enum16`, y para una columna
`Nullable(Enum...)` cuya celda no sea NULL. Devuelve `false`, con `value` establecido en `0`, para una celda NULL o para
cualquier columna que no sea un enum. El ordinal es el
valor con signo recibido por el wire, por lo que puede ser negativo, y un ordinal `Enum16` puede ocupar más de un
byte.

<h2 id="best-practices">
  Buenas prácticas
</h2>

<h3 id="best-practices-connection-lifetime">
  Tiempo de vida de las conexiones y pool de conexiones
</h3>

`ClickHouse.Driver` usa `System.Net.Http.HttpClient` internamente. `HttpClient` tiene un pool de conexiones por `endpoint`. Como consecuencia:

* Las sesiones de la base de datos se multiplexan a través de conexiones HTTP administradas por el pool de conexiones.
* El pool recicla automáticamente las conexiones HTTP.
* Las conexiones pueden permanecer activas después de desechar los objetos `ClickHouseClient` o `ClickHouseConnection`.

**Patrones recomendados:**

| Escenario | Enfoque recomendado |
| - | - |
| Uso general | Use un `ClickHouseClient` singleton |
| ADO.NET / ORMs | Use `ClickHouseDataSource` (crea conexiones que comparten el mismo pool) |
| Entornos de DI | Registre `ClickHouseClient` o `ClickHouseDataSource` como singleton con `IHttpClientFactory` |

<Warning>
  Al usar un `HttpClient` o `HttpClientFactory` personalizado, asegúrese de que `PooledConnectionIdleTimeout` esté configurado con un valor menor que el `keep_alive_timeout` del servidor para evitar errores debidos a conexiones semicerradas. El valor predeterminado de `keep_alive_timeout` en implementaciones de Cloud es de 10 segundos.
</Warning>

<Warning>
  Evite crear varias instancias de `ClickHouseClient` o instancias independientes de `ClickHouseConnection` sin un `HttpClient` compartido. Cada instancia crea su propio pool de conexiones.
</Warning>

***

<h3 id="best-practice-datetime">
  Gestión de DateTime
</h3>

1. **Usa UTC siempre que sea posible.** Almacena las marcas de tiempo como columnas `DateTime('UTC')` y usa `DateTimeKind.Utc` en tu código. Esto elimina la ambigüedad de la zona horaria.

2. **Usa `DateTimeOffset` para gestionar explícitamente la zona horaria.** Siempre representa un instante específico e incluye la información de desplazamiento.

3. **Especifica la zona horaria en las indicaciones de tipo de SQL.** Al usar parámetros con valores `DateTime` `Unspecified` destinados a columnas que no son UTC, incluye la zona horaria en el 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">
  Inserciones asíncronas
</h3>

Las [inserciones asíncronas](/es/concepts/features/operations/insert/asyncinserts) trasladan la responsabilidad de agrupar en lotes del cliente al servidor. En lugar de requerir el agrupamiento en lotes del lado del cliente, el servidor guarda en un búfer los datos entrantes y los vuelca al almacenamiento en función de umbrales configurables. Esto resulta útil en escenarios de alta concurrencia, como las cargas de trabajo de observabilidad, donde muchos agentes envían payloads pequeños.

Habilite las inserciones asíncronas mediante `CustomSettings` o la cadena de conexión:

```csharp theme={null}
// Usando CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Recomendado: esperar confirmación de flush

// O mediante connection string
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"
```

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

| Modo | Comportamiento | Caso de uso |
| - | - | - |
| `wait_for_async_insert=1` | La inserción devuelve después de que los datos se escriben en disco. Los errores se devuelven al cliente. | **Recomendado** para la mayoría de las cargas de trabajo |
| `wait_for_async_insert=0` | La inserción devuelve inmediatamente cuando los datos se almacenan en el búfer. No se garantiza que los datos se persistan. | Solo cuando la pérdida de datos sea aceptable |

<Warning>
  Con `wait_for_async_insert=0`, los errores solo aparecen durante el vaciado y no pueden rastrearse hasta la inserción original. El cliente tampoco proporciona contrapresión, lo que puede sobrecargar el servidor.
</Warning>

**Configuraciones clave:**

| Configuración | Descripción |
| - | - |
| `async_insert_max_data_size` | Vacía el búfer cuando alcanza este tamaño (bytes) |
| `async_insert_busy_timeout_ms` | Vacía el búfer cuando se alcanza este tiempo de espera (milisegundos) |
| `async_insert_max_query_number` | Vacía el búfer cuando se acumula esta cantidad de consultas |

***

<h3 id="best-practices-sessions">
  Sesiones
</h3>

Habilita las sesiones solo cuando necesites funcionalidades con estado en el servidor, por ejemplo:

* Tablas temporales (`CREATE TEMPORARY TABLE`)
* Mantener el contexto de la consulta entre varias sentencias
* Ajustes a nivel de sesión (`SET max_threads = 4`)

Cuando las sesiones están habilitadas, las solicitudes se serializan para evitar el uso concurrente de la misma sesión. Esto añade sobrecarga a las cargas de trabajo que no requieren estado de sesión.

```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 de ADO.NET (para compatibilidad con ORM):**

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

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

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

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

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

***

<h2 id="supported-data-types">
  Tipos de datos compatibles
</h2>

`ClickHouse.Driver` admite todos los tipos de datos de ClickHouse. Las tablas siguientes muestran la correspondencia entre los tipos de ClickHouse y los tipos nativos de .NET al leer datos de la base de datos.

<h3 id="clickhouse-native-type-map-reading">
  Correspondencia de tipos: lectura desde ClickHouse
</h3>

<h4 id="type-map-reading-integer">
  Tipos enteros
</h4>

| Tipo de ClickHouse | Tipo de .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 coma flotante
</h4>

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

***

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

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

<Note>
  La conversión de tipos decimales se controla con la configuración UseCustomDecimals.
</Note>

***

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

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

***

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

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

<Note>
  De forma predeterminada, las columnas `String` y `FixedString(N)` se devuelven como `string`. Establezca `ReadStringsAsByteArrays=true` en la cadena de conexión para leerlas como `byte[]` en su lugar. Esto es útil cuando se almacenan datos binarios que podrían no ser UTF-8 válidos.

  La configuración también se aplica a las cadenas anidadas dentro de otros tipos, por lo que `Array(String)` se lee como `byte[][]`
  y `Map(String, String)` como `Dictionary<byte[], byte[]>`, incluidas las claves. La única excepción es una
  columna `JSON`, cuyas hojas de tipo cadena siempre son texto; consulte [JSON type](#type-map-reading-json).
</Note>

***

<h4 id="type-map-reading-datetime">
  Tipos de fecha y hora
</h4>

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

ClickHouse almacena internamente los valores `DateTime` y `DateTime64` como marcas de tiempo Unix (segundos o fracciones de segundo desde la época Unix). Aunque el almacenamiento siempre está en UTC, las columnas pueden tener una zona horaria asociada que afecta a cómo se muestran e interpretan los valores.

Al leer valores `DateTime`, la propiedad `DateTime.Kind` se establece en función de la zona horaria de la columna:

| Definición de columna | DateTime.Kind devuelto | Notas |
| - | - | - |
| `DateTime('UTC')` | `Utc` | Zona horaria UTC explícita |
| `DateTime('Europe/Amsterdam')` | `Unspecified` | Se aplica el desplazamiento |
| `DateTime` | `Unspecified` | Se conserva la hora tal cual |

Para las columnas que no son UTC, el `DateTime` devuelto representa la hora local en esa zona horaria. Usa `ClickHouseDataReader.GetDateTimeOffset()` para obtener un `DateTimeOffset` con el desplazamiento correcto para esa zona horaria:

```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 las columnas **sin** una zona horaria explícita (es decir, `DateTime` en lugar de `DateTime('Europe/Amsterdam')`), el driver devuelve un `DateTime` con `Kind=Unspecified`. Esto conserva la hora local exactamente tal como está almacenada, sin hacer suposiciones sobre la zona horaria.

Si necesita un comportamiento con reconocimiento de zona horaria para columnas sin una zona horaria explícita, haga una de estas dos cosas:

1. Use zonas horarias explícitas en las definiciones de sus columnas: `DateTime('UTC')` o `DateTime('Europe/Amsterdam')`
2. Aplique usted mismo la zona horaria después de leer el valor.

***

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

| Tipo de ClickHouse | Tipo de .NET | Notas |
| - | - | - |
| Json | `JsonObject` | Predeterminado (`JsonReadMode=Binary`) |
| Json | `string` | Cuando `JsonReadMode=String` |

El tipo de retorno de las columnas JSON está determinado por la configuración `JsonReadMode`:

* **`Binary` (predeterminado)**: Devuelve `System.Text.Json.Nodes.JsonObject`. Proporciona acceso estructurado a los datos JSON, pero los tipos especializados de ClickHouse (como direcciones IP, UUIDs y decimales grandes) se convierten a su representación en cadena dentro de la estructura JSON.

* **`String`**: Devuelve el JSON sin procesar como `string`. Conserva la representación exacta del JSON de ClickHouse, lo que resulta útil cuando necesitas pasar el JSON sin analizarlo o cuando quieres encargarte tú mismo de la deserialización.

```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` es un tercer modo. Se lee exactamente igual que `Binary`, pero no envía ninguna configuración de servidor con la
consulta: úsalo en una conexión que no tenga permitido establecerla.

<h5 id="type-map-reading-json-nulls">
  path tipados y NULL
</h5>

Un path declarado en el tipo de la columna es un **path tipado**; cualquier otro path del documento es un **path dinámico**. Ambos se comportan de forma distinta cuando el valor es NULL.

Un path tipado siempre aparece en el `JsonObject`. Si se declara como `Nullable(T)` o `Dynamic`, se devuelve como un NULL de JSON tanto cuando el valor almacenado es NULL como cuando el documento no contiene ese path: ambos casos son indistinguibles:

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

Cuando se declara con un tipo no nullable, un path ausente toma el valor predeterminado del tipo: `JSON(x String)`
devuelve `{"x":""}` y `JSON(x Int64)` devuelve `{"x":0}`.

Un path dinámico cuyo valor es null se elimina por completo del objeto, por lo que `ContainsKey` devuelve
false para él. Leer `{"x":null}` desde una columna `JSON` simple devuelve `{}`.

Los paths tipados anidados construyen sus ancestros, de modo que `JSON(a.b Nullable(Int64))` produce `{"a":{"b":null}}`
incluso para un documento vacío.

<Note>
  Esto es lo que renderiza el propio server, por lo que los modos `Binary` y `String` ahora coinciden. Antes de la versión 1.4.0, un
  path tipado que contuviera null se eliminaba del `JsonObject`, lo que hacía que `{"x":null}` se leyera como
  `{}`; y, en el caso de un path anidado como `JSON(a.b Nullable(Int64))`, desaparecía todo el subárbol `a`.
</Note>

<h5 id="type-map-reading-json-strings">
  Cadenas dentro de una columna JSON
</h5>

Las hojas de tipo cadena dentro de una columna `JSON` siempre se devuelven como texto, sea cual sea el valor de
`ReadStringsAsByteArrays`: `JsonValue` no dispone de una forma de array de bytes, por lo que un `byte[]` se
representaría como base64. Esto vale para `String`, `FixedString` y para los tipos envueltos en
`LowCardinality`, `Nullable` o `SimpleAggregateFunction`, así como para las cadenas dentro de `Array` y `Map`,
incluidas las claves del map.

<Note>
  Un array de bytes cuyo tipo el lector de JSON no puede determinar sí se representa como base64: un
  path tipado como `Variant` o `Dynamic` contiene un valor cuyo tipo solo se conoce fila a fila, de modo que una cadena
  bajo `Variant(Array(UInt8), String)` se devuelve codificada en base64. Esto ocurre igual con ambas configuraciones.

  Un tipo de clave de map JSON que no sea exactamente `String` —por ejemplo, `Map(LowCardinality(String), String)`— lanza `NotSupportedException`.
</Note>

<h5 id="overlapping-paths">
  Rutas superpuestas
</h5>

ClickHouse acepta una columna que declara una ruta tanto como valor como en calidad de padre de otra
ruta, por ejemplo `JSON(a Int64, a.b Int64)`. Ambas rutas están presentes en cada fila, por lo que el servidor
representa la fila con una clave duplicada: `{"a":0,"a":{"b":7}}`. Un `JsonObject` no puede contener dos valores
para una misma clave, de modo que `JsonReadMode.Binary` lanza una `SerializationException` que indica ambas rutas. Lo
mismo ocurre cuando el valor es un `Map`, como en `JSON(a Map(String, Int64))` leído de una fila que
también tiene un `a.b` dinámico.

Esto solo se aplica cuando ambos lados contienen un valor en esa fila. Un lado que no contiene nada —un valor nulo,
un objeto vacío o un subárbol cuyos valores son todos nulos— cede ante el lado que sí tiene los datos,
sea cual sea de las dos rutas la que el servidor envíe primero. Por lo tanto, una superposición declarada con tipos `Nullable` rellena un solo lado por fila y se lee sin
error: `JSON(a Nullable(Int64), a.b Nullable(Int64))` devuelve `{"a":5}` y `{"a":{"b":7}}`, tal como
se espera.

Lea una columna de este tipo con `JsonReadMode.String` para obtener el texto JSON del servidor sin cambios, incluida la clave
duplicada.

Establezca `AllowDuplicateJsonKeys` para seguir leyendo la columna como un `JsonObject` en lugar de lanzar una excepción. En ese caso, el
driver conserva el último de los dos valores que lleva la fila y descarta el otro, por lo que el
resultado es con pérdida: `JSON(a Int64, a.b Int64)` que contiene `{"a.b":7}` se lee como `{"a":0}`. Una ruta que
contiene un valor y cuyo padre contiene un valor escalar o un array sigue lanzando una excepción, porque un subárbol no puede
ubicarse bajo ninguno de los dos.

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

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

***

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

| ClickHouse Type | .NET Type | Notas |
| - | - | - |
| Map(K, V) | `Dictionary<K, V>` | Predeterminado (`MapReadMode=Dictionary`) |
| Map(K, V) | `List<KeyValuePair<K, V>>` | Cuando `MapReadMode=KeyValuePairs` |

Un `Map(K, V)` de ClickHouse es físicamente un `Array(Tuple(K, V))` y puede contener varias entradas con la misma clave. Un `Dictionary` no puede, por lo que en el modo predeterminado una clave repetida conserva únicamente su último valor y los pares anteriores se descartan. El ajuste `MapReadMode` determina la representación:

* **`Dictionary` (predeterminado)**: devuelve `Dictionary<K, V>`.

* **`KeyValuePairs`**: devuelve `List<KeyValuePair<K, V>>` en el orden en que el server envió los pares, con lo que se conservan todos, incluidas las entradas que repiten una clave.

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

El mode selecciona el tipo de framework de una columna `Map`, por lo que también se aplica a `GetFieldValue<T>`, a los tipos de schema que informa el driver y a la correspondencia de propiedades POCO. Se aplica en cualquier lugar donde aparezca un map dentro del árbol de tipos de una columna, incluidos `Array(Map(...))`, `Map(K, Map(...))`, `Tuple(..., Map(...))` y `Dynamic`.

Ambas representaciones se aceptan en la ruta de escritura en cualquiera de los dos modes; consulte [escritura de maps](#type-map-writing-other).

***

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

| Tipo de ClickHouse | Tipo de .NET |
| - | - |
| UUID | `Guid` |
| IPv4 | `IPAddress` |
| IPv6 | `IPAddress` |
| Nothing | `DBNull` |
| Dynamic | Vea la nota |
| Array(T) | `T[]` (el `Array(Array(T))` anidado se lee como un `T[][]` escalonado; use `reader.GetFieldValue<T[,]>(ordinal)` para materializar datos rectangulares como una matriz CLR multidimensional) |
| Tuple(T1, T2, ...) | `Tuple<T1, T2, ...>` / `LargeTuple` |
| Map(K, V) | `Dictionary<K, V>`, o `List<KeyValuePair<K, V>>` cuando `MapReadMode=KeyValuePairs` — vea [Map type](#type-map-reading-map) |
| Nullable(T) | `T?` |
| Enum8 | `string` |
| Enum16 | `string` |
| LowCardinality(T) | Igual que T |
| SimpleAggregateFunction | Igual que el tipo subyacente |
| Nested(...) | `Tuple[]` |
| Variant(T1, T2, ...) | Vea la nota |
| QBit(T, dimension) | `T[]` |

<Note>
  Los tipos Dynamic y Variant se convertirán al tipo correspondiente según el tipo subyacente real de cada fila.
</Note>

***

<h4 id="type-map-reading-geometry">
  Tipos de geometría
</h4>

| Tipo de ClickHouse | Tipo de .NET |
| - | - |
| Point | `Tuple<double, double>` |
| Ring | `Tuple<double, double>[]` |
| LineString | `Tuple<double, double>[]` |
| Polygon | `Ring[]` |
| MultiLineString | `LineString[]` |
| MultiPolygon | `Polygon[]` |
| Geometry | Consulte la nota |

<Note>
  El tipo Geometry es un tipo Variant que puede contener cualquiera de los tipos de geometría. Se convertirá al tipo correspondiente.
</Note>

***

<h3 id="clickhouse-native-type-map-writing">
  Correspondencia de tipos: escritura en ClickHouse
</h3>

Al insertar datos, el driver convierte los tipos de .NET en sus correspondientes tipos de ClickHouse. Las tablas siguientes muestran qué tipos de .NET se admiten para cada tipo de columna de ClickHouse.

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

| Tipo de ClickHouse | Tipos .NET aceptados | Notas |
| - | - | - |
| Int8 | `sbyte`, cualquier tipo compatible con `Convert.ToSByte()` | |
| UInt8 | `byte`, cualquier tipo compatible con `Convert.ToByte()` | |
| Int16 | `short`, cualquier tipo compatible con `Convert.ToInt16()` | |
| UInt16 | `ushort`, cualquier tipo compatible con `Convert.ToUInt16()` | |
| Int32 | `int`, cualquier tipo compatible con `Convert.ToInt32()` | |
| UInt32 | `uint`, cualquier tipo compatible con `Convert.ToUInt32()` | |
| Int64 | `long`, cualquier tipo compatible con `Convert.ToInt64()` | |
| UInt64 | `ulong`, cualquier tipo compatible con `Convert.ToUInt64()` | |
| Int128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, cualquier tipo compatible con `Convert.ToInt64()` | |
| UInt128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, cualquier tipo compatible con `Convert.ToInt64()` | |
| Int256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, cualquier tipo compatible con `Convert.ToInt64()` | |
| UInt256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, cualquier tipo compatible con `Convert.ToInt64()` | |

***

<h4 id="type-map-writing-floating-point">
  Tipos de coma flotante
</h4>

| Tipo de ClickHouse | Tipos de .NET aceptados | Notas |
| - | - | - |
| Float32 | `float`, cualquier tipo compatible con `Convert.ToSingle()` | |
| Float64 | `double`, cualquier tipo compatible con `Convert.ToDouble()` | |
| BFloat16 | `float`, cualquier tipo compatible con `Convert.ToSingle()` | Se trunca al formato brain float de 16 bits |

***

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

| Tipo de ClickHouse | Tipos de .NET aceptados | Notas |
| - | - | - |
| Bool | `bool` | |

***

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

| Tipo de ClickHouse | Tipos de .NET aceptados | Notas |
| - | - | - |
| String | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | Los tipos binarios se escriben directamente; los streams pueden ser posicionables o no |
| FixedString(N) | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | String se codifica en UTF-8 y se rellena; los tipos binarios deben tener exactamente N bytes |

***

<h4 id="type-map-writing-datetime">
  Tipos de fecha y hora
</h4>

| Tipo de ClickHouse | Tipos de .NET aceptados | Notas |
| - | - | - |
| Date | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos de NodaTime | Convertido a días Unix como UInt16; rango admitido `[1970-01-01, 2149-06-06]` |
| Date32 | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos de NodaTime | Convertido a días Unix como Int32; rango admitido `[1900-01-01, 2299-12-31]` |
| DateTime | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos de NodaTime | Consulta más abajo para obtener detalles; rango admitido `[1970-01-01, 2106-02-07 06:28:15]` UTC |
| DateTime32 | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos de NodaTime | Igual que DateTime |
| DateTime64 | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos de NodaTime | Precisión basada en el parámetro Scale |
| Time | `TimeSpan`, `TimeOnly`, `int` | Limitado a ±999:59:59; `int` se trata como segundos |
| Time64 | `TimeSpan`, `TimeOnly`, `decimal`, `double`, `float`, `int`, `long`, `string` | La cadena se interpreta como `[-]HHH:MM:SS[.fraction]`; limitado a ±999:59:59.999999999 |

<Note>
  **Valores fuera de rango**

  En la ruta de escritura binaria, los valores `Date`, `Date32`, `DateTime` y `DateTime32` fuera de su rango admitido lanzan `ArgumentOutOfRangeException` en el momento de `Write`, indicando el tipo de columna y el rango admitido. Anteriormente, los valores fuera de rango podían truncarse silenciosamente a través de un entero de 32 bits y ser reinterpretados por el servidor, lo que producía timestamps reales pero incorrectos.
</Note>

El driver respeta `DateTime.Kind` al escribir valores:

| DateTime.Kind | Parámetros HTTP | Copia masiva |
| - | - | - |
| Utc | Se conserva el instante | Se conserva el instante |
| Local | Se conserva el instante | Se conserva el instante |
| Unspecified | Se trata como hora local en la zona horaria del tipo de parámetro (UTC de forma predeterminada) | Se trata como hora local en la zona horaria de la columna |

Los valores `DateTimeOffset` siempre conservan el instante exacto.

**Ejemplo: DateTime UTC (se conserva el instante)**

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

**Ejemplo: DateTime sin especificar (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
```

**Recomendación:** para obtener el comportamiento más simple y predecible, use `DateTimeKind.Utc` o `DateTimeOffset` para todas las operaciones con DateTime. Esto garantiza que su código funcione de forma coherente independientemente de la zona horaria del servidor, la zona horaria del cliente o la zona horaria de la columna.

<h4 id="datetime-http-param-vs-bulkcopy">
  Parámetros HTTP vs Bulk Copy
</h4>

Hay una diferencia importante entre la vinculación de parámetros HTTP y Bulk Copy al escribir valores DateTime `Unspecified`:

**Bulk Copy** conoce la zona horaria de la columna de destino e interpreta correctamente los valores `Unspecified` en esa zona horaria.

**HTTP Parameters** no conocen automáticamente la zona horaria de la columna. Debe especificarla en la indicación de tipo de SQL:

```csharp theme={null}
// CORRECTO: Zona horaria en la indicación de tipo SQL - el tipo se extrae automáticamente
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// INCORRECTO: Sin indicación de zona horaria, se interpreta como UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// ¡El valor de cadena "2024-01-15 14:30:00" se interpreta como UTC, no como hora de Ámsterdam!
```

| `DateTime.Kind` | Columna de destino | Parámetro HTTP (con indicación de zona horaria) | Parámetro HTTP (sin indicación de zona horaria) | Copia masiva |
| - | - | - | - | - |
| `Utc` | UTC | Se conserva el instante | Se conserva el instante | Se conserva el instante |
| `Utc` | Europe/Amsterdam | Se conserva el instante | Se conserva el instante | Se conserva el instante |
| `Local` | Cualquiera | Se conserva el instante | Se conserva el instante | Se conserva el instante |
| `Unspecified` | UTC | Se trata como UTC | Se trata como UTC | Se trata como UTC |
| `Unspecified` | Europe/Amsterdam | Se trata como hora de Ámsterdam | **Se trata como UTC** | Se trata como hora de Ámsterdam |

***

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

| Tipo de ClickHouse | Tipos de .NET aceptados | Notas |
| - | - | - |
| Decimal(P,S) | `decimal`, `ClickHouseDecimal`, cualquier tipo compatible con `Convert.ToDecimal()` | Lanza `OverflowException` si excede la precisión |
| Decimal32 | `decimal`, `ClickHouseDecimal`, cualquier tipo compatible con `Convert.ToDecimal()` | Precisión máxima: 9 |
| Decimal64 | `decimal`, `ClickHouseDecimal`, cualquier tipo compatible con `Convert.ToDecimal()` | Precisión máxima: 18 |
| Decimal128 | `decimal`, `ClickHouseDecimal`, cualquier tipo compatible con `Convert.ToDecimal()` | Precisión máxima: 38 |
| Decimal256 | `decimal`, `ClickHouseDecimal`, cualquier tipo compatible con `Convert.ToDecimal()` | Precisión máxima: 76 |

***

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

| Tipo de ClickHouse | Tipos de .NET aceptados | Notas |
| - | - | - |
| Json | `string`, `JsonObject`, `JsonNode`, cualquier objeto | El comportamiento depende del ajuste `JsonWriteMode` |

El comportamiento al escribir JSON está controlado por el ajuste `JsonWriteMode`:

| Tipo de entrada | `JsonWriteMode.String` (predeterminado) | `JsonWriteMode.Binary` |
| - | - | - |
| `string` | Se pasa directamente | Lanza `ArgumentException` |
| `JsonObject` | Se serializa con `ToJsonString()` | Lanza `ArgumentException` |
| `JsonNode` | Se serializa con `ToJsonString()` | Lanza `ArgumentException` |
| POCO registrado | Se serializa con `JsonSerializer.Serialize()` | Codificación binaria con indicaciones de tipo; se admiten atributos de ruta personalizados |
| POCO no registrado / objeto anónimo | Se serializa con `JsonSerializer.Serialize()` | Lanza `ClickHouseJsonSerializationException` |

* **`String` (predeterminado)**: Acepta `string`, `JsonObject`, `JsonNode` o cualquier objeto. Todas las entradas se serializan mediante `System.Text.Json.JsonSerializer` y se envían como cadenas JSON para que el servidor las procese. Este es el modo más flexible y funciona sin registrar tipos.

* **`Binary`**: Solo acepta tipos POCO registrados. Los datos se convierten en el cliente al formato JSON binario de ClickHouse, con compatibilidad completa con indicaciones de tipo. Requiere llamar a `connection.RegisterJsonSerializationType<T>()` antes de usarlo. Escribir valores `string` o `JsonNode` en este modo lanza `ArgumentException`.

```csharp theme={null}
// El modo String predeterminado funciona con cualquier entrada
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// El modo Binary requiere habilitación explícita y registro de tipos
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
```

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

Cuando una columna JSON tiene indicaciones de tipo (p. ej., `JSON(id UInt64, price Decimal128(2))`), el driver usa estas indicaciones para serializar los valores respetando plenamente sus tipos. Esto preserva la precisión de tipos como `UInt64`, `Decimal`, `UUID` y `DateTime64`, que de otro modo la perderían al serializarse como JSON genérico.

<h5 id="json-poco-serialization">
  Serialización de POCO
</h5>

Los POCO se pueden escribir en columnas JSON de dos formas, según `JsonWriteMode`:

**Modo String (predeterminado)**: los POCO se serializan mediante `System.Text.Json.JsonSerializer`. No es necesario registrar tipos. Es el enfoque más sencillo y funciona con objetos anónimos.

**Modo binario**: los POCO se serializan usando el formato JSON binario del driver, con compatibilidad completa con indicaciones de tipo. Los tipos deben registrarse con `connection.RegisterJsonSerializationType<T>()` antes de usarlos. Este modo admite asignaciones de rutas personalizadas mediante atributos:

* **`[ClickHouseJsonPath("path")]`**: Asigna una propiedad a una ruta JSON personalizada. Es útil para estructuras anidadas o cuando el nombre de la propiedad difiere de la clave JSON deseada. **Solo funciona en modo binario.**

* **`[ClickHouseJsonIgnore]`**: Excluye una propiedad de la serialización. **Solo funciona en modo binario.**

```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; }  // No se serializa
}

// Para el modo Binary: registre el tipo y habilítelo
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Insertar un POCO: se serializa a JSON con una estructura anidada mediante atributos de ruta personalizados
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// JSON resultante: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}
```

La coincidencia entre el nombre de la propiedad y las indicaciones de tipo de la columna es sensible a mayúsculas y minúsculas. Una propiedad `UserId` solo coincidirá con una indicación definida como `UserId`, no como `userid`. Esto sigue el comportamiento de ClickHouse, que permite que rutas como `userName` y `UserName` coexistan como campos independientes.

**Limitaciones (solo en modo Binary):**

* Los tipos POCO deben registrarse en la conexión con `connection.RegisterJsonSerializationType<T>()` antes de serializarse. Si se intenta serializar un tipo no registrado, se lanza `ClickHouseJsonSerializationException`.
* Las propiedades de diccionario y array/lista requieren indicaciones de tipo en la definición de la columna para serializarse correctamente. Sin esas indicaciones, use el modo String.
* Los valores NULL en las propiedades POCO solo se escriben cuando la ruta tiene una indicación de tipo `Nullable(T)` en la definición de la columna. ClickHouse no permite tipos `Nullable` dentro de rutas JSON dinámicas, por lo que las propiedades con valor NULL sin indicación se omiten.
* Los atributos `ClickHouseJsonPath` y `ClickHouseJsonIgnore` se ignoran en modo String (solo funcionan en modo Binary).

***

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

| Tipo de ClickHouse | Tipos de .NET aceptados | Notas |
| - | - | - |
| UUID | `Guid`, `string` | La cadena se interpreta como `Guid` |
| IPv4 | `IPAddress`, `string` | Debe ser IPv4; la cadena se analiza con `IPAddress.Parse()` |
| IPv6 | `IPAddress`, `string` | Debe ser IPv6; la cadena se analiza con `IPAddress.Parse()` |
| Nothing | Any | No escribe nada (no-op) |
| Dynamic | — | **No admitido** (lanza `NotImplementedException`) |
| Array(T) | `IList`, `null` | `null` escribe un array vacío. Para los tipos anidados (`Array(Array(T))` y más profundos), se aceptan tanto las formas irregulares (`T[][]`, `List<List<T>>`) como los arrays CLR multidimensionales rectangulares (`T[,]`, `T[,,]`, …); el número de dimensiones de CLR debe coincidir con la profundidad de anidamiento de ClickHouse. |
| Tuple(T1, T2, ...) | `ITuple`, `IList` | El número de elementos debe coincidir con la aridad de la tupla. Consulta la [advertencia sobre ValueTuple](#valuetuple-caveat) para más de 7 elementos. |
| Map(K, V) | `IDictionary`, `IEnumerable<KeyValuePair<K, V>>` | Se acepta una secuencia de pares (por ejemplo, la `List<KeyValuePair<K, V>>` que produce `MapReadMode=KeyValuePairs`) en cualquiera de los modos de lectura, y puede repetir una clave. Se aplica a las inserciones binarias y a los parámetros de consulta |
| Nullable(T) | `null`, `DBNull`, o tipos aceptados por T | Escribe un byte indicador de null antes del valor |
| Enum8 | `string`, `sbyte`, tipos numéricos | La cadena se busca en el diccionario del enum |
| Enum16 | `string`, `short`, tipos numéricos | La cadena se busca en el diccionario del enum |
| LowCardinality(T) | Tipos aceptados por T | Se delega en el tipo subyacente |
| SimpleAggregateFunction | Tipos aceptados por el tipo subyacente | Se delega en el tipo subyacente |
| Nested(...) | `IList` de tuplas | El número de elementos debe coincidir con el número de campos |
| Variant(T1, T2, ...) | Valor que coincida con uno de T1, T2, ... | Lanza `ArgumentException` si no coincide ningún tipo |
| QBit(T, dim) | `IList` | Se delega en Array; la dimensión es solo metadatos |

***

<h4 id="type-map-writing-geometry">
  Tipos de geometría
</h4>

| Tipo de ClickHouse | Tipos de .NET aceptados | Notas |
| - | - | - |
| 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 | Cualquier tipo de geometría anterior | Variante de todos los tipos de geometría |

***

<h4 id="type-map-writing-not-supported">
  No admitido para escritura
</h4>

| Tipo de ClickHouse | Notas |
| - | - |
| Dynamic | Lanza `NotImplementedException` |
| AggregateFunction | Lanza `AggregateFunctionException` |

***

<h3 id="nested-type-handling">
  Manejo de tipos anidados
</h3>

Los tipos anidados de ClickHouse (`Nested(...)`) se pueden leer y escribir usando la 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">
  Registro y diagnósticos
</h2>

El cliente .NET de ClickHouse se integra con las abstracciones de `Microsoft.Extensions.Logging` para ofrecer un registro ligero y opcional. Cuando está habilitado, el driver emite mensajes estructurados sobre eventos del ciclo de vida de la conexión, la ejecución de comandos, las operaciones de transporte y las operaciones de inserción masiva. El registro es totalmente opcional: las aplicaciones que no configuran un logger siguen ejecutándose sin sobrecarga adicional.

<h3 id="logging-quick-start">
  Primeros pasos
</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">
  Uso de appsettings.json
</h4>

Puede configurar los niveles de registro mediante la configuración estándar de .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">
  Uso de la configuración en memoria
</h4>

También puede configurar en el código la verbosidad del registro por categoría:

```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">
  Categorías y emisores
</h3>

El driver usa categorías específicas para que puedas ajustar con precisión los niveles de registro de cada componente:

| Categoría | Origen | Aspectos destacados |
| - | - | - |
| `ClickHouse.Driver.Connection` | `ClickHouseConnection` | Ciclo de vida de la conexión, selección de la factoría de clientes HTTP, apertura y cierre de conexiones, gestión de sesiones. |
| `ClickHouse.Driver.Command` | `ClickHouseCommand` | Inicio y finalización de la ejecución de consultas, tiempos, ID de consulta, estadísticas del servidor y detalles de errores. |
| `ClickHouse.Driver.Transport` | `ClickHouseConnection` | Solicitudes HTTP streaming de bajo nivel, indicadores de compresión, códigos de estado de la respuesta y errores de transporte. |
| `ClickHouse.Driver.Client` | `ClickHouseClient` | Inserción binaria, consultas y otras operaciones |
| `ClickHouse.Driver.NetTrace` | `TraceHelper` | Trazado de red, solo cuando el modo de depuración está habilitado |

<h4 id="logging-config-example">
  Ejemplo: Cómo diagnosticar problemas de conexión
</h4>

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

Esto registrará:

* Selección de la fábrica de clientes HTTP (pool predeterminado frente a conexión única)
* Configuración del controlador HTTP (`SocketsHttpHandler` o `HttpClientHandler`)
* Configuración del pool de conexiones (`MaxConnectionsPerServer`, `PooledConnectionLifetime`, etc.)
* Configuración de timeout (`ConnectTimeout`, `Expect100ContinueTimeout`, etc.)
* Configuración de SSL/TLS
* Eventos de apertura/cierre de conexiones
* Seguimiento del ID de sesión

<h3 id="logging-debugmode">
  Modo de depuración: tracing de red y diagnóstico
</h3>

Para ayudar a diagnosticar problemas de red, la biblioteca del driver incluye un asistente que habilita el tracing de bajo nivel de los componentes internos de red de .NET. Para habilitarlo, debe pasar una LoggerFactory con el nivel establecido en Trace y establecer EnableDebugMode en true (o habilitarlo manualmente mediante la clase `ClickHouse.Driver.Diagnostic.TraceHelper`). Los eventos se registrarán en la categoría `ClickHouse.Driver.NetTrace`. Advertencia: esto generará logs extremadamente verbosos y afectará al rendimiento. No se recomienda habilitar el modo de depuración en producción.

```csharp theme={null}
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Debe estar en el nivel Trace para ver los eventos de red
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Habilita el tracing de red de bajo nivel
};
```

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

El driver ofrece compatibilidad integrada con el tracing distribuido de OpenTelemetry mediante la API de .NET [`System.Diagnostics.Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). Cuando está habilitado, el driver emite spans para las operaciones de base de datos que pueden exportarse a backends de observabilidad como Jaeger o al propio ClickHouse (mediante el [OpenTelemetry Collector](/es/guides/use-cases/observability/build-your-own/integrating-opentelemetry)).

<h3 id="opentelemetry-enabling">
  Habilitar el tracing
</h3>

En las aplicaciones ASP.NET Core, agregue el `ActivitySource` del driver de ClickHouse a su configuración de OpenTelemetry:

```csharp theme={null}
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Suscribirse a los spans del driver de ClickHouse
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // O bien AddJaegerExporter(), etc.
```

Para aplicaciones de consola, pruebas o configuración manual:

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

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

<h3 id="opentelemetry-attributes">
  Atributos del span
</h3>

Cada span incluye atributos de base de datos estándar de OpenTelemetry, además de estadísticas de consulta específicas de ClickHouse que pueden usarse para depuración.

| Atributo | Descripción |
| - | - |
| `db.system` | Siempre `"clickhouse"` |
| `db.name` | Nombre de la base de datos |
| `db.user` | Nombre de usuario |
| `db.statement` | Consulta SQL (si está habilitada) |
| `db.clickhouse.read_rows` | Filas leídas por la consulta |
| `db.clickhouse.read_bytes` | Bytes leídos por la consulta |
| `db.clickhouse.written_rows` | Filas escritas por la consulta |
| `db.clickhouse.written_bytes` | Bytes escritos por la consulta |
| `db.clickhouse.elapsed_ns` | Tiempo de ejecución del lado del servidor en nanosegundos |

<h3 id="opentelemetry-configuration">
  Opciones de configuración
</h3>

Controle el comportamiento del tracing con `ClickHouseDiagnosticsOptions`:

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

// Incluir sentencias SQL en spans (valor predeterminado: false por seguridad)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Truncar sentencias SQL largas (valor predeterminado: 1000 caracteres)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;
```

<Warning>
  Habilitar `IncludeSqlInActivityTags` puede exponer datos confidenciales en las trazas. Úselo con precaución en entornos de producción.
</Warning>

<h2 id="tls-configuration">
  Configuración de TLS
</h2>

Al conectarse a ClickHouse a través de HTTPS, puede configurar el comportamiento de TLS/SSL de varias formas.

<h3 id="custom-certificate-validation">
  Validación personalizada de certificados
</h3>

Para entornos de producción que requieran una lógica personalizada de validación de certificados, proporcione su propio `HttpClient` con un controlador `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>
  Consideraciones importantes al proporcionar un `HttpClient` personalizado

  * **Descompresión automática**: deja `AutomaticDecompression` desactivado. El driver decodifica por sí mismo las respuestas comprimidas, por lo que no es necesario — y habilitarlo juega en tu contra en el lado de la solicitud: al enviar, el handler *también añade* todos los algoritmos de su máscara al `Accept-Encoding` saliente, ampliando lo que el driver hubiera anunciado, de modo que ClickHouse puede responder con un codec que no solicitaste. Consulta [Descompresión de respuestas](#response-decompression).
  * **Tiempo de espera de inactividad**: Configura `PooledConnectionIdleTimeout` con un valor inferior al `keep_alive_timeout` del servidor (10 segundos en ClickHouse Cloud) para evitar errores de conexión causados por conexiones semiabiertas.
</Note>

<h2 id="performance-tuning">
  Ajuste del rendimiento
</h2>

En esta sección se describe cómo usar el client para obtener un rendimiento óptimo, así como las distintas opciones que puede ajustar para adaptar el rendimiento del client a su caso de uso concreto.

<h3 id="perf-at-a-glance">
  De un vistazo
</h3>

\| Si necesitas | Haz esto |
\|---|---|---|
\| Leer filas en POCOs | Usa [`QueryAsync<T>`](#perf-read-path), no `MapTo<T>` |
\| Realizar inserciones grandes | Aumenta [`InsertOptions.BatchSize`](#perf-insert-batching) |
\| Ejecutar una aplicación de consola o worker con gran volumen de inserciones | Activa el [GC de servidor](#perf-gc) |
\| Leer resultados grandes a través de la red | Mantén activada la compresión de respuestas (opción predeterminada) |
\| Insertar a través de un enlace rápido | Prueba [`InsertOptions.Compressor = null`](#perf-compression) |
\| Insertar muchas veces en la misma tabla | Usa [`UseSchemaCache` o `ColumnTypes`](#skip-schema-query) |
\| Leer resultados muy grandes | Aumenta [`ReadBufferSize`](#perf-buffers) |

***

<h3 id="perf-read-path">
  Lectura: elegir la ruta de materialization
</h3>

Hay tres formas de obtener una fila de un resultado, y no todas cuestan lo mismo. Algunas de las rutas aplican boxing a los resultados, lo que aumenta las allocations y reduce el rendimiento.

| Cómo lees | Aplica boxing a cada valor | Notas |
| - | - | - |
| `QueryAsync<T>` | **No** | Lee del stream directamente en tus propiedades. La ruta rápida. |
| Accessors tipados del lector (`GetInt32`, `GetInt64`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetFieldValue<T>`) | **No** | Lectura sin boxing desde un almacén de valores tipados. |
| `MapTo<T>` | Sí | Materializa primero la fila y después copia los valores desde ella. |
| `GetValue` y `GetValues` | Sí | Devuelven `object`, por lo que el valor debe pasar por boxing al solicitarlo. |

Para una lectura de 1.000.000 de filas de 105 columnas del dataset *hits*:

| API | Asignado |
| - | -: |
| `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>
  *Los ORM toman la ruta rápida cuando usan accesores tipados.* linq2db registra `GetInt64`,
  `GetDouble` y `GetDateTime` para cada columna, por lo que la lectura se realiza sin boxing. El código que lee mediante
  `GetValue` (incluido un resultado `dynamic` de Dapper) aplica boxing a cada valor. Si una consulta de un ORM se ejecuta con mucha frecuencia
  y lee mediante `GetValue`, utilice `QueryAsync<T>` para esa consulta en concreto.
</Note>

***

<h3 id="perf-insert-batching">
  Inserción: tamaño de lote y paralelismo
</h3>

El tamaño de lote es el factor que más influye en el throughput de inserción. `InsertOptions.BatchSize` tiene un valor predeterminado de
100.000 filas.

**Use lotes grandes.** En una inserción de 1.000.000 de filas, aumentar de 10.000 a 100.000 filas por
lote dio como resultado:

| Inserción | 10.000 filas/lote | 100.000 filas/lote | |
| - | -: | -: | -: |
| POCO | 15.308 ms | 7.853 ms | −49 % |
| `object[]` | 17.027 ms | 10.671 ms | −37 % |

Si no puede controlar el tamaño de lote (por ejemplo, cuando muchos productores pequeños envían filas de forma independiente), use [inserciones async](#async-inserts) y deje que el server se encargue del batching.

**Cargas en paralelo.** `InsertOptions.MaxDegreeOfParallelism` tiene el valor predeterminado `1`. Auméntelo para enviar
varios lotes a la vez. Resulta especialmente útil cuando la compresión está activada, ya que así cada lote se comprime
en su propio thread. Las sessions no funcionan con inserciones en paralelo: desactive las sessions o mantenga
`MaxDegreeOfParallelism = 1`.

**Elimine el schema probe.** Cada llamada a `InsertBinaryAsync` envía primero una consulta `SELECT ... WHERE 1=0`
para averiguar los column types. Consulte [Skipping the schema probe query](#skip-schema-query) para eliminar ese
viaje de ida y vuelta mediante `ColumnTypes` o `UseSchemaCache`.

<Note>
  La ruta de inserción sin boxing se aplica al format predeterminado `RowBinary`. `RowBinaryWithDefaults` debe
  examinar cada value para encontrar el marker `DBDefault`, por lo que mantiene la ruta más lenta.
</Note>

***

<h3 id="perf-compression">
  Compresión: las dos direcciones no coinciden
</h3>

La compresión intercambia CPU por bytes. Que ese intercambio resulte conveniente depende de la dirección de la
transferencia, del ancho de banda de tu conexión con el ClickHouse server, de cómo interactúan tus datos con el algoritmo de compresión elegido y de si debes pagar por cada byte transferido.

**Lecturas:** mantén la compresión activada, salvo que tu server se ejecute en la misma máquina. Es el comportamiento predeterminado. En comparación con no usar compresión, `zstd` en el nivel 1
arrojó:

| Cliente a servidor | Efecto de la compresión |
| - | - |
| Mismo host (loopback) | Cuesta un 8% |
| Misma región de la nube | **Ahorra un 16%** |
| A una región de distancia | **Ahorra un 33%** |

**Inserciones:** mide antes de comprimir. Puede que el ahorro no baste para justificar su activación. Ten en cuenta también que la descompresión añadirá carga adicional al servidor; esa carga es moderada con Zstd y LZ4, pero puede ser alta con otros algoritmos (por ejemplo, Brotli).

Para desactivar la compresión en las inserciones:

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

Para la selección del codec, los niveles de compresión y cómo encontrar tu propio punto de equilibrio, consulta
[Ajuste de la compresión](#tuning-compression).

***

<h3 id="perf-buffers">
  Búferes
</h3>

`ReadBufferSize` establece el tamaño del búfer que lee las respuestas HTTP. Su valor predeterminado es 64 KiB.

El driver toma prestado este búfer de un grupo compartido y lo devuelve al liberar el lector, por lo que
no implica una reserva de memoria en cada consulta. Auméntelo para reducir la cantidad de rellenados del búfer en
resultados grandes. El driver mantiene un búfer por cada lector abierto simultáneamente, de modo que el uso de memoria
crece con el tamaño del búfer y con la cantidad de lectores concurrentes.

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

<Warning>
  *Libere siempre los lectores.* Al liberarlo, un lector devuelve su búfer al grupo y cierra su conexión HTTP.
  Si se abandona un lector, el búfer no se devuelve al grupo y la conexión HTTP puede quedar
  no disponible; la recolección de basura ordinaria no sustituye a la liberación explícita.
</Warning>

***

<h3 id="perf-gc">
  Runtime y GC
</h3>

**Active el GC de servidor en aplicaciones con muchas inserciones.** Con el mismo código y el mismo número de
bytes asignados, el GC de workstation resultó hasta un 97 % más lento en las inserciones que el GC de servidor.

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

Los proyectos de ASP.NET Core ya lo configuran así. Las aplicaciones de consola, los worker services y la mayoría de las imágenes de contenedor, no.

La causa es el tamaño del presupuesto de la generación 0. El Workstation GC usa un presupuesto pequeño, de modo que los búferes de corta duración que crea una inserción no mueren en la generación 0, sino que pasan a la generación 1, lo que incrementa la promoción y genera mucho más trabajo en la generación 2. En un caso de inserción, las recolecciones de la generación 2 por cada 1000 operaciones fueron 4000 con Server GC y 73 000 con Workstation GC.

<Note>
  El Server GC es una configuración orientada al throughput, no a la latencia. En esas mismas mediciones, el Server GC pasó en pausa menos de la mitad del tiempo total, pero sus pausas individuales fueron más largas (percentil 95 de 114,6 ms frente a 61,9 ms). Si tu service es sensible a la latencia de cola, mide ambos modes antes de decidir.
</Note>

***

<h3 id="perf-latency">
  Latencia: reutilizar conexiones
</h3>

Establecer una nueva conexión TCP y realizar el handshake TLS lleva una cantidad de tiempo considerable.
Reutilizar las conexiones reducirá significativamente la latencia de sus consultas.

* No cree un client para cada solicitud. Cada nuevo client con su propio `HttpClient` crea un nuevo
  grupo de conexiones y vuelve a pagar el costo del handshake. Use un único `ClickHouseClient` durante toda la vida de la aplicación. Es seguro para subprocesos y está diseñado para
  un uso singleton.
* Para ADO.NET y los ORM, use `ClickHouseDataSource`, de modo que todas las conexiones compartan un mismo grupo.

Para consultar el conjunto completo de patrones, vea
[Ciclo de vida y agrupación de conexiones](#best-practices-connection-lifetime).

***

<h3 id="perf-measuring">
  Mídalo usted mismo
</h3>

En muchos casos, el rendimiento dependerá de la estructura de sus datos, de la velocidad de su conexión con el servidor,
de si prefiere sacrificar CPU del cliente a cambio de CPU del servidor (o a la inversa), de las limitaciones de su hardware, etc.
Por ello, se recomienda medir el rendimiento usted mismo en función de sus datos y su entorno.

Para conocer la parte del trabajo que corresponde al servidor, establezca `QueryOptions.QueryId` y consulte los 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">
  Compatibilidad con los ORM
</h2>

Los ORM requieren la API de ADO.NET (`ClickHouseConnection`). Para gestionar correctamente el ciclo de vida de la conexión, cree las conexiones desde un `ClickHouseDataSource`:

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

// Crear conexiones para usarlas con el ORM
await using var connection = await dataSource.OpenConnectionAsync();
// Pasar la conexión al ORM...
```

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

`ClickHouse.Driver` funciona con Dapper. El driver convierte automáticamente la sintaxis `@parameter` de Dapper a la sintaxis nativa `{parameter:Type}` de ClickHouse, e infiere los tipos a partir de los valores de .NET.

Usa `ClickHouseDataSource` para gestionar correctamente el ciclo de vida de la conexión:

```csharp theme={null}
var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Registrar como singleton en la DI

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

<h4 id="dapper-parameter-passing">
  Estilos para pasar parámetros
</h4>

Se admiten todos los estilos estándar de parámetros de Dapper:

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

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

**Diccionario:**

```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 un diccionario o de un objeto anónimo):**

```csharp theme={null}
var dynParams = new DynamicParameters(new { Id = 1 });
// o bien: 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 con POCOs
</h4>

Dapper asigna columnas a propiedades por nombre (sin distinguir entre mayúsculas y minúsculas):

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

// De una tabla
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// De un 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">
  Sintaxis de parámetros nativa de ClickHouse
</h4>

Cuando necesites un control explícito de los tipos, usa directamente en el SQL la sintaxis `{param:Type}` de ClickHouse con un `Dictionary<string, object>` para los valores de los parámetros. No combines la sintaxis `@param` con la sintaxis `{param:Type}` para el mismo 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>

**La expansión nativa de IN de 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 } });
```

Dapper reescribe esto como `WHERE id IN (@Ids1, @Ids2, @Ids3)`, y el driver convierte cada parámetro expandido.

**La función `has()` de ClickHouse con un parámetro Array también funciona:**

```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">
  Manejadores de tipos personalizados
</h4>

Algunos tipos de ClickHouse, p. ej., `ITuple`, `BigInteger` y `ClickHouseDecimal`, requieren registrar manejadores al inicio:

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

// BigInteger (para columnas Int128/Int256/UInt128/UInt256)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (para columnas IPv4/IPv6)
SqlMapper.AddTypeHandler(new IpAddressHandler());
```

Consulte el [ejemplo de Dapper](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/ORM/ORM_001_Dapper.cs) como ejemplo de implementación de un manejador de tipos.

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

`GetAll<T>()` y `Get<T>(id)` funcionan. `Insert<T>()` no: genera sintaxis de SQL Server (`SCOPE_IDENTITY`, `[]`). En su lugar, se recomienda usar el 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);
```

Los nombres de las propiedades deben coincidir exactamente con los nombres de columna de ClickHouse (la coincidencia es sensible a mayúsculas y minúsculas).

<h4 id="dapper-limitations">
  Limitaciones
</h4>

| Qué | Estado | Detalles |
| - | - | - |
| Tuple como **resultado** | Funciona | Requiere registrar `SqlMapper.TypeHandler<ITuple>` |
| Tuple como **parámetro** | No compatible | Dapper no puede serializar `ITuple`/`Tuple<>` como valor de `DbParameter` |
| Tipos anidados como parámetro | No compatible | Mismo motivo: Dapper rechaza los tipos complejos como valores de parámetro |
| Tipos Geo como parámetro | No compatible | Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon |
| `Dapper.Contrib.Insert<T>()` | No compatible | Genera sintaxis específica de SQL Server |
| Tipo `Nothing` | No compatible | No tiene una representación significativa en .NET |

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

Este driver es compatible con [linq2db](https://github.com/linq2db/linq2db), un ORM ligero y un proveedor de LINQ para .NET. Consulta el sitio web del proyecto para obtener documentación detallada.

**Ejemplo de uso:**

Crea una `DataConnection` con el proveedor de 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);
```

Los mapeos de tablas pueden definirse mediante atributos o la API fluida. Si los nombres de la clase y de las propiedades coinciden exactamente con los nombres de la tabla y de las columnas, no se necesita ninguna configuración:

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

**Copia masiva:**

Utilice `BulkCopyAsync` para realizar inserciones masivas de forma eficiente.

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

El proveedor oficial de Entity Framework Core para ClickHouse. Permite asignar clases de C# a tablas de ClickHouse, realizar consultas con LINQ e insertar datos mediante `SaveChanges`, todo ello con los patrones habituales de EF Core.

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

<Note>
  Este proveedor está en desarrollo activo. La versión actual admite consultas LINQ (incluidos JOIN, subconsultas y operaciones de conjuntos), `INSERT` mediante `SaveChanges` / `BulkInsertAsync`, migraciones con DDL completo (CREATE / ALTER / DROP) y la configuración del motor de tabla específica de ClickHouse. `UPDATE` / `DELETE` no son compatibles.
</Note>

<h4 id="ef-core-installation">
  Instalación
</h4>

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

Requiere .NET 10.0 y EF Core 10.

<h4 id="ef-core-quick-start">
  Inicio rápido
</h4>

Define la entidad y `DbContext`, y luego haz consultas con 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");
}

// Consulta
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 compatibles
</h4>

| Categoría | Tipos de ClickHouse | Tipos de CLR |
| - | - | - |
| **Enteros** | `Int8`–`Int64`, `UInt8`–`UInt64` | `sbyte`, `short`, `int`, `long`, `byte`, `ushort`, `uint`, `ulong` |
| **Enteros grandes** | `Int128`, `Int256`, `UInt128`, `UInt256` | `BigInteger` |
| **Punto flotante** | `Float32`, `Float64`, `BFloat16` | `float`, `double` |
| **Decimales** | `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` | `decimal` o `ClickHouseDecimal` |
| **Bool** | `Bool` | `bool` |
| **Cadenas** | `String`, `FixedString(N)` | `string` |
| **Enumeraciones** | `Enum8(...)`, `Enum16(...)` | `string` o `enum` de C# |
| **Fecha/hora** | `Date`, `Date32`, `DateTime`, `DateTime64(P, 'TZ')` | `DateOnly`, `DateTime` |
| **Hora** | `Time`, `Time64(N)` | `TimeSpan` |
| **UUID** | `UUID` | `Guid` |
| **Red** | `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>` |
| **Tuples** | `Tuple(T1, ...)` | `Tuple<...>` o `ValueTuple<...>` |
| **Variant** | `Variant(T1, T2, ...)` | `object` |
| **Dinámico** | `Dynamic` | `object` |
| **JSON** | `Json` | `JsonNode` o `string` |
| **Geográficos** | `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` | `Tuple<double,double>` y arrays de estos; `object` para `Geometry` |
| **Envoltorios** | `Nullable(T)`, `LowCardinality(T)` | Se desempaquetan automáticamente |

Usa `ClickHouseDecimal` (de `ClickHouse.Driver.Numerics`) en lugar de `decimal` cuando necesites toda la precisión de las columnas `Decimal128`/`Decimal256`: `decimal` de .NET está limitado a 28–29 dígitos significativos.

<h4 id="ef-core-linq">
  Operaciones LINQ compatibles
</h4>

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

**GROUP BY y agregaciones:** `GroupBy` con `Count`, `LongCount`, `Sum`, `Average`, `Min`, `Max` — incluido `HAVING` (`.Where()` después de `.GroupBy()`), varias agregaciones en una sola proyección y `OrderBy` sobre resultados agregados.

**JOINs:** `Join` (INNER) y patrones `GroupJoin`/`SelectMany` (LEFT y CROSS). LEFT JOIN devuelve `null` real para las filas sin coincidencia (consulta [la semántica de null de LEFT JOIN](#ef-core-join-nulls) más abajo).

**Subconsultas:** `Contains` / `IN` correlacionados, `Any` / `EXISTS`, `All` y subconsultas escalares en proyecciones.

**Operaciones de conjuntos:** `Concat` (→ `UNION ALL`), `Union` (→ `UNION DISTINCT`), `Intersect`, `Except`.

**Colecciones locales insertadas en línea:** los joins y `Contains` con colecciones en memoria (`int[]`, `List<T>`, etc.) se traducen en una serie de `UNION`.

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

**Funciones matemáticas:** los métodos estándar de `Math` y `MathF` se traducen a sus equivalentes en ClickHouse — funciones aritméticas, logarítmicas, trigonométricas y auxiliares.

<h5 id="ef-core-join-nulls">
  Semántica de NULL en LEFT JOIN
</h5>

El proveedor inserta automáticamente `set_join_use_nulls=1` en cada connection path para ajustarse a las expectativas de Entity Framework sobre el comportamiento de JOIN.

Si su servidor ClickHouse o profile impide cambiar esta configuración (por ejemplo, un profile `readonly=1`), desactívelo con:

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

Con la exclusión activada, LEFT JOIN devuelve los valores predeterminados de las columnas de ClickHouse y la detección de navegación de EF basada en valores nulos deja de funcionar como se espera. Use comparaciones explícitas con `0` / `""` en lugar de `== null`.

<h4 id="ef-core-insert">
  Inserción de datos
</h4>

`SaveChanges` usa la API nativa `InsertBinaryAsync` del driver: codificación RowBinary con un cuerpo de solicitud comprimido, mucho más eficiente que SQL con parámetros:

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

Las entidades pasan de `Added` a `Unchanged` tras guardar, igual que con cualquier otro proveedor de EF Core.

**El tamaño del lote** es configurable (valor predeterminado: 1000):

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

<h4 id="ef-core-bulk-insert">
  Inserción masiva
</h4>

Para cargas de alto rendimiento, use `BulkInsertAsync` en lugar de `SaveChanges`. Es un método de extensión de `DbContext` que omite por completo el seguimiento de cambios, la resolución de identidad y la administración del estado de EF Core; llama directamente al método `InsertBinaryAsync` del driver con codificación RowBinary y un cuerpo de solicitud comprimido.

Esto lo hace adecuado para cargar grandes volúmenes de datos cuando no necesita el seguimiento de entidades después de la inserción:

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

La entrada puede ser cualquier `IEnumerable<T>` — recorre las entidades en streaming sin cargarlas todas en memoria. El valor devuelto es el número de filas insertadas. Las entidades **no** se adjuntan al `DbContext` después de la inserción, por lo que no hay transición de estado `Added` → `Unchanged`.

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

Las columnas `Enum8`/`Enum16` de ClickHouse se pueden asignar a propiedades `string` o a tipos `enum` de C#. Al usar enumeraciones de C#, el proveedor convierte automáticamente entre la enumeración y su representación textual:

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

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

// Consulta con valores de enum
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();
```

<h4 id="ef-core-value-converters">
  Conversiones de tipos personalizadas
</h4>

El sistema `ValueConverter` de EF Core te permite mapear tipos personalizados a tipos que el proveedor ya admite. El proveedor nunca ve tu tipo personalizado: EF Core realiza la conversión en ese punto.

**Conversión por propiedad:**

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

// En el método 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");
```

**Clase de convertidor reutilizable:**

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

// Aplicar a una propiedad individual:
.HasConversion<MoneyConverter>()

// O aplicarlo a todas las propiedades de un tipo mediante convenciones:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}
```

<h4 id="ef-core-column-types">
  Anotaciones de tipo de columna
</h4>

Para tipos escalares como `string`, `int`, `DateTime`, etc., el proveedor infiere automáticamente el tipo de ClickHouse. Para los tipos parametrizados y los envoltorios, debe especificar explícitamente el tipo de ClickHouse.

**Uso de anotaciones de datos (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; }
}
```

**Uso de la API fluida en `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)");
});
```

Se admiten envoltorios anidados como `Array(Nullable(Int32))` y `LowCardinality(Nullable(String))` — el proveedor elimina automáticamente `Nullable` y `LowCardinality` en cada nivel de anidamiento.

<h4 id="ef-core-variant-dynamic">
  Columnas Variant y Dynamic
</h4>

Las columnas `Variant(T1, T2, ...)` y `Dynamic` de ClickHouse se corresponden con `object` en .NET. Como `object` es demasiado genérico para la inferencia automática de tipos, debe declarar explícitamente el tipo de almacenamiento mediante `.HasColumnType()`:

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

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

Al leer, el valor se deserializa automáticamente al tipo .NET correspondiente según el discriminador almacenado (p. ej., `string`, `ulong`, `ulong[]`).

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

El proveedor admite el tipo de columna `Json` de ClickHouse, que se corresponde con `System.Text.Json.Nodes.JsonNode` (principal) o `string` (mediante `ValueConverter` automático):

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

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

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

La lectura y escritura de JSON funcionan tanto con `SaveChanges` como con `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"
```

Si prefiere cadenas JSON sin procesar, asigne a la propiedad el tipo `string` con un tipo de columna `Json`; el proveedor aplica automáticamente un `ValueConverter`:

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

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

<Note>
  * **Sin traducción de rutas JSON** — `entity.Data["name"]` en LINQ no se corresponde con la sintaxis SQL `data.name` de ClickHouse. Filtre por columnas no JSON e inspeccione el JSON en memoria.
  * **Semántica de NULL** — El tipo JSON de ClickHouse devuelve `{}` (objeto vacío) para los valores NULL en lugar de SQL NULL.
  * **Precisión de enteros** — El JSON de ClickHouse almacena todos los enteros como `Int64`. Al leerlo mediante `JsonNode`, use `GetValue<long>()` en lugar de `GetValue<int>()`.
</Note>

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

Configure los motores de tablas de ClickHouse y las cláusulas específicas de cada motor mediante la API fluida `ToTable(name, t => ...)`. Si no se configura ningún motor, el proveedor usa `MergeTree` de forma predeterminada, con `ORDER BY` derivado de la clave primaria de la entidad.

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

Familias de motores compatibles:

| Engine | Método fluido | Notas |
| - | - | - |
| `MergeTree` | `HasMergeTreeEngine()` | Predeterminado si no se configura ninguno |
| `ReplacingMergeTree` | `HasReplacingMergeTreeEngine("Version", "IsDeleted")` o `HasReplacingMergeTreeEngine<T>(e => e.Version)` | Las columnas Version / IsDeleted son opcionales |
| `SummingMergeTree` | `HasSummingMergeTreeEngine(…)` o `HasSummingMergeTreeEngine<T>(e => new { … })` | Columnas a sumar opcionales |
| `AggregatingMergeTree` | `HasAggregatingMergeTreeEngine()` | — |
| `CollapsingMergeTree` | `HasCollapsingMergeTreeEngine("Sign")` o `HasCollapsingMergeTreeEngine<T>(e => e.Sign)` | La columna `Sign` debe ser `Int8` |
| `VersionedCollapsingMergeTree` | `HasVersionedCollapsingMergeTreeEngine("Sign", "Version")` o `<T>(e => e.Sign, e => e.Version)` | — |
| `GraphiteMergeTree` | `HasGraphiteMergeTreeEngine("config_section")` | — |
| `Log`, `TinyLog`, `StripeLog`, `Memory` | `HasLogEngine()`, `HasTinyLogEngine()`, `HasStripeLogEngine()`, `HasMemoryEngine()` | Sin ORDER BY / PARTITION BY |

**Cláusulas del motor:** `WithOrderBy`, `WithPartitionBy`, `WithPrimaryKey`, `WithSampleBy`, `WithTtl`, `WithSettings`. Todas se aplican al generador de motores devuelto por `HasXxxEngine()`.

**Características a nivel de columna:** `HasCodec`, `HasTtl`, `HasComment`, `HasDefault` — todas forman parte de las migraciones.

**Índices de omisión de datos** — mediante `HasIndex(...).HasSkippingIndexType(...)`:

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

// Índice con parámetros (p. ej., bloom_filter, tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);
```

Los índices estándar (sin omitir datos) se ignoran sin avisar, ya que ClickHouse no tiene ningún equivalente. Los índices únicos provocan un error, ya que ClickHouse no garantiza la unicidad.

<h4 id="ef-core-migrations">
  Migraciones
</h4>

Flujo de trabajo estándar para las migraciones de EF Core:

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

Operaciones admitidas:

| Operación | Genera |
| - | - |
| `CREATE TABLE` | Incluye la cláusula ENGINE, ORDER BY, PARTITION BY, SETTINGS, códecs/TTL/comentarios/valores predeterminados de columnas |
| `ALTER TABLE ADD COLUMN` | — |
| `ALTER TABLE DROP COLUMN` | — |
| `ALTER TABLE MODIFY COLUMN` | Gestiona el cambio de tipo, además de la adición/eliminación de anotaciones (CODEC, TTL, COMMENT, DEFAULT) |
| `ALTER TABLE RENAME COLUMN` | — |
| `RENAME TABLE` | — |
| `ALTER TABLE ADD INDEX` / `DROP INDEX` | Solo índices de omisión de datos |
| `CREATE DATABASE` / `DROP DATABASE` | Mediante `EnsureCreated` / `EnsureDeleted` y migraciones |

<h4 id="ef-core-limitations">
  Limitaciones de las migraciones
</h4>

| Funcionalidad | Motivo |
| - | - |
| Claves foráneas | ClickHouse no hace cumplir las claves foráneas. Las migraciones rechazan `AddForeignKey`; el validador del modelo emite una advertencia al compilar el modelo. |
| Restricciones de unicidad / índices únicos | ClickHouse no hace cumplir la unicidad. Los índices únicos generan un error durante la migración. |
| Valores generados por el servidor (incremento automático / `IDENTITY`) | ClickHouse no tiene un equivalente. |
| columnas `Nested(…)` | Aún no son compatibles como tipo CLR asignado. |
| Entidades owned como JSON (`.ToJson()`) | La correspondencia estructural de JSON para entidades owned aún no está implementada. Use `JsonNode` / `string` en una columna `Json` en su lugar (consulte [columnas JSON](#ef-core-json)). |

Además de las migraciones, el proveedor tampoco admite aún:

* **`UPDATE` / `DELETE`**
* **Transacciones**: `BeginTransaction` es una operación sin efecto. ClickHouse no admite transacciones ACID.
* **Traducción de consultas con rutas JSON**: `entity.Data["key"]` en LINQ no se traduce a la sintaxis SQL `data.key` de ClickHouse. Filtre por columnas que no sean JSON e inspeccione el JSON en memoria.

<h2 id="limitations">
  Limitaciones
</h2>

<h3 id="valuetuple-caveat">
  Tuplas con más de 8 elementos y una tupla anidada en la última posición
</h3>

Los tipos `ValueTuple` de C# con más de 7 elementos usan un esquema de anidamiento generado por el compilador: el 8.º argumento genérico (`TRest`) es, a su vez, un `ValueTuple` que contiene los elementos restantes. Por ejemplo, `(int, int, int, int, int, int, int, string, string)` se compila como `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

Esto crea una ambigüedad cuando la columna de ClickHouse es una tupla de 8 elementos cuyo último elemento es, a su vez, una tupla; por ejemplo, `Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String))`. El driver no puede distinguir entre:

* Una **tupla plana de 9 elementos** (anidamiento TRest generado por el compilador)
* Una **tupla de 8 elementos** cuyo último elemento es un `Tuple(String, String)` anidado

Ambas producen el mismo tipo de .NET: `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

El driver trata el 8.º argumento como TRest (es decir, lo aplana), lo que significa que el caso de 8 elementos con una tupla anidada se serializará incorrectamente.

Esto afecta tanto a `System.Tuple` como a `ValueTuple`, ya que ambos usan anidamiento TRest para >7 elementos. Las tuplas de 7 elementos o menos, o aquellas cuyo último elemento no es a su vez una tupla, no se ven afectadas.

**Solución alternativa:** Envuelva la tupla interna en una capa adicional para que el driver pueda distinguirla del anidamiento 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">
  Columnas de AggregateFunction
</h3>

Las columnas de tipo `AggregateFunction(...)` no se pueden consultar ni insertar directamente.

Para insertar:

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

Para consultar:

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

***
