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

> El Client oficial de C# para conectarse a ClickHouse por HTTP.

# Client HTTP de C# para 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 Client oficial de C# para conectarse a ClickHouse.
El código fuente del Client 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 Client de alto nivel, seguro para subprocesos y diseñado para utilizarse como instancia única. Ofrece una API asíncrona sencilla para realizar 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 .NET para bases de datos. Son necesarias para integrar ORM (Dapper, Linq2db) y cuando se requiere compatibilidad con ADO.NET. `ClickHouseBulkCopy` es una clase auxiliar que permite insertar datos de forma eficiente mediante una conexión ADO.NET. `ClickHouseBulkCopy` está obsoleta y se eliminará en una versión futura; utilice `ClickHouseClient.InsertBinaryAsync` en su lugar.

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

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

1. Actualice el archivo `.csproj` para usar el nuevo nombre de paquete `ClickHouse.Driver` y [la versión más reciente disponible en NuGet](https://www.nuget.org/packages/ClickHouse.Driver).
2. Sustituya todas las referencias a `ClickHouse.Client` por `ClickHouse.Driver` en su base de código.

***

<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 compatibles de ClickHouse
</h2>

El Client es oficialmente compatible con las tres versiones más recientes, además de las dos últimas versiones LTS.

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

Instale el paquete desde NuGet:

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

O mediante 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;

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

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

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

Puede configurar la conexión a ClickHouse de dos maneras:

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

A continuación se enumeran todos los parámetros de configuración, sus valores predeterminados y sus efectos.

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

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

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

| Propiedad | Tipo | Valor predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| UseCompression | `bool` | `true` | `Compression` | Controla la compresión de los datos transmitidos en ambos sentidos para las consultas habituales: solicita al servidor que comprima la respuesta (`enable_http_compression`; consulte `AcceptEncoding` para elegir el códec, que puede solicitarse explícitamente aunque esta opción esté desactivada) **y** comprime con gzip el cuerpo de la solicitud, salvo cuando se usa `UseFormDataParameters`, cuyo cuerpo multipart siempre se envía sin comprimir. Las inserciones binarias no utilizan esta opción, sino `InsertOptions.Compressor`. Consulte [Compresión de inserciones](#insert-compression) |
| AcceptEncoding | `string` | `null` | `AcceptEncoding` | Encabezado `Accept-Encoding` que se envía con cada solicitud y sustituye los códecs que el controlador anuncia de forma predeterminada (`zstd, lz4, gzip, deflate`). La respuesta del servidor se descomprime de forma transparente. Consulte [Descompresión de respuestas](#response-decompression) |
| UseCustomDecimals | `bool` | `true` | `UseCustomDecimals` | Usa `ClickHouseDecimal` para obtener 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 incluirlos en la cadena de consulta de la URL |
| ReadBufferSize | `int` | `65536` (64 KiB) | `ReadBufferSize` | Tamaño en bytes del búfer con el que se leen las respuestas a las consultas HTTP. El controlador toma el búfer de un grupo compartido y lo devuelve al liberar el lector, por lo que no necesita asignar uno para cada consulta. Aumente este valor para reducir las recargas del búfer con conjuntos de resultados grandes. El controlador mantiene un búfer por cada lector concurrente, por lo que el consumo de memoria aumenta con el tamaño del búfer y el número de lectores concurrentes. Consulte [Búferes](#perf-buffers). |
| ParameterTypeResolver | `IParameterTypeResolver` | `null` | — | Resolvedor personalizado para la correspondencia de tipos de parámetros con la sintaxis `@`; consulte [Correspondencia personalizada de tipos de parámetros](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | `null` | — | Formateador personalizado para la serialización de valores de parámetros; consulte [Formato personalizado de valores de parámetros](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | `null` | — | Transformación personalizada que se aplica a los valores devueltos por el lector de datos; consulte [Conversión personalizada de valores de lectura](#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` y 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>`; si una clave se repite, solo conserva su último valor) o `KeyValuePairs` (devuelve `List<KeyValuePair<K, V>>` y conserva todos los pares). Consulte [Tipo Map](#type-map-reading-map) |
| AllowDuplicateJsonKeys | `bool` | `false` | `AllowDuplicateJsonKeys` | Cómo leer una fila `JSON` en la que dos rutas superpuestas contienen un valor. `false` produce un error, ya que conservar uno de los valores implica descartar el otro; `true` conserva el que aparece en último lugar en la fila. Consulte [Rutas superpuestas](#type-map-reading-json) |

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

| Propiedad | Tipo | Valor predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| UseSession | `bool` | `false` | `UseSession` | Habilita sesiones que conservan el 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` permite mantener la sesión en el servidor, lo que hace posible usar sentencias `SET` y tablas temporales. Las sesiones se restablecen tras 60 segundos de inactividad (el tiempo de espera predeterminado). La duración de la sesión puede ampliarse ajustando su configuración mediante sentencias de ClickHouse o la configuración del servidor.

  La clase `ClickHouseConnection` normalmente permite ejecutar consultas en paralelo desde varios threads. Sin embargo, al habilitar el indicador `UseSession`, solo podrá haber una consulta activa por conexión en un momento dado (una limitación del lado del servidor).
</Note>

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

| Propiedad | Tipo | Valor 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 Client HTTP
</h3>

| Propiedad | Tipo | Valor predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| HttpClient | `HttpClient` | `null` | — | Instancia de HttpClient personalizada y preconfigurada |
| HttpClientFactory | `IHttpClientFactory` | `null` | — | Fábrica personalizada para crear instancias de HttpClient |
| HttpClientName | `string` | `null` | — | Nombre que utiliza HttpClientFactory para crear un Client específico |

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

| Propiedad | Tipo | Valor predeterminado | Clave de la cadena de conexión | Descripción |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | Fábrica de registradores para generar registros de diagnóstico |
| EnableDebugMode | `bool` | `false` | — | Habilita el tracing de red de .NET (requiere LoggerFactory con el nivel configurado en Trace); **afecta considerablemente al 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; consulte 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 texto libre añadidas al encabezado HTTP `User-Agent` para atribuir las consultas a cada aplicación. |

<Note>
  Si utiliza una cadena de conexión para establecer ajustes personalizados, añada el prefijo `set_`, por ejemplo, "set\_max\_threads=4". Si utiliza un objeto ClickHouseClientSettings, no añada el prefijo `set_`.

  Para consultar la lista completa de ajustes disponibles, visite [esta página](/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 sobrescribir la configuración del Client para cada consulta. Todas las propiedades son opcionales y, si no se especifican, se mantienen los valores predeterminados del Client.

| Propiedad | Tipo | Descripción |
| - | - | - |
| QueryId | `string` | Identificador de consulta personalizado para hacer seguimiento de la consulta en `system.query_log` o cancelarla |
| Database | `string` | Sobrescribe la base de datos predeterminada para esta consulta |
| Roles | `IReadOnlyList<string>` | Sobrescribe los roles del Client para esta consulta |
| CustomSettings | `IDictionary<string, object>` | Ajustes del servidor de ClickHouse para esta consulta (p. ej., `max_threads`) |
| CustomHeaders | `IDictionary<string, string>` | Encabezados HTTP adicionales para esta consulta |
| UseSession | `bool?` | Sobrescribe el comportamiento de la sesión para esta consulta |
| SessionId | `string` | ID de sesión para esta consulta (requiere `UseSession = true`) |
| BearerToken | `string` | Sobrescribe el token de autenticación para esta consulta |
| ParameterTypeResolver | `IParameterTypeResolver` | Sobrescribe el resolvedor del Client para la correspondencia de tipos de parámetros con la sintaxis `@`; consulta [Personalización de la correspondencia de tipos de parámetros](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | Sobrescribe el formateador del Client para la serialización de valores de parámetros con la sintaxis `@`; consulta [Personalización del formato de valores de parámetros](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | Sobrescribe la transformación aplicada por el Client a los valores devueltos por el lector de datos; consulta [Personalización de la conversión de valores leídos](#read-value-conversion) |
| MaxExecutionTime | `TimeSpan?` | Tiempo de espera de la consulta del lado del servidor (se pasa mediante el ajuste `max_execution_time`); el servidor cancela la consulta si se supera ese límite |
| AcceptEncoding | `string` | Sobrescribe `Accept-Encoding` para esta consulta (p. ej., `"br"`, `"identity"`) y tiene prioridad sobre `ClickHouseClientSettings.AcceptEncoding`; también establece `enable_http_compression=1` en la URL. Consulta [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 las inserciones masivas mediante `InsertBinaryAsync`.

| Propiedad | Tipo | Valor predeterminado | Descripción |
| - | - | - | - |
| BatchSize | `int` | 100,000 | Número de filas por lote |
| MaxDegreeOfParallelism | `int` | 1 | Número de lotes que se cargan 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`). Con `null`, se envía sin comprimir. Consulte [Compresión de inserciones](#insert-compression) |
| QueryPlacement | `InsertQueryPlacement` | `Body` | Ubicación de la sentencia `INSERT INTO ... FORMAT ...`: `Body` (antes de las filas) o `Url` (como parámetro de URL `query`). Consulte [Posición de la sentencia INSERT](#insert-query-placement) |
| ColumnTypes | `IReadOnlyDictionary<string, string>` | `null` | Nombre de columna → cadena de tipo de ClickHouse. Si se establece, se omite la consulta preliminar del esquema. |
| UseSchemaCache | `bool` | `false` | Almacena en caché el esquema completo de cada tabla, identificado por el par (base de datos, tabla), mientras exista el Client. |

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 preliminar 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. En escenarios de alto caudal, puede evitar este coste adicional de dos maneras:

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

Si conoce el esquema de la tabla en tiempo de compilación, proporciónelo directamente mediante `ColumnTypes`. De este modo, no se envía ninguna consulta para obtener el esquema:

```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: Guardar el esquema en caché**

Si inserta datos varias veces en la misma tabla, establezca `UseSchemaCache = true` para consultar el esquema una sola vez y reutilizarlo en las inserciones posteriores desde la misma instancia de `ClickHouseClient`:

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

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

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

<Note>
  * `ColumnTypes` tiene prioridad sobre `UseSchemaCache`. Si se establecen ambos, se utilizan los tipos explícitos.
  * La caché de esquemas no detecta los cambios realizados mediante `ALTER TABLE`. Si modifica el esquema de la tabla, cree un nuevo `ClickHouseClient` o no utilice `UseSchemaCache` para esa tabla.
  * La caché está asociada a la instancia de `ClickHouseClient` y utiliza como clave el par (base de datos, tabla). Los distintos subconjuntos de columnas de una misma tabla comparten un único esquema almacenado en caché.
</Note>

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

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

<h3 id="creating-a-client">
  Creación de un Client
</h3>

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

Los datos de conexión de su servicio de ClickHouse Cloud están disponibles en la consola de ClickHouse Cloud.

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

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

Elija **C#**. Los datos de conexión aparecerán debajo.

<Image img="https://mintcdn.com/private-7c7dfe99-vortex-format/tee0vtC1xMN3cEIV/images/_snippets/connection-details-csharp.webp?fit=max&auto=format&n=tee0vtC1xMN3cEIV&q=85&s=60f04e40db829dd87b2407094cf1765d" size="md" alt="Datos 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, el administrador de ClickHouse establece los datos de conexión.

Para usar una cadena de conexión:

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

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

O mediante `ClickHouseClientSettings`:

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

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

En escenarios de 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 tener una vida útil prolongada y compartirse en toda la aplicación. Créelo una sola vez (normalmente como un singleton) y reutilícelo para todas las operaciones de la base de datos. El Client gestiona internamente el pool de conexiones HTTP.
</Note>

***

<h3 id="executing-queries">
  Ejecución de consultas
</h3>

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

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

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

Use `ExecuteScalarAsync` para obtener un único valor:

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

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

***

<h3 id="inserting-data">
  Inserción de datos
</h3>

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

Inserte datos mediante consultas parametrizadas con `ExecuteNonQueryAsync`. Los tipos de los parámetros deben indicarse en la consulta SQL con 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>

Utilice `InsertBinaryAsync` para insertar grandes cantidades de filas de forma eficiente. Transmite los datos en el formato binario nativo de filas de ClickHouse, permite subir lotes en paralelo y evita los errores "URL too long" que pueden producirse con las consultas parametrizadas.

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

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

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

Para conjuntos de datos grandes, configure la inserción por lotes y el paralelismo mediante `InsertOptions`:

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

<Note>
  * El Client obtiene automáticamente la estructura de la tabla mediante `SELECT * FROM <table> WHERE 1=0` antes de insertar datos. Los valores proporcionados deben coincidir con los tipos de las columnas de destino. Para evitar esta consulta, use [`InsertOptions.ColumnTypes` o `InsertOptions.UseSchemaCache`](#skip-schema-query).
  * Cuando `MaxDegreeOfParallelism > 1`, los lotes 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 los valores DEFAULT a las columnas para las que no se hayan proporcionado valores.
</Note>

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

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

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

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

// Insert directly — column names are derived from property names
var readings = Enumerable.Range(0, 100_000)
    .Select(i => new SensorReading
    {
        Id = (ulong)i,
        SensorName = $"sensor_{i % 10}",
        Value = Random.Shared.NextDouble() * 100,
        Timestamp = DateTime.UtcNow,
    });

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

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

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

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

    public string Payload { get; set; }

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

| Atributo | Función |
| - | - |
| `[ClickHouseColumn(Name = "...")]` | Sobrescribir el nombre de la columna de destino |
| `[ClickHouseColumn(Type = "...")]` | Indicar explícitamente el tipo de ClickHouse |
| `[ClickHouseNotMapped]` | Excluir la propiedad de la inserción |

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

`InsertBinaryAsync<T>` admite las mismas opciones de `InsertOptions` (procesamiento por lotes, paralelismo y almacenamiento en caché del esquema) que la sobrecarga de `object[]`.

<Note>
  A diferencia de la sobrecarga de `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`, sus valores prevalecen sobre los atributos del POCO.
</Note>

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

Las inserciones POCO siguen funcionando sin problemas si se añaden columnas a la tabla de destino después de registrar el tipo. Como el controlador solo inserta las columnas mapeadas por el POCO, el servidor completa automáticamente las columnas nuevas que tengan `DEFAULT` u otra expresión de valor predeterminado. No es necesario modificar el código ni volver a registrar el tipo.

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

Una inserción binaria coloca la sentencia `INSERT INTO ... FORMAT ...` en la primera línea del cuerpo de la solicitud, antes de las filas. El cuerpo se comprime de forma predeterminada, por lo que los sistemas de enrutamiento y registro que solo inspeccionan la URL no pueden ver la sentencia. Establezca `InsertOptions.QueryPlacement` en `InsertQueryPlacement.Url` para enviar la sentencia como el parámetro de URL `query` y reservar el cuerpo exclusivamente para las filas:

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

Úselo cuando un proxy, balanceador de carga o gateway enrute o inspeccione las solicitudes en función del parámetro `query`, o cuando quiera que la sentencia quede registrada en los logs de acceso y en las herramientas de observabilidad. Esta opción debe habilitarse explícitamente porque la sentencia pasa a contar para el límite de longitud de la URL. El límite efectivo es el menor de los impuestos por el entorno de ejecución de .NET, cualquier intermediario y el servidor. Entre .NET 6 y .NET 9, `System.Uri` limita la URI completa y codificada de la solicitud a 65,519 caracteres; si se supera ese límite, el controlador genera una `InvalidOperationException` que indica que debe volver a usar `InsertQueryPlacement.Body`. El valor predeterminado de `http_max_uri_size` en ClickHouse es 1 MiB, aunque un intermediario puede imponer un límite menor. En el modo de cuerpo, la sentencia y las filas no están sujetas a ese límite de longitud de la URL; otras opciones de la solicitud pueden seguir apareciendo en ella.

Este ajuste es independiente de `Compressor`: el cuerpo se codifica de la misma manera en ambos modos.

***

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

Use `ExecuteReaderAsync` para ejecutar consultas SELECT. El objeto `ClickHouseDataReader` que se devuelve permite acceder a las columnas del resultado con el tipo de dato correspondiente mediante métodos como `GetInt64()`, `GetString()` y `GetFieldValue<T>()`.

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

```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 objetos POCO
</h4>

En lugar de leer las columnas por índice o por nombre, puede obtener los resultados de la consulta directamente como instancias de sus propias clases. Registre el tipo una sola vez en el Client y, después, use `QueryAsync<T>`:

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

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

}

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

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

<h5 id="poco-read-registration">
  Registro
</h5>

`RegisterPocoType<T>()` configura las correspondencias de inserción y lectura y valida ambas desde el principio. `RegisterBinaryInsertType<T>()` no cambia y sigue siendo exclusivo para inserciones 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 las propiedades `required`.

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

La correspondencia de columnas distingue entre mayúsculas y minúsculas. Si falta una columna en el resultado, la propiedad correspondiente conserva su valor predeterminado; las columnas adicionales del resultado se ignoran.

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

<h5 id="poco-read-types">
  Tipos de propiedades compatibles
</h5>

`QueryAsync<T>` lee cada una de estas columnas directamente en una propiedad del tipo correspondiente:

| Columna de ClickHouse | Tipos 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 versiones 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 formato wire) |
| `String`/`FixedString` | `string` o `byte[]` |

Cada fila admite también la variante anulable del tipo de propiedad (`long?`, `DateOnly?`, etc.),
independientemente de que la columna sea `Nullable(...)` o no. Se permite registrar una propiedad de tipo valor no anulable para una columna `Nullable(T)`,
pero se genera una excepción si se recibe un valor NULL.

Las envolturas como `LowCardinality(T)`, `SimpleAggregateFunction(f, T)` y `Object(T)` se asignan igual que `T`.

También se admiten las columnas compuestas, que usan el tipo del framework indicado en la
[referencia de tipos de lectura](#clickhouse-native-type-map-reading): `Array(T)` se asigna a `T[]`, `Tuple(...)`
a `System.Tuple<...>`, `Nested(...)` a `Tuple<...>[]`, `JSON` a `JsonObject` (o a `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 sin boxing y conserva el orden de transmisión y las claves repetidas, con cualquiera de los
valores de [`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 admite más de un tipo de propiedad (por ejemplo, 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
solo están disponibles al leer objetos POCO: `QueryAsync<T>` las admite, pero `MapTo<T>` no.

<h5 id="poco-read-mapto">
  Materializar una sola fila
</h5>

Al recorrer manualmente un lector, use `ClickHouseDataReader.MapTo<T>()` para materializar la fila actual como un POCO registrado sin avanzar a la siguiente fila:

```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 directamente el bucle de lectura, por ejemplo, para combinar el acceso a columnas sin procesar
con la materialización de objetos POCO. Lee la fila a partir de los valores encapsulados del lector, por lo que no admite
los tipos de propiedad alternativos mencionados arriba y realiza más asignaciones de memoria que `QueryAsync<T>`. Es preferible usar
`QueryAsync<T>` si solo necesita las filas; consulte
[cómo elegir la vía de materialización](#perf-read-path) para ver las cifras.

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

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

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

Cuando hay una `LoggerFactory` configurada, `RegisterPocoType<T>()` y `RegisterBinaryInsertType<T>()` generan un log de nivel `Debug` (categoría `ClickHouse.Driver.Client`) que detalla qué propiedades se asignaron a cada columna, cuáles se omitieron y por qué. Consulte [Registro y diagnóstico](#logging-and-diagnostics).

***

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

En ClickHouse, los parámetros de las consultas SQL siguen el formato estándar `{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 'bind' de SQL se envían como parámetros de consulta en el URI HTTP, por lo que usar demasiados puede provocar una excepción de "URL demasiado larga". Utilice `InsertBinaryAsync` para insertar datos en bloque y evitar esta limitación.
</Note>

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

El driver también acepta marcadores de posición `@name`, como los que generan los ORM, por ejemplo, Dapper. Son una
comodidad para el cliente: antes de enviar la solicitud, cada marcador se sustituye por
`{name:ResolvedType}`, de modo que el servidor nunca recibe un `@`. Consulte
[resolución de tipos](#parameter-type-mapping) para saber cómo se determina el tipo. Utilice la forma explícita
`{name:Type}` siempre que sea posible.

Si `@name` no tiene un parámetro correspondiente, se deja tal cual para que el servidor lo rechace. La correspondencia distingue
entre mayúsculas y minúsculas, por lo que `@ID` no se vincula a un parámetro llamado `id`.

<Note>
  Para desactivar esta sustitución, establezca el modificador de AppContext `ClickHouse.Driver.DisableReplacingParameters`
  antes de usar el driver por primera vez. Solo se desactiva la sustitución en el 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 identificador
</h4>

El tipo de parámetro `Identifier` permite usar de forma segura el nombre de una base de datos, una tabla o una columna como parámetro, en lugar de un literal de cadena entre comillas. Para ello, utilice la sintaxis `{name:Identifier}` en SQL o establezca `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 tal cual y el servidor lo sustituye como un identificador SQL sin comillas, añadiendo las comillas invertidas y escapando los caracteres según sus propias reglas. Los identificadores que contienen caracteres especiales (incluidas las comillas invertidas) pueden enviarse y recuperarse sin alteraciones.

***

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

A cada consulta se le asigna un `query_id` único que permite consultar datos en la tabla `system.query_log` o cancelar consultas de larga duración. Puede 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 especifica un `QueryId` personalizado, asegúrese de que sea único en cada llamada. Un GUID generado aleatoriamente es una buena opción.
</Tip>

***

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

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

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

  En los parámetros con la sintaxis `@` que no tienen una indicación `{name:Type}` en SQL ni un valor establecido para `ClickHouseType`, los valores que representan un instante se infieren como `DateTime('UTC')` en lugar de `DateTime` sin especificar una zona horaria. Los valores `DateTime` cuyo `Kind` es `Utc` o `Local`, así como todos los valores `DateTimeOffset`, se envían como `DateTime('UTC')`, lo que permite conservar 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 sobrescribir estos valores predeterminados, configura `ParameterTypeResolver` en `ClickHouseClientSettings`. Esto resulta útil si quieres que todos los parámetros `DateTime` usen `DateTime64(3)` para obtener precisión de milisegundos, o que todos los valores decimales usen una escala determinada, sin tener que establecer `ClickHouseType` en cada parámetro.

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

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

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

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

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

**`IParameterTypeResolver` personalizado para casos avanzados:**

Para resolver el tipo en función del valor o del nombre, implemente directamente la interfaz `IParameterTypeResolver`. Devuelva `null` para recurrir a 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 resolvedor para una consulta concreta mediante `QueryOptions.ParameterTypeResolver`. Si lo hace, este tiene precedencia sobre el resolvedor configurado a nivel del client.

**Orden de precedencia para la resolución de tipos:**

El resolvedor forma parte de una cadena de precedencia. De mayor a menor prioridad:

1. `ClickHouseType` establecido explícitamente en el parámetro
2. Indicación de tipo SQL mediante la sintaxis `{name:Type}` en la consulta
3. `IParameterTypeResolver` (de `QueryOptions.ParameterTypeResolver`; si no está establecido, se utiliza `ClickHouseClientSettings.ParameterTypeResolver`)
4. Inferencia de tipos integrada (`TypeConverter.ToClickHouseType`)

El resolvedor también funciona con la ruta de ADO.NET `ClickHouseConnection`: las conexiones creadas a partir del client heredan la configuración.

***

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

`IParameterFormatter` es un punto de extensión que determina cómo se serializan los valores de los parámetros. Úselo cuando el formato integrado (p. ej., la precisión de DateTime, la configuración regional de los decimales, el escape de cadenas o la representación de números) no se ajuste a lo que esperan su esquema o las herramientas posteriores.

Establezca `ParameterFormatter` en `ClickHouseClientSettings` para usar un formateador en todas las consultas parametrizadas. El formateador recibe el valor, el nombre del tipo de ClickHouse determinado y el nombre del parámetro, y devuelve la representación en cadena que se envía al servidor. Devuelva `null` para usar el formateador predeterminado.

**Uso de `DictionaryParameterFormatter` para aplicar un formato sencillo según el tipo CLR:**

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

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

**`IParameterFormatter` personalizado para escenarios 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 configurar un formateador para cada consulta mediante `QueryOptions.ParameterFormatter`. Si se configura, tiene prioridad sobre el formateador del client.

**Valores compuestos:**

El formateador se aplica tanto a los parámetros de colección de nivel superior como a cada elemento de los valores compuestos (`Array`, `Tuple`, `Map`, `Nullable`, `LowCardinality`, `Variant`). Por ejemplo, una correspondencia para `typeof(int)` formatea por separado cada elemento `Int32` de un `Array(Int32)`.

**Uso de comillas simples en contextos compuestos:**

Cuando los tipos de ClickHouse que representan cadenas (`String`, `FixedString`, `Enum8`, `Enum16`, `IPv4`, `IPv6`, `UUID`) forman parte de un literal compuesto, el driver encierra el resultado del formateador entre comillas simples, pero no escapa su contenido. Si la cadena devuelta contiene una comilla simple o una barra invertida sin escapar, el literal compuesto no será válido y el servidor rechazará la consulta.

Los parámetros de cadena de nivel superior (que no forman parte de un valor compuesto) se utilizan tal cual, sin encerrarlos entre comillas, por lo que no es necesario escaparlos.

**Orden de prioridad de los formateadores:**

1. `IParameterFormatter` (de `QueryOptions.ParameterFormatter` o, si no está disponible, de `ClickHouseClientSettings.ParameterFormatter`). Si devuelve un valor distinto de `null`, se utiliza ese valor.
2. El formato integrado específico de cada tipo en `HttpParameterFormatter`.

El formateador no se invoca para los valores `null` o `DBNull`; estos siempre se serializan como el marcador de valor nulo de ClickHouse (`\N`).

***

<h3 id="read-value-conversion">
  Conversión personalizada de valores al leer
</h3>

`IReadValueConverter` permite transformar los valores que devuelve el lector de datos después de la deserialización, sin cambiar su tipo CLR. Entre sus usos habituales están establecer `DateTime.Kind = Utc` en una columna `DateTime` sin zona horaria, eliminar espacios o normalizar cadenas y procesar una columna JSON antes de que los datos lleguen al código de la aplicación.

Configure `ReadValueConverter` en `ClickHouseClientSettings` para aplicar un conversor a todas las lecturas. El conversor se invoca una vez por cada columna de cada fila, tanto en la ruta de valores encapsulados (`GetValue`) como en la genérica (`GetFieldValue<T>`). Si no se configura ningún conversor, no se añade ninguna sobrecarga: el lector devuelve los valores directamente.

**Uso de `DictionaryReadValueConverter` para conversiones sencillas según el 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 mediante `For<T>` pasan sin modificarse. La selección se basa en el tipo CLR exacto, así que registre el tipo que realmente produce el lector (por ejemplo, `For<JsonObject>` para una columna JSON en `JsonReadMode.Binary`).

**`IReadValueConverter` personalizado para casos avanzados:**

Si necesita seleccionar el conversor según la cadena de tipo de ClickHouse (por ejemplo, para distinguir `DateTime` de `DateTime('UTC')`, que se representan con 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 conversor debe preservar el tipo CLR en tiempo de ejecución; los metadatos de la columna (`GetFieldType`, `GetSchemaTable`) no pasan por él y deben ser coherentes con el valor devuelto.

También puede configurar un conversor para cada consulta mediante `QueryOptions.ReadValueConverter`; si lo hace, tendrá prioridad sobre el conversor configurado a nivel de cliente.

**Alcance de la conversión:**

El conversor se invoca una vez por columna y recibe el valor completo de la celda deserializada; **no** procesa recursivamente los contenedores compuestos. Para una columna `Array(Int32)`, recibe un `int[]`; para `Tuple(Int32, String)`, un `ITuple`.

**Qué sobrecarga se ejecuta:**

Ambas sobrecargas deben producir resultados coherentes, ya que la sobrecarga que invoca el driver depende de cómo el llamador haya leído la
columna:

* `ConvertValue<T>` — los métodos de acceso 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 conversión a objeto en la
  [ruta de lectura de POCO](#poco-read-converters).
* `ConvertValue` (con conversión a objeto) — `GetValue`, `GetValues`, los indexadores, `GetChar`, `GetTuple` y las
  rutas que fuerzan una conversión de tipo en `GetBoolean`, `GetDecimal` y `GetString`.

`IsDBNull` no invoca ningún conversor: lee directamente el indicador de valor nulo, por lo que un conversor nunca puede
cambiar si un valor se considera nulo. `TryGetEnumOrdinal` también prescinde del conversor; consulte
[la lectura del valor ordinal de un enum](#ado-net-reader-enum-ordinal).

El conversor también funciona con la ruta ADO.NET de `ClickHouseConnection`: las conexiones creadas a partir del cliente heredan la configuración.

***

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

Use `ExecuteRawResultAsync` para transmitir directamente los resultados de una consulta en un formato específico, sin pasar por 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 habituales: `JSONEachRow`, `CSV`, `TSV`, `Parquet`, `Native`. Consulte la [documentación de formatos](/es/reference/formats/index) para ver 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 decodifica el flujo por sí mismo, de forma transparente.

Para las exportaciones sin procesar (p. ej., Parquet, Arrow, Native), puede ser conveniente negociar otro códec (p. ej., `zstd` o `lz4`) para reducir el consumo de ancho de banda a cambio de un mayor uso de CPU, sin modificar la configuración de toda la conexión. `QueryOptions.AcceptEncoding` y `ClickHouseCommand.AcceptEncoding` establecen el encabezado HTTP `Accept-Encoding` para una sola solicitud, sustituyen cualquier valor predeterminado que se hubiera añadido y fuerzan `enable_http_compression=1` en la URL (ClickHouse lo requiere para 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 crea el driver deja `AutomaticDecompression` en `DecompressionMethods.None`, y el propio driver decodifica las respuestas. Así, `Content-Encoding` no se elimina a sus espaldas y el cuerpo sin procesar le llega exactamente como lo envió el servidor.

<Warning>
  Si proporciona su propio `HttpClient`, deje también desactivado `AutomaticDecompression`. Esta opción no afecta solo a las respuestas: al enviar la solicitud, el handler **añade todos los algoritmos de su máscara que no estén en el `Accept-Encoding` saliente**. Por tanto, un handler con `GZip | Deflate` convierte un `AcceptEncoding = "lz4"` explícito en `lz4, gzip, deflate` y un `"identity"` explícito en `identity, gzip, deflate` durante la transmisión. Como ClickHouse interpreta el encabezado según su propio orden fijo de preferencia de códecs (sin tener en cuenta el orden ni los valores q), puede responder con un códec que usted no solicitó. El handler decodifica entonces la respuesta y elimina el encabezado, de modo que ni siquiera podrá darse cuenta de lo ocurrido. Si deja la máscara desactivada, solo se ofrecen los algoritmos que usted eligió.
</Warning>

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

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

Cuando el servidor responde con un código 4xx/5xx y `enable_http_compression=1` está activado, comprime el cuerpo del error con el mismo códec que habría usado para una respuesta correcta. El driver lo descomprime si se ha utilizado alguno de los códecs que admite (`lz4`, `zstd`, `gzip`, `deflate`, `br`/`brotli`), para que el mensaje de `ClickHouseServerException` sea legible. Si se ha utilizado otro códec (`snappy`, …), devuelve un marcador de posición que indica el códec y remite a `system.query_log` para consultar el texto original del error.

***

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

`Accept-Encoding` solo solicita al servidor que comprima la respuesta; después hay que descomprimirla. El driver se encarga de hacerlo según el valor de `Content-Encoding` de la respuesta. Así, todas las API habituales de lectura (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, Dapper, EF Core, linq2db) funcionan con respuestas comprimidas sin necesidad de configurar nada. Admite `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`. Si prefiere otra opción, establezca `Accept-Encoding` 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, si usas un ORM y nunca modificas `ClickHouseClientSettings`:

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

Establecerlo también añade `enable_http_compression=1` a la URL, requisito para que ClickHouse tenga en cuenta el encabezado, incluso si `UseCompression` es `false`: indicar expresamente un códec se interpreta como una solicitud de compresión. Si no se establece ningún valor, `UseCompression=false` no envía el encabezado `Accept-Encoding`.

`Accept-Encoding` puede establecerse en cuatro lugares. Prevalece el primero en el que se indique 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 la palabra clave `AcceptEncoding` de la cadena de conexión

Si no se indica ningún códec en ninguno de ellos, el driver envía su lista predeterminada. Un valor que no indique ningún códec (nulo, vacío,
compuesto solo por espacios en blanco o solo por comas) se considera no establecido y se pasa al siguiente lugar. Para desactivar la compresión, use `identity`.

**El servidor, no el client, elige el códec.** ClickHouse busca los tokens de `Accept-Encoding` según su propio orden fijo de preferencia — `zstd` > `br` > `lz4` > `snappy` > `gzip` > `deflate` — e ignora tanto el orden en que se enumeran como los valores q. Por tanto, el encabezado indica qué códecs se admiten, pero no exige uno en particular: la única forma de influir en la elección es omitir tokens. La lista predeterminada incluye `zstd`, por lo que una consulta con la configuración predeterminada recibe una respuesta comprimida con zstd; los demás tokens sirven como alternativas. `br` se puede decodificar, pero no se incluye en el encabezado de forma predeterminada.

El tamaño de la carga útil y el uso de CPU del servidor y del client varían según el códec, los datos, la conexión y el 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`.** Esta configuración se aplica a todos los códecs HTTP y su valor predeterminado es 3. Ajuste este valor según los datos, la velocidad de la conexión y el uso de CPU.
* **Un client limitado por la CPU en una conexión rápida.** El driver decodifica el cuerpo de la respuesta en el thread desde el que se realiza la llamada. Por tanto, si la red no es el cuello de botella, la velocidad de decodificación del client podría convertirse en el factor limitante.

Solicite un códec diferente por consulta o para todo el client si se da cualquiera de estas situaciones:

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

Como la decisión se basa en la respuesta, el cuerpo se decodifica cuando `Content-Encoding` así lo indica, con independencia de lo solicitado: si el encabezado no está presente o su valor es `identity`, el cuerpo se deja intacto; si indica un códec compatible, se decodifica; y cualquier otro valor provoca un error que lo menciona. No hay riesgo de decodificarlo dos veces: si `AutomaticDecompression` de un manejador proporcionado por el llamador ya ha decodificado el cuerpo, también elimina `Content-Encoding`, de modo que el driver recibe el texto sin cifrar y lo deja intacto.

**Los resultados sin procesar no anuncian ningún códec.** `ExecuteRawResultAsync` (y los métodos públicos `PostStreamAsync` / `InsertRawStreamAsync`) entregan el cuerpo tal como se recibió. Por eso, si no se especifica un códec, no solicitan ninguno: el driver no decodifica ese cuerpo, de modo que anunciar un códec en este caso podría convertir inadvertidamente una exportación en un archivo comprimido. La regla es sencilla y no depende de cómo esté configurado `HttpClient`: **el cuerpo llega exactamente como lo envió el servidor, y el servidor envía datos sin cifrar a menos que se solicite un códec.** Para exportar bytes comprimidos de forma deliberada, hay que solicitar un códec (para todo el client o para una consulta concreta).

Un valor explícito de `AcceptEncoding` (en cualquiera de los dos niveles) también se aplica a las solicitudes sin procesar. Si se desea decodificar el resultado, puede usarse `ClickHouseRawResult.ReadDecompressedStreamAsync()`; `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();
```

Lea el flujo devuelto hasta el final antes de que salga de su ámbito, como se muestra arriba. Si la respuesta *está* comprimida, se obtiene un decodificador creado con `leaveOpen`, por lo que desecharlo no afecta a la respuesta; si **no** está comprimida, se obtiene el propio flujo de contenido HTTP, por lo que desecharlo cierra el cuerpo. En ambos casos, `ClickHouseRawResult` es responsable de la respuesta: no llame a sus otros miembros de lectura después de desechar el flujo. Siempre debe desechar `ClickHouseRawResult`, y eso basta para liberar tanto la respuesta como cualquier decodificador creado aquí (los decodificadores mantienen búferes reutilizables). Por tanto, el `await using` anterior es opcional, aunque puede conservarse sin problemas. Las llamadas secuenciales repetidas devuelven el mismo flujo; no es seguro usar este tipo de forma concurrente.

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

<h4 id="insert-compression">
  Compresión de las solicitudes de inserción
</h4>

Zstd es el códec predeterminado para las inserciones: `InsertOptions.Compressor` se inicializa con `ZstdCompressor.Default`,
que corresponde a zstd con nivel de compresión 3. Asígnale 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 dispone de una instancia `Default` y de un constructor que acepta un nivel
y el tamaño del búfer de escritura:

| 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 los compresores.* Cada `Default` es una instancia compartida, y los cuatro compresores
  pueden usarse de forma segura desde varios threads a la vez. Esto ocurre cuando
  `InsertOptions.MaxDegreeOfParallelism` es mayor que 1, ya que cada operación de inserción usa un compresor por
  lote. Ninguno implementa `IDisposable`. Cree una instancia propia y reutilícela, tal
  como se hace con `Default`.
</Note>

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

`IClickHouseCompressor` es público y basta con que una implementación proporcione 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 descomprimir también los cuerpos de las respuestas, además de comprimir las solicitudes, y haz que el flujo que devuelve lance
`InvalidDataException` cuando un cuerpo esté dañado o tenga un formato incorrecto.

`InsertOptions.Compressor` solo se aplica a las inserciones binarias. Los demás cuerpos de las solicitudes del driver se comprimen según otras reglas y ninguno pasa por él:

* **Cada solicitud que contiene texto SQL** (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, `ExecuteRawResultAsync`, la capa ADO.NET) envía su sentencia con `Content-Encoding: gzip` siempre que `UseCompression` sea `true`, es decir, de forma predeterminada. El códec no se puede configurar: `AcceptEncoding` solo afecta a la respuesta, así que las opciones son gzip o ninguna compresión. `Compression=false` envía la sentencia sin comprimir. Las sentencias son pequeñas, por lo que rara vez es necesario prestar atención a esto, pero conviene saberlo al inspeccionar las solicitudes en un proxy o una captura de paquetes.
* **Un cuerpo multipart** — una consulta cuyos parámetros se envían como datos de formulario (`UseFormDataParameters=true`) — siempre se envía sin comprimir, independientemente del valor de `UseCompression`.
* **Una carga sin procesar** (`InsertRawStreamAsync`, `PostStreamAsync`) tiene su propio indicador para cada llamada y no utiliza ni `UseCompression` ni `InsertOptions.Compressor`: usa gzip cuando el indicador está activado y se envía sin comprimir en caso contrario. Ten en cuenta que el parámetro `useCompression` de `InsertRawStreamAsync` tiene `true` como valor predeterminado, por lo que una carga sin procesar se comprime con gzip a menos que pases `false`, incluso si el cliente tiene `Compression=false`.

***

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

La compresión reduce el volumen de datos a costa de un mayor uso de CPU. Que esto compense depende casi por completo de la velocidad de la
conexión en comparación con la velocidad del códec. No hay ningún ajuste que sea adecuado para
todos los casos.

<h4 id="the-one-number-that-decides-it">
  La cifra decisiva
</h4>

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

En la ruta de lectura, ese umbral es más bajo de lo que suele pensarse, porque ClickHouse comprime
las respuestas HTTP en un único thread en el búfer de salida. En las mediciones realizadas con un servicio de ClickHouse Cloud de 16 vCPU
(`hits`, RowBinary, nivel 3), el servidor genera datos comprimidos a unos 100-200 MB/s.

Por tanto, con un resultado grande y si 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 velocidad, mientras que una conexión que atraviese la Internet pública, una VPN o el límite entre regiones suele quedar por debajo.

La compresión sigue siendo útil en la ruta de inserción a velocidades de enlace más altas, porque el client comprime en un núcleo propio y suele hacerlo más rápido que el servidor al comprimir las respuestas.

<h4 id="rough-guide-by-deployment">
  Guía orientativa según el entorno de despliegue
</h4>

| Dónde se ejecuta el client | Ancho de banda habitual | Lecturas | Inserciones |
| - | - | - | - |
| Mismo host / loopback | > 500 MB/s | `identity` | `lz4` es el más rápido, o sin compresión |
| Misma región, misma nube | \~100–500 MB/s | `identity` o `lz4` | `zstd:1` |
| Distintas regiones, misma nube | \~10–100 MB/s | `zstd` | `zstd:3` |
| Internet / VPN / distinta nube | \< 25 MB/s | `zstd` | `zstd:3` |
| Con tráfico medido o muy limitado | \< 5 MB/s | `zstd` | `zstd:5`+ o `br` |

Hay tres aspectos que esta tabla no refleja:

* **Coste del tráfico de salida:** si pagas por la transferencia de datos, los bytes tienen un coste más allá de la latencia, lo que
  inclina la balanza hacia una mayor compresión, independientemente de la velocidad de la conexión.
* **Resultados pequeños:** todo lo anterior se aplica a cargas útiles grandes. En las respuestas pequeñas, el códec apenas
  influye y predomina la sobrecarga de cada solicitud.
* **Las inserciones en paralelo elevan los umbrales de inserción.** Todas las cifras de caudal anteriores corresponden a un *único* thread. El valor predeterminado de `InsertOptions.MaxDegreeOfParallelism` es `1`, pero, al aumentarlo, los lotes se comprimen de forma concurrente. Por tanto, la tasa de codificación total del client crece aproximadamente en proporción al número de núcleos que se le asignan. Así, en una conexión rápida, puede seguir mereciendo la pena comprimir una inserción en paralelo
  incluso a velocidades a las que ya no compensaría comprimirla con un solo thread. Toma las filas sobre inserciones de la tabla como un *límite inferior* y, si ya procesas lotes en paralelo, repite las pruebas antes de concluir que tu conexión es demasiado rápida para que la compresión resulte útil.

La ruta de lectura solo se paraleliza cuando se ejecutan varias consultas.

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

| Códec | Relación de compresión | Cuándo usarlo | Aspectos a tener en cuenta |
| - | - | - | - |
| `lz4` | la más baja | Conexiones rápidas en las que 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, por lo que conviene especificarlo si se quiere evitar `zstd`, el códec predeterminado. | **No tiene codificador de entropía**, así que, con datos cuya distribución está sesgada pero que no son repetitivos (por ejemplo, secuencias largas de texto numérico), su relación de compresión queda muy por detrás de la de los demás. También es el códec al que más perjudica aumentar `http_zlib_compression_level`: pasar del nivel 1 al 3 requiere \~2,7 veces más CPU a cambio de \~29 % menos bytes. |
| `zstd` | alta | La opción de uso general cuando hay una red de por medio. Ofrece la mejor relación entre compresión y consumo de CPU en los niveles que importan y, en el nivel 3, supera a `lz4` en bytes, consumo de CPU del servidor *y* tiempo transcurrido. | Es más costoso de **decodificar** que `lz4`: 1,6 veces más en el nivel 3 según nuestras mediciones, aunque en el nivel 1 ambos son comparables. Además, el driver decodifica en el thread desde el que se realiza la llamada. Con `http_zlib_compression_level=1`, en concreto, consume *algo más* de CPU del servidor que `lz4`. |
| `gzip` | media | Interoperabilidad: los proxies y las pasarelas lo admiten de forma generalizada. | Según nuestras mediciones, tanto `lz4` como `zstd` lo superan en todos los aspectos: genera más bytes que `zstd` y requiere varias veces más CPU para codificar y entre 5 y 9 veces más para decodificar. Elíjalo por compatibilidad, no por rendimiento. |
| `br` | la más alta en niveles bajos | El ancho de banda es realmente el factor limitante y se puede dedicar más CPU a reducir el volumen de datos. | Su consumo de CPU se dispara en los niveles más altos: con `http_zlib_compression_level=6`, medimos un consumo de CPU del servidor entre 3 y 4 veces mayor que el de `zstd`. No se anuncia de forma predeterminada porque tiene prioridad sobre todos los tokens de respaldo de la lista predeterminada. |

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

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

No la cambie a menos que las mediciones justifiquen hacerlo. Por encima del valor predeterminado, la reducción de tamaño es mínima en relación con el aumento del uso de CPU (con `zstd`, pasar de 3 a 6 aproximadamente duplica el uso de CPU del servidor y reduce los bytes en torno a un 14 %), mientras que el rendimiento de `br` se degrada drásticamente. En el nivel 1, en cambio, la situación cambia de forma notable: `lz4` consume mucha menos CPU y `zstd` pierde su ventaja frente a él en ese aspecto. Si lo necesita, ajuste el nivel por consulta:

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

<h4 id="measuring-your-own-crossover">
  Determinar su propio punto de equilibrio
</h4>

La forma más rápida de elegir el códec y el nivel de compresión óptimos es cronometrar la misma consulta con varios códecs y comparar los tiempos.

```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 los mismos datos desde el lado del servidor, consulte `ProfileEvents` en `system.query_log` — establezca
`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 advertencia si realiza este 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 las proporciones dejan de ser significativas. Haga la comparación
con un conjunto de resultados fijo.

***

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

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

**Inserción 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 asume la responsabilidad de gestionar el flujo.* `InsertRawStreamAsync` y `PostStreamAsync` llaman a `Dispose` en el
  flujo que reciben cuando termina la solicitud, tanto si se completa correctamente como si falla. No llame a `Dispose`
  usted mismo ni reutilice el flujo después; por eso el ejemplo anterior no incluye
  `FileStream` en un bloque `using`.

  Si añade su propio bloque `using`, este se ejecutará después de que el driver ya haya llamado a `Dispose` en el flujo. En un `FileStream` o
  `MemoryStream`, esa segunda llamada es inofensiva, pero en un flujo cuyo método `Dispose` devuelve un
  búfer a un grupo o decrementa un contador de referencias, el recurso se libera dos veces.

  La responsabilidad de gestionar el flujo solo se transfiere cuando se aceptan los argumentos: si la llamada lanza `ArgumentException` o
  `ArgumentNullException` porque falta una tabla, un flujo o un formato, usted sigue siendo responsable del flujo.
</Warning>

<Note>
  Consulte la [documentación de configuración de formatos](/es/reference/settings/formats) para conocer las opciones que permiten controlar 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, consulte el [directorio de ejemplos](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples) del repositorio de GitHub.

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

La biblioteca ofrece compatibilidad completa con ADO.NET a través de `ClickHouseConnection`, `ClickHouseCommand` y `ClickHouseDataReader`. Esta API es necesaria para integrar ORM (Dapper, Linq2db) y para utilizar las abstracciones estándar de .NET para bases de datos.

<h3 id="ado-net-datasource">
  Gestión de la duración de las conexiones con ClickHouseDataSource
</h3>

**Cree siempre las conexiones a partir de un `ClickHouseDataSource`** para garantizar una gestión adecuada de su duración y la agrupación de conexiones. DataSource gestiona internamente un único `ClickHouseClient`, y todas las conexiones comparten su pool de conexiones HTTP.

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

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

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

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

Para la inyección de dependencias:

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

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

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

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

<Warning>
  **No cree instancias de `ClickHouseConnection` directamente** en el código de producción. Cada vez que lo hace, se crean un nuevo cliente HTTP y un nuevo pool de conexiones, lo que puede agotar los sockets cuando la carga es elevada:

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

  En su lugar, utilice siempre `ClickHouseDataSource` o comparta una única instancia de `ClickHouseClient`.
</Warning>

***

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

Cree comandos desde una conexión para ejecutar consultas SQL:

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

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

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

Métodos de comando:

* `ExecuteNonQueryAsync()` - Para sentencias INSERT, UPDATE, DELETE y DDL
* `ExecuteScalarAsync()` - Devuelve el valor de 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 las consultas:

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

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

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

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

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

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

Una columna `Enum8` o `Enum16` se materializa como una etiqueta: `GetFieldType` devuelve `string`, y
`GetString`, `GetValue` y `GetFieldValue<string>` devuelven esa etiqueta. Los accesores numéricos
lanzan `InvalidCastException` al usarse con una columna de tipo enum, ya que el valor almacenado es una cadena.

Use `TryGetEnumOrdinal` para obtener el número asociado a 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 asigna un valor a `value` si la columna es de tipo `Enum8`/`Enum16` o si es una columna
`Nullable(Enum...)` cuya celda no es NULL. Devuelve `false` y asigna `0` a `value` si la celda es NULL o si
la columna no es de tipo enum. El ordinal es el
valor con signo recibido en formato wire, por lo que puede ser negativo; además, el ordinal de un `Enum16` puede ocupar más de un
byte.

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

<h3 id="best-practices-connection-lifetime">
  Duración y reutilización de las conexiones
</h3>

`ClickHouse.Driver` utiliza `System.Net.Http.HttpClient` internamente. `HttpClient` mantiene un pool de conexiones por punto de conexión. Por tanto:

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

**Prácticas recomendadas:**

| Escenario | Enfoque recomendado |
| - | - |
| Uso general | Utilice una única instancia compartida de `ClickHouseClient` |
| ADO.NET / ORM | Utilice `ClickHouseDataSource` (crea conexiones que comparten el mismo pool) |
| Entornos de inyección de dependencias | Registre `ClickHouseClient` o `ClickHouseDataSource` como instancia única mediante `IHttpClientFactory` |

<Warning>
  Si utiliza un `HttpClient` o `HttpClientFactory` personalizado, asegúrese de que `PooledConnectionIdleTimeout` tenga un valor inferior al de `keep_alive_timeout` del servidor para evitar errores causados por conexiones semicerradas. En las implementaciones de Cloud, el valor predeterminado de `keep_alive_timeout` 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. **Use UTC siempre que sea posible.** Almacene las marcas de tiempo en columnas `DateTime('UTC')` y use `DateTimeKind.Utc` en su código. Así evitará ambigüedades con las zonas horarias.

2. **Use `DateTimeOffset` para gestionar explícitamente la zona horaria.** Siempre representa un instante concreto e incluye el desfase respecto de UTC.

3. **Especifique la zona horaria en las indicaciones de tipo SQL.** Si usa parámetros con valores DateTime `Unspecified` para columnas que no están en UTC, incluya la zona horaria en la sentencia 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 del cliente al servidor la responsabilidad de agrupar los datos en lotes. En lugar de agruparlos en el cliente, el servidor almacena temporalmente los datos entrantes y los escribe en el almacenamiento cuando se alcanzan los umbrales configurados. Esto resulta útil en escenarios de alta concurrencia, como las cargas de trabajo de observabilidad, en las que muchos agentes envían cargas útiles pequeñas.

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

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

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

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

| Modo | Comportamiento | Caso de uso |
| - | - | - |
| `wait_for_async_insert=1` | La inserción finaliza cuando los datos se han guardado en disco. Los errores se notifican al client. | **Recomendado** para la mayoría de los workload |
| `wait_for_async_insert=0` | La inserción finaliza en cuanto los datos se almacenan en el búfer. No se garantiza que los datos persistan. | Solo si la pérdida de datos es aceptable |

<Warning>
  Con `wait_for_async_insert=0`, los errores solo se detectan durante el vaciado y no pueden atribuirse a la inserción original. Además, el client no aplica contrapresión, por lo que el servidor podría sobrecargarse.
</Warning>

**Parámetros de configuración principales:**

| Parámetro | 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 al transcurrir este tiempo de espera (milisegundos) |
| `async_insert_max_query_number` | Vacía el búfer cuando se acumula este número de consultas |

***

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

Habilite las sesiones solo cuando necesite funciones del lado del servidor que mantengan el estado, por ejemplo:

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

Cuando se habilitan las sesiones, las solicitudes se procesan de forma secuencial para evitar que se use una misma sesión de forma simultánea. Esto añade una sobrecarga a los workloads que no necesitan mantener el estado de la 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 garantizar la 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 de 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 mediante 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`. Para leerlas como `byte[]`, establezca `ReadStringsAsByteArrays=true` en la cadena de conexión. Esto resulta útil si almacena datos binarios que podrían no tener una codificación UTF-8 válida.

  La configuración también afecta a las cadenas anidadas en otros tipos: `Array(String)` se lee como `byte[][]`
  y `Map(String, String)` como `Dictionary<byte[], byte[]>`, incluidas las claves. La única excepción son las
  columnas `JSON`, cuyos valores de cadena siempre se tratan como texto; consulte [Tipo JSON](#type-map-reading-json).
</Note>

***

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

| Tipo de ClickHouse | Tipo de .NET |
| - | - |
| 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 transcurridos desde la época Unix). Aunque el almacenamiento siempre se realiza en UTC, las columnas pueden tener una zona horaria asociada que determina cómo se muestran e interpretan los valores.

Al leer valores `DateTime`, la propiedad `DateTime.Kind` se establece según la zona horaria de la columna:

| Definición de la columna | DateTime.Kind devuelto | Notas |
| - | - | - |
| `DateTime('UTC')` | `Utc` | Zona horaria UTC explícita |
| `DateTime('Europe/Amsterdam')` | `Unspecified` | Desplazamiento horario aplicado |
| `DateTime` | `Unspecified` | Hora local conservada sin cambios |

En las columnas con una zona horaria distinta de UTC, el valor `DateTime` devuelto representa la hora local de esa zona horaria. Use `ClickHouseDataReader.GetDateTimeOffset()` para obtener un `DateTimeOffset` con el desplazamiento horario correspondiente:

```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 controlador devuelve un `DateTime` con `Kind=Unspecified`. Así se conserva la hora tal como se almacenó, sin suponer ninguna zona horaria.

Si necesita que se tenga en cuenta la zona horaria en columnas que no la especifican, puede:

1. Especificar una zona horaria en la definición de cada columna: `DateTime('UTC')` o `DateTime('Europe/Amsterdam')`
2. Aplicar la zona horaria después de leer los datos.

***

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

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

El tipo de retorno de las columnas JSON depende de la configuración `JsonReadMode`:

* **`Binary` (predeterminado)**: Devuelve `System.Text.Json.Nodes.JsonObject`. Permite acceder a los datos JSON de forma estructurada, pero convierte los tipos especializados de ClickHouse (como las direcciones IP, los UUIDs y los valores decimales grandes) a sus representaciones en cadena dentro de la estructura JSON.

* **`String`**: Devuelve el JSON sin procesar como `string`. Conserva la representación JSON exacta que proporciona ClickHouse, lo que resulta útil si necesita pasar el JSON a otro sistema sin analizarlo o encargarse 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. Lee los datos exactamente igual que `Binary`, pero no envía ninguna configuración del servidor junto con la
consulta; úselo en una conexión que no permita establecerla.

<h5 id="type-map-reading-json-nulls">
  Rutas tipadas y valores NULL
</h5>

Una ruta declarada en el tipo de columna es una **ruta tipada**; cualquier otra ruta del documento es una
**ruta dinámica**. La diferencia entre ambas se manifiesta cuando un valor es nulo.

Una ruta tipada siempre aparece en el `JsonObject`. Si se declara como `Nullable(T)` o `Dynamic`, se devuelve
como un valor nulo de JSON tanto si el valor almacenado es nulo como si el documento no contiene esa ruta; ambos
casos son indistinguibles:

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

Si se declara con un tipo que no admite valores nulos, una ruta ausente adopta el valor predeterminado de ese tipo: `JSON(x String)`
da `{"x":""}` y `JSON(x Int64)` da `{"x":0}`.

Una ruta dinámica cuyo valor es nulo se elimina por completo del objeto, por lo que `ContainsKey` devuelve
false para esa ruta. Al leer `{"x":null}` de una columna `JSON` sin más, se obtiene `{}`.

Las rutas tipadas anidadas crean sus rutas superiores, por lo que `JSON(a.b Nullable(Int64))` produce `{"a":{"b":null}}`
incluso si el documento está vacío.

<Note>
  Esta es la representación que genera el propio servidor, por lo que los modos `Binary` y `String` ahora coinciden. Antes de la versión 1.4.0, una
  ruta tipada con un valor nulo se eliminaba del `JsonObject`, de modo que `{"x":null}` se leía como
  `{}`; en una ruta anidada 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 cadenas en las hojas de una columna `JSON` siempre se devuelven como texto, sea cual sea
el valor de `ReadStringsAsByteArrays`: `JsonValue` no tiene una representación como array de bytes, por lo que un `byte[]` se
representaría en base64. Esto se aplica a `String`, `FixedString` y a los tipos encapsulados en
`LowCardinality`, `Nullable` o `SimpleAggregateFunction`, así como a las cadenas dentro de `Array` y `Map`,
incluidas las claves de los mapas.

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

  Si el tipo de una clave de un mapa JSON no es exactamente `String` —por
  ejemplo, `Map(LowCardinality(String), String)`—, se lanza `NotSupportedException`.
</Note>

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

ClickHouse acepta una columna que declara una ruta tanto como valor como ruta 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
genera una fila con una clave duplicada: `{"a":0,"a":{"b":7}}`. Un `JsonObject` no puede contener dos valores
para una misma clave, así que `JsonReadMode.Binary` lanza una `SerializationException` que identifica ambas rutas. Lo
mismo ocurre si el valor es un `Map`, como en `JSON(a Map(String, Int64))`, al leer una fila que
también contiene una ruta dinámica `a.b`.

Esto solo ocurre cuando ambas rutas contienen un valor en la misma fila. Si una de ellas no contiene nada —un valor nulo,
un objeto vacío o un subárbol cuyos valores son todos nulos—, prevalece la que contiene datos,
sin importar cuál envíe primero el servidor. Por tanto, cuando las rutas superpuestas se declaran con tipos `Nullable`, solo una aporta un valor en cada fila y la columna se lee sin
errores: `JSON(a Nullable(Int64), a.b Nullable(Int64))` produce `{"a":5}` y `{"a":{"b":7}}`, como
se esperaba.

Lea la columna con `JsonReadMode.String` para obtener el texto JSON enviado por el servidor sin cambios, incluida la clave
duplicada.

Configure `AllowDuplicateJsonKeys` para seguir leyendo la columna como un `JsonObject` sin que se lance una excepción. El
controlador conserva entonces el último de los dos valores que aparecen en la fila y descarta el otro, por lo que se
pierde información: `JSON(a Int64, a.b Int64)` con `{"a.b":7}` se lee como `{"a":0}`. Si una ruta
contiene un valor y su ruta padre contiene un escalar o un array, se sigue lanzando una excepción, porque no se puede
insertar un subárbol en ninguno de ellos.

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

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

***

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

| Tipo de ClickHouse | Tipo de .NET | 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 se almacena físicamente como un `Array(Tuple(K, V))` y puede contener varias entradas con la misma clave. Un `Dictionary` no admite claves duplicadas; por eso, en el modo predeterminado, solo se conserva el último valor de cada clave repetida y se descartan los pares anteriores. La opción `MapReadMode` determina la representación:

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

* **`KeyValuePairs`**: Devuelve `List<KeyValuePair<K, V>>` en el orden en que el servidor envió los pares, de modo que se conservan todos, incluidos los que tienen claves repetidas.

```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 modo determina qué tipo utiliza el framework para una columna `Map`, por lo que también afecta a `GetFieldValue<T>`, a los tipos del esquema que devuelve el controlador y a la correspondencia con las propiedades de POCO. Se aplica en cualquier punto del árbol de tipos de una columna donde aparezca un mapa, incluidos `Array(Map(...))`, `Map(K, Map(...))`, `Tuple(..., Map(...))` y `Dynamic`.

Ambas representaciones se aceptan al escribir, independientemente del modo; consulta [escritura de mapas](#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 | Consulte la nota |
| Array(T) | `T[]` (un `Array(Array(T))` anidado se lee como un array escalonado `T[][]`; use `reader.GetFieldValue<T[,]>(ordinal)` para materializar los datos rectangulares en un array multidimensional de CLR) |
| Tuple(T1, T2, ...) | `Tuple<T1, T2, ...>` / `LargeTuple` |
| Map(K, V) | `Dictionary<K, V>` o `List<KeyValuePair<K, V>>` si `MapReadMode=KeyValuePairs` — consulte [el tipo Map](#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, ...) | Consulte la nota |
| QBit(T, dimension) | `T[]` |

<Note>
  En cada fila, los tipos Dynamic y Variant se convierten al tipo correspondiente al valor subyacente real.
</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 | Véase la nota |

<Note>
  Geometry es un tipo Variant que puede contener cualquiera de los tipos de geometría. Cada valor se convierte al tipo correspondiente.
</Note>

***

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

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

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

| Tipo de ClickHouse | Tipos de .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()` | Trunca al formato de coma flotante de 16 bits bfloat16 |

***

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

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

***

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

| Tipo de ClickHouse | Tipos .NET aceptados | Notas |
| - | - | - |
| String | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | Los tipos binarios se escriben directamente; los flujos pueden admitir o no el reposicionamiento |
| FixedString(N) | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | La cadena se codifica en UTF-8 y se completa con caracteres de relleno; 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 | Se convierten a días Unix como UInt16; rango admitido `[1970-01-01, 2149-06-06]` |
| Date32 | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos de NodaTime | Se convierten a días Unix como Int32; rango admitido `[1900-01-01, 2299-12-31]` |
| DateTime | `DateTime`, `DateTimeOffset`, `DateOnly`, tipos de NodaTime | Consulte los detalles más abajo; 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 determinada por el parámetro Scale |
| Time | `TimeSpan`, `TimeOnly`, `int` | Limitado a ±999:59:59; int se interpreta como segundos |
| Time64 | `TimeSpan`, `TimeOnly`, `decimal`, `double`, `float`, `int`, `long`, `string` | La cadena se interpreta con el formato `[-]HHH:MM:SS[.fraction]`; limitado a ±999:59:59.999999999 |

<Note>
  **Valores fuera de rango**

  En la ruta de escritura binaria, los valores de `Date`, `Date32`, `DateTime` y `DateTime32` que quedan fuera del rango admitido generan una excepción `ArgumentOutOfRangeException` al ejecutar `Write`, que indica el tipo de columna y el rango admitido. Antes, los valores fuera de rango podían truncarse sin aviso al convertirse en enteros de 32 bits y luego ser reinterpretados por el servidor, lo que producía marcas de tiempo válidas, pero incorrectas.
</Note>

El controlador tiene en cuenta `DateTime.Kind` al escribir valores:

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

Los valores de `DateTimeOffset` siempre conservan el instante exacto.

**Ejemplo: DateTime en 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 sencillo y predecible, utilice `DateTimeKind.Utc` o `DateTimeOffset` en todas las operaciones con DateTime. Así, su código funcionará de forma coherente independientemente de la zona horaria del servidor, del cliente o de la columna.

<h4 id="datetime-http-param-vs-bulkcopy">
  Parámetros HTTP frente a copia masiva
</h4>

Existe una diferencia importante entre la vinculación de parámetros HTTP y la copia masiva al escribir valores DateTime `Unspecified`:

**La copia masiva** conoce la zona horaria de la columna de destino e interpreta correctamente los valores `Unspecified` según esa zona horaria.

**Los parámetros HTTP** no detectan automáticamente la zona horaria de la columna. Debe especificarla en la indicación de tipo SQL:

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

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

| `DateTime.Kind` | 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 interpreta como UTC | Se interpreta como UTC | Se interpreta como UTC |
| `Unspecified` | Europe/Amsterdam | Se interpreta como hora de Ámsterdam | **Se interpreta como UTC** | Se interpreta como hora de Ámsterdam |

***

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

| Tipo de ClickHouse | Tipos de .NET aceptados | Notas |
| - | - | - |
| Decimal(P,S) | `decimal`, `ClickHouseDecimal`, cualquier tipo compatible con `Convert.ToDecimal()` | Lanza `OverflowException` si se supera la precisión máxima |
| 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 .NET aceptados | Notas |
| - | - | - |
| Json | `string`, `JsonObject`, `JsonNode`, cualquier objeto | El comportamiento depende de la configuración `JsonWriteMode` |

La configuración `JsonWriteMode` determina cómo se escribe JSON:

| Tipo de entrada | `JsonWriteMode.String` (predeterminado) | `JsonWriteMode.Binary` |
| - | - | - |
| `string` | Se pasa directamente | Lanza `ArgumentException` |
| `JsonObject` | Se serializa mediante `ToJsonString()` | Lanza `ArgumentException` |
| `JsonNode` | Se serializa mediante `ToJsonString()` | Lanza `ArgumentException` |
| POCO registrado | Se serializa mediante `JsonSerializer.Serialize()` | Codificación binaria con indicaciones de tipo; admite atributos de ruta personalizados |
| POCO no registrado / objeto anónimo | Se serializa mediante `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 analice. Es el modo más flexible y no requiere registrar los tipos.

* **`Binary`**: Solo acepta tipos POCO registrados. Los datos se convierten al formato JSON binario de ClickHouse en el cliente, con compatibilidad total con indicaciones de tipo. Antes de usar este modo, es necesario llamar a `connection.RegisterJsonSerializationType<T>()`. Si se escriben valores `string` o `JsonNode` en este modo, se lanza `ArgumentException`.

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

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

<h5 id="json-typed-columns">
  Columnas JSON con tipos definidos
</h5>

Cuando una columna JSON tiene indicaciones de tipo (p. ej., `JSON(id UInt64, price Decimal128(2))`), el controlador las utiliza para serializar los valores sin alterar sus tipos. Así se conserva 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 maneras, según el valor de `JsonWriteMode`:

**Modo de cadena (predeterminado)**: Los POCO se serializan con `System.Text.Json.JsonSerializer`. No es necesario registrar los tipos. Es la opción más sencilla y funciona con objetos anónimos.

**Modo binario**: Los POCO se serializan con el formato JSON binario del controlador, que admite indicaciones de tipo completas. Los tipos deben registrarse con `connection.RegisterJsonSerializationType<T>()` antes de usarlos. Este modo permite asignar rutas JSON personalizadas mediante atributos:

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

* **`[ClickHouseJsonIgnore]`**: Excluye una propiedad de la serialización. **Solo funciona en el 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; }  // Not serialized
}

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

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

La comparación de los nombres de las propiedades con las indicaciones de tipo de la columna distingue entre mayúsculas y minúsculas. Una propiedad `UserId` solo coincidirá con una indicación definida como `UserId`, no como `userid`. Esto es coherente con el comportamiento de ClickHouse, que permite que rutas como `userName` y `UserName` coexistan como campos distintos.

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

* Los tipos POCO deben registrarse en la conexión mediante `connection.RegisterJsonSerializationType<T>()` antes de serializarlos. Si se intenta serializar un tipo no registrado, se produce una excepción `ClickHouseJsonSerializationException`.
* Las propiedades de diccionario y array/lista necesitan indicaciones de tipo en la definición de la columna para serializarse correctamente. Si no se proporcionan, utilice String mode.
* Los valores nulos de las propiedades POCO solo se escriben si la ruta tiene una indicación de tipo `Nullable(T)` en la definición de la columna. ClickHouse no permite tipos `Nullable` en rutas JSON dinámicas, por lo que se omiten las propiedades nulas sin indicación de tipo.
* Los atributos `ClickHouseJsonPath` y `ClickHouseJsonIgnore` se ignoran en String mode (solo funcionan en Binary mode).

***

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

| Tipo de ClickHouse | Tipos .NET aceptados | Notas |
| - | - | - |
| UUID | `Guid`, `string` | La cadena se convierte en un `Guid` |
| IPv4 | `IPAddress`, `string` | Debe ser IPv4; la cadena se analiza mediante `IPAddress.Parse()` |
| IPv6 | `IPAddress`, `string` | Debe ser IPv6; la cadena se analiza mediante `IPAddress.Parse()` |
| Nothing | Cualquiera | No escribe nada (no tiene efecto) |
| Dynamic | — | **No es compatible** (lanza `NotImplementedException`) |
| Array(T) | `IList`, `null` | Un valor nulo se escribe como un array vacío. Para los tipos anidados (`Array(Array(T))` y niveles más profundos), se aceptan tanto arrays irregulares (`T[][]`, `List<List<T>>`) como arrays CLR multidimensionales rectangulares (`T[,]`, `T[,,]`, …); el número de dimensiones del array CLR debe coincidir con la profundidad de anidamiento en 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) si hay 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>>` generada por `MapReadMode=KeyValuePairs`) en cualquiera de los dos modos de lectura, y puede contener claves repetidas. Se aplica a las inserciones binarias y a los parámetros de consulta |
| Nullable(T) | `null`, `DBNull` o tipos aceptados por T | Escribe el byte que indica si el valor es nulo antes del valor |
| Enum8 | `string`, `sbyte`, tipos numéricos | Busca la cadena en el diccionario de la enumeración |
| Enum16 | `string`, `short`, tipos numéricos | Busca la cadena en el diccionario de la enumeración |
| LowCardinality(T) | Tipos aceptados por T | Delega en el tipo subyacente |
| SimpleAggregateFunction | Tipos aceptados por el tipo subyacente | 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 con ningún tipo |
| QBit(T, dim) | `IList` | Delega en Array; la dimensión solo se almacena como metadata |

***

<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 | Cualquiera de los tipos de geometría anteriores | Variante de todos los tipos de geometría |

***

<h4 id="type-map-writing-not-supported">
  No se admiten 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 mediante la semántica de los 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óstico
</h2>

El client .NET de ClickHouse se integra con las abstracciones de `Microsoft.Extensions.Logging` para ofrecer un registro ligero que se habilita solo si se solicita. Cuando está habilitado, el controlador emite mensajes estructurados sobre el ciclo de vida de la conexión, la ejecución de comandos, las operaciones de transporte y las inserciones masivas. El registro es totalmente opcional: las aplicaciones que no configuran un logger siguen funcionando sin sobrecarga adicional.

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

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

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

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

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

<h4 id="logging-appsettings-config">
  Uso de appsettings.json
</h4>

Puede configurar los niveles de registro con 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 el nivel de detalle de los logs 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 controlador utiliza categorías específicas que permiten ajustar el nivel de log de cada componente:

| Categoría | Fuente | Aspectos destacados |
| - | - | - |
| `ClickHouse.Driver.Connection` | `ClickHouseConnection` | Ciclo de vida de la conexión, selección de la fábrica de clientes HTTP, apertura y cierre de conexiones y gestión de sesiones. |
| `ClickHouse.Driver.Command` | `ClickHouseCommand` | Inicio y finalización de la ejecución de consultas, tiempos de ejecución, identificadores de consultas, estadísticas del servidor y detalles de errores. |
| `ClickHouse.Driver.Transport` | `ClickHouseConnection` | Solicitudes de streaming HTTP de bajo nivel, indicadores de compresión, códigos de estado de las respuestas y fallos de transporte. |
| `ClickHouse.Driver.Client` | `ClickHouseClient` | Inserciones binarias, consultas y otras operaciones |
| `ClickHouse.Driver.NetTrace` | `TraceHelper` | Tracing de red, solo cuando está activado el modo de depuración |

<h4 id="logging-config-example">
  Ejemplo: Diagnóstico de problemas de conexión
</h4>

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

Se registrará en el log:

* La selección de la fábrica de clientes HTTP (pool de conexiones predeterminado o conexión única)
* La configuración del controlador HTTP (SocketsHttpHandler o HttpClientHandler)
* La configuración del pool de conexiones (MaxConnectionsPerServer, PooledConnectionLifetime, etc.)
* La configuración de los tiempos de espera (ConnectTimeout, Expect100ContinueTimeout, etc.)
* La configuración de SSL/TLS
* Los eventos de apertura y cierre de conexiones
* El 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 controlador incluye un proceso auxiliar que permite realizar tracing de bajo nivel de los componentes internos de red de .NET. Para activarlo, debe pasar una LoggerFactory con el nivel configurado como Trace y establecer EnableDebugMode en true (o activarlo 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 detallados y afectará al rendimiento. No se recomienda activar el modo de depuración en producción.

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

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

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

El controlador incluye compatibilidad con el trazado distribuido de OpenTelemetry a través de la API de .NET [`System.Diagnostics.Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). Cuando se habilita, el controlador genera spans para las operaciones de base de datos que pueden exportarse a sistemas de observabilidad como Jaeger o el propio ClickHouse (mediante el [OpenTelemetry Collector](/es/guides/use-cases/observability/build-your-own/integrating-opentelemetry)).

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

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

```csharp theme={null}
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Subscribe to ClickHouse driver spans
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // Or 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 de los spans
</h3>

Cada span incluye atributos estándar de OpenTelemetry para bases de datos, además de estadísticas de las consultas específicas de ClickHouse que facilitan la 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 trazado mediante `ClickHouseDiagnosticsOptions`:

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

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

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

<Warning>
  Activar `IncludeSqlInActivityTags` puede exponer datos sensibles en las trazas. Use esta opción con precaución en entornos de producción.
</Warning>

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

Al conectarse a ClickHouse por HTTPS, puede configurar TLS/SSL de varias formas.

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

En entornos de producción que requieran una lógica de validación de certificados personalizada, proporcione su propia instancia de `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**: mantenga `AutomaticDecompression` desactivado. El controlador descomprime las respuestas por su cuenta, por lo que no es necesario activarlo. Además, activarlo puede causar problemas al enviar solicitudes: en el momento del envío, el handler *también añade* todos los algoritmos de su máscara al encabezado `Accept-Encoding` saliente. Esto amplía los algoritmos anunciados por el controlador y permite que ClickHouse responda con un códec que no se había solicitado. Consulte [Descompresión de respuestas](#response-decompression).
  * **Tiempo de espera de inactividad**: configure `PooledConnectionIdleTimeout` con un valor inferior al `keep_alive_timeout` del servidor (10 segundos para ClickHouse Cloud) para evitar errores causados por conexiones semiabiertas.
</Note>

<h2 id="performance-tuning">
  Optimización del rendimiento
</h2>

En esta sección se explica cómo usar el client para obtener un rendimiento óptimo y qué opciones puede ajustar para adaptarlo a las necesidades de su caso de uso.

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

\| Si | Haga esto |
\|---|---|---|
\| Lee filas en objetos POCO | Use [`QueryAsync<T>`](#perf-read-path), no `MapTo<T>` |
\| Realiza inserciones masivas | Aumente [`InsertOptions.BatchSize`](#perf-insert-batching) |
\| Ejecuta una aplicación de consola o un proceso de trabajo que realiza muchas inserciones | Active [Server GC](#perf-gc) |
\| Lee resultados voluminosos a través de la red | Mantenga activada la compresión de las respuestas (activada de forma predeterminada) |
\| Inserta datos a través de una conexión rápida | Pruebe [`InsertOptions.Compressor = null`](#perf-compression) |
\| Inserta datos repetidamente en la misma tabla | Use [`UseSchemaCache` o `ColumnTypes`](#skip-schema-query) |
\| Lee resultados muy voluminosos | Aumente [`ReadBufferSize`](#perf-buffers) |

***

<h3 id="perf-read-path">
  Lectura: elija la ruta de materialización
</h3>

Hay tres formas de obtener una fila de un resultado, y no todas tienen el mismo costo. Algunas encapsulan los valores como objetos, lo que aumenta las asignaciones de memoria y reduce el rendimiento.

| Cómo se leen los datos | Encapsula cada valor | Notas |
| - | - | - |
| `QueryAsync<T>` | **No** | Lee los datos directamente del flujo y los asigna a las propiedades. Es la ruta rápida. |
| Métodos de acceso tipados del lector (`GetInt32`, `GetInt64`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetFieldValue<T>`) | **No** | Lee los valores tipados sin encapsularlos como objetos. |
| `MapTo<T>` | Sí | Primero materializa la fila y luego copia sus valores. |
| `GetValue` y `GetValues` | Sí | Devuelven `object`, por lo que deben encapsular cada valor solicitado. |

Para una lectura de 1,000,000 filas y 105 columnas del conjunto de datos *hits*:

| API | Memoria asignada |
| - | -: |
| `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 usan la ruta rápida cuando emplean métodos de acceso tipados.* linq2db registra `GetInt64`,
  `GetDouble` y `GetDateTime` para cada columna, lo que le permite leer 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 frecuencia
  y lee mediante `GetValue`, use `QueryAsync<T>` para esa consulta.
</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 caudal de inserción. `InsertOptions.BatchSize` tiene un valor predeterminado de
100 000 filas.

**Utilice lotes grandes.** En una inserción de 1 000 000 de filas, aumentar el tamaño de cada lote de 10 000 a 100 000 filas
dio estos resultados:

| 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, porque muchos productores envían pequeñas cantidades de filas por separado), utilice la [inserción asíncrona](#async-inserts) y deje que el servidor agrupe las filas en lotes.

**Envíos paralelos.** `InsertOptions.MaxDegreeOfParallelism` tiene un valor predeterminado de `1`. Auméntelo para enviar
varios lotes a la vez. Esto resulta especialmente útil si la compresión está activada, ya que cada lote se comprime
en su propio thread. Las sesiones no son compatibles con las inserciones paralelas: desactívelas o mantenga
`MaxDegreeOfParallelism = 1`.

**Elimine la consulta de comprobación del esquema.** Cada llamada a `InsertBinaryAsync` envía primero una consulta `SELECT ... WHERE 1=0`
para determinar los tipos de columna. Consulte [Cómo omitir la consulta de comprobación del esquema](#skip-schema-query) para evitar ese
viaje de ida y vuelta mediante `ColumnTypes` o `UseSchemaCache`.

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

***

<h3 id="perf-compression">
  Compresión: los resultados varían según la dirección
</h3>

La compresión reduce la cantidad de bytes transferidos a cambio de consumir CPU. Que compense depende de la dirección de la
transferencia, del ancho de banda de la conexión con el servidor de ClickHouse, de la eficacia del algoritmo de compresión elegido con sus datos y de si paga por cada byte transferido.

**Lecturas:** mantenga activada la compresión, salvo que el servidor se ejecute en la misma máquina. Este es el comportamiento predeterminado. En comparación con la transferencia sin compresión, `zstd` en el nivel 1
dio estos resultados:

| Del cliente al servidor | Efecto de la compresión |
| - | - |
| Mismo host (bucle local) | Supone un coste adicional del 8 % |
| Misma región de la nube | **Ahorra un 16 %** |
| Una región de distancia | **Ahorra un 33 %** |

**Inserciones:** mida el efecto antes de activar la compresión. El ahorro quizá no sea suficiente para justificarla. Tenga en cuenta también que la descompresión aumenta la carga del servidor; ese aumento es moderado con Zstd y LZ4, pero puede ser considerable con otros algoritmos (por ejemplo, Brotli).

Para desactivar la compresión de inserciones:

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

Para elegir un códec, ajustar los niveles de compresión y determinar a partir de qué punto le conviene comprimir, consulte
[Optimización de la compresión](#tuning-compression).

***

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

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

El controlador obtiene este búfer de un grupo compartido y lo devuelve cuando libera el lector, por lo que
no necesita asignar uno por cada consulta. Aumente su tamaño para reducir la frecuencia con la que debe rellenarse el búfer al procesar
resultados grandes. El controlador mantiene un búfer por cada lector abierto simultáneamente, por lo que el consumo de memoria
aumenta tanto con el tamaño del búfer como con el número de lectores concurrentes.

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

<Warning>
  *Libere siempre los recursos de los lectores.* Al hacerlo, el lector devuelve su búfer al grupo y libera su conexión HTTP.
  Si se abandona un lector sin liberar sus recursos, el búfer no vuelve al grupo y la conexión HTTP puede quedar
  inutilizable; la recolección de basura no sustituye la liberación explícita de recursos.
</Warning>

***

<h3 id="perf-gc">
  Entorno de ejecución y recolección de basura
</h3>

**Active Server GC en aplicaciones que realizan muchas inserciones.** Con el mismo código y la misma cantidad de
bytes asignados, las inserciones con Workstation GC fueron hasta un 97 % más lentas que con Server GC.

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

Los proyectos de ASP.NET Core ya tienen esta opción configurada. Las aplicaciones de consola, los servicios de trabajo y la mayoría de las
imágenes de contenedor no la tienen.

La causa es el tamaño del presupuesto de la generación 0. Workstation GC utiliza un presupuesto pequeño, por lo que los
búferes de corta duración que crea una inserción sobreviven a la generación 0 y pasan a la generación 1.
Esto aumenta la promoción y el trabajo de la generación 2. En una prueba de inserción,
se registraron 4,000 recolecciones de la generación 2 por cada 1,000 operaciones con Server GC, frente a 73,000 con
Workstation GC.

<Note>
  Server GC es una configuración orientada al caudal, no a la latencia. En las mismas mediciones, Server GC acumuló
  menos de la mitad del tiempo total de pausa, pero cada pausa fue más larga
  (percentil 95: 114.6 ms frente a 61.9 ms). Si su servicio es sensible a la latencia de las solicitudes más lentas, mida
  ambos modos antes de elegir uno.
</Note>

***

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

Establecer una nueva conexión TCP y completar el handshake TLS lleva bastante tiempo.
Reutilizar las conexiones reduce considerablemente la latencia de las consultas.

* No cree un cliente por cada solicitud. Cada cliente nuevo con su propio `HttpClient` crea un nuevo
  grupo de conexiones y requiere otro handshake. Utilice un único `ClickHouseClient` durante toda la vida útil de la aplicación. Es seguro para subprocesos y está diseñado para
  usarse como singleton.
* Para ADO.NET y los ORM, utilice `ClickHouseDataSource` para que todas las conexiones compartan el mismo grupo.

Para conocer todos los patrones, consulte
[Duración y agrupación de conexiones](#best-practices-connection-lifetime).

***

<h3 id="perf-measuring">
  Mida el rendimiento usted mismo
</h3>

En muchos casos, el rendimiento dependerá de la estructura de sus datos, de la velocidad de la conexión con el servidor,
de si prefiere aumentar el uso de CPU del client para reducir el del servidor (o viceversa), de las limitaciones de su hardware, etc.
Por eso, le recomendamos que mida el rendimiento con sus propios datos y en su entorno.

Para conocer la parte del trabajo que realiza el 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 ORM
</h2>

Los ORM requieren la API ADO.NET (`ClickHouseConnection`). Para gestionar correctamente la duración de las conexiones, créelas a partir de un `ClickHouseDataSource`:

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

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

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

`ClickHouse.Driver` funciona con Dapper. El controlador 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.

Utilice `ClickHouseDataSource` para gestionar correctamente la duración de la conexión:

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

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

<h4 id="dapper-parameter-passing">
  Formas de pasar parámetros
</h4>

Dapper admite todas las formas estándar de pasar parámetros:

**Objetos anónimos:**

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

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

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

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

<h4 id="dapper-pocos">
  Consultas que devuelven objetos POCO
</h4>

Dapper relaciona las columnas con las 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; }
}

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

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

<h4 id="dapper-clickhouse-param-syntax">
  Sintaxis de parámetros nativa de ClickHouse
</h4>

Si necesita controlar explícitamente el tipo, use la sintaxis `{param:Type}` de ClickHouse directamente en SQL y proporcione los valores de los parámetros mediante un `Dictionary<string, object>`. No combine las sintaxis `@param` y `{param:Type}` para un 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 en 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 la consulta como `WHERE id IN (@Ids1, @Ids2, @Ids3)`, y el controlador convierte cada parámetro expandido.

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

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

<h4 id="dapper-type-handlers">
  Handlers de tipos personalizados
</h4>

Algunos tipos de ClickHouse, como `ITuple`, `BigInteger` y `ClickHouseDecimal`, requieren registrar handlers al inicio:

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

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

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

Consulte el [ejemplo de Dapper](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Http/ORM/ORM_001_Dapper.cs) para ver cómo se implementa un controlador de tipos personalizado.

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

`GetAll<T>()` y `Get<T>(id)` funcionan. `Insert<T>()`, en cambio, no: genera sintaxis propia de SQL Server (`SCOPE_IDENTITY`, `[]`). Se recomienda utilizar el método nativo `InsertBinaryAsync` de `ClickHouseClient` en su lugar.

```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 las columnas de ClickHouse (se distinguen mayúsculas y minúsculas).

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

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

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

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

**Ejemplo de uso:**

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

Las correspondencias de tablas se pueden definir mediante atributos o mediante una configuración fluida. Si los nombres de tus clases y propiedades coinciden exactamente con los nombres de las tablas y columnas, no necesitas ninguna configuración adicional:

```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 insertar grandes volúmenes de datos 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. Asocie clases de C# a tablas de ClickHouse, haga consultas con LINQ e inserte 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 de motores de tabla específicos 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>

Defina la entidad y el `DbContext` y, después, haga 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");
}

// Query
await using var ctx = new AnalyticsContext();

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

<h4 id="ef-core-types">
  Tipos compatibles
</h4>

| Categoría | Tipos de ClickHouse | Tipos de CLR |
| - | - | - |
| **Enteros** | `Int8`–`Int64`, `UInt8`–`UInt64` | `sbyte`, `short`, `int`, `long`, `byte`, `ushort`, `uint`, `ulong` |
| **Enteros de gran tamaño** | `Int128`, `Int256`, `UInt128`, `UInt256` | `BigInteger` |
| **Números de coma flotante** | `Float32`, `Float64`, `BFloat16` | `float`, `double` |
| **Decimales** | `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` | `decimal` o `ClickHouseDecimal` |
| **Booleanos** | `Bool` | `bool` |
| **Cadenas** | `String`, `FixedString(N)` | `string` |
| **Enumeraciones** | `Enum8(...)`, `Enum16(...)` | `string` o `enum` de C# |
| **Fecha y 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>` |
| **Tuplas** | `Tuple(T1, ...)` | `Tuple<...>` o `ValueTuple<...>` |
| **Variant** | `Variant(T1, T2, ...)` | `object` |
| **Dynamic** | `Dynamic` | `object` |
| **JSON** | `Json` | `JsonNode` o `string` |
| **Tipos geográficos** | `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` | `Tuple<double,double>` y arrays de este tipo; `object` para Geometry |
| **Envolturas** | `Nullable(T)`, `LowCardinality(T)` | Se extrae automáticamente el tipo subyacente |

Use `ClickHouseDecimal` (de `ClickHouse.Driver.Numerics`) en lugar de `decimal` si necesita toda la precisión de las columnas `Decimal128`/`Decimal256`: el tipo `decimal` de .NET admite como máximo entre 28 y 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` y `Max`; también admite `HAVING` (`.Where()` después de `.GroupBy()`), varias agregaciones en una misma proyección y `OrderBy` aplicado a resultados de agregaciones.

**JOIN:** `Join` (INNER) y patrones `GroupJoin`/`SelectMany` (LEFT y CROSS). LEFT JOIN devuelve `null` para las filas sin coincidencia (consulte [Semántica de los valores nulos en 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 en línea:** las uniones y `Contains` con colecciones en memoria (`int[]`, `List<T>`, etc.) se traducen en una serie de operaciones UNION.

**Métodos de cadenas:** `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 las funciones equivalentes de ClickHouse: aritméticas, logarítmicas, trigonométricas y auxiliares.

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

El provider añade automáticamente `set_join_use_nulls=1` a todas las rutas de conexión para que el comportamiento de JOIN coincida con el que espera Entity Framework.

Si el servidor de ClickHouse o el perfil impiden cambiar esta configuración (por ejemplo, un perfil con `readonly=1`), desactive esta opción con:

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

Cuando se activa la opción de exclusión, LEFT JOIN devuelve los valores predeterminados de las columnas de ClickHouse, por lo que la detección de propiedades de navegación basada en valores nulos de EF deja de funcionar como se espera. Compare explícitamente con `0` / `""` en lugar de usar `== null`.

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

`SaveChanges` utiliza la API nativa `InsertBinaryAsync` del controlador, con codificación RowBinary y un cuerpo de solicitud comprimido, lo que resulta mucho más eficiente que el SQL parametrizado:

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

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

await ctx.SaveChangesAsync();
```

Las entidades pasan de `Added` a `Unchanged` después de guardarse, como ocurre con cualquier otro provider 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 datos de alto caudal, use `BulkInsertAsync` en lugar de `SaveChanges`. Este método de extensión de `DbContext` prescinde por completo del seguimiento de cambios, la resolución de identidades y la gestión de estados de EF Core: llama directamente a `InsertBinaryAsync` del controlador con codificación RowBinary y un cuerpo de solicitud comprimido.

Por eso, resulta adecuado para cargar grandes conjuntos de datos si no necesita hacer un seguimiento de las entidades después de insertarlas:

```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>`: las entidades se procesan una a una sin cargarlas todas en memoria. El valor devuelto indica cuántas filas se han insertado. Tras la inserción, las entidades **no** quedan asociadas al `DbContext`, por lo que no pasan del estado `Added` al estado `Unchanged`.

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

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

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

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

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

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

El sistema `ValueConverter` de EF Core permite convertir tipos personalizados en tipos que el provider ya admite. El provider nunca llega a trabajar con el tipo personalizado: EF Core lo convierte al pasar de una capa a otra.

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

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

**Class de conversor 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] };
    }
}

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

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

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

Para los tipos escalares como `string`, `int`, `DateTime`, etc., el provider infiere automáticamente el tipo de ClickHouse. Para los tipos parametrizados y las envolturas, debe indicar el tipo de ClickHouse de forma explícita.

**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 envolturas anidadas como `Array(Nullable(Int32))` y `LowCardinality(Nullable(String))`: el provider elimina automáticamente las envolturas `Nullable` y `LowCardinality` en todos los niveles de anidamiento.

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

Las columnas `Variant(T1, T2, ...)` y `Dynamic` de ClickHouse se representan como `object` en .NET. Como `object` es demasiado genérico para inferir el tipo automáticamente, debe especificar el tipo de almacenamiento mediante `.HasColumnType()`:

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

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

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

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

El provider admite el tipo de columna `Json` de ClickHouse y lo asigna a `System.Text.Json.Nodes.JsonNode` (opción principal) o a `string` (mediante un `ValueConverter` automático):

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

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

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

La lectura y escritura de JSON son compatibles 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, configure la propiedad como `string` con un tipo de columna `Json`; el provider aplicará automáticamente un `ValueConverter`:

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

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

<Note>
  * **No se traducen las rutas JSON** — `entity.Data["name"]` en LINQ no se convierte a la sintaxis SQL `data.name` de ClickHouse. Filtre por columnas que no sean JSON y examine el contenido JSON en memoria.
  * **Semántica de NULL** — El tipo JSON de ClickHouse devuelve `{}` (un objeto vacío) para los valores NULL, en lugar de SQL NULL.
  * **Precisión de los enteros** — ClickHouse almacena todos los enteros en JSON como `Int64`. Al leerlos mediante `JsonNode`, use `GetValue<long>()` en lugar de `GetValue<int>()`.
</Note>

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

Configure los motores de tabla 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 provider usa `MergeTree` de forma predeterminada y determina `ORDER BY` a partir 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:

| Motor | Método fluido | Notas |
| - | - | - |
| `MergeTree` | `HasMergeTreeEngine()` | Predeterminado si no se configura ningún motor |
| `ReplacingMergeTree` | `HasReplacingMergeTreeEngine("Version", "IsDeleted")` o `HasReplacingMergeTreeEngine<T>(e => e.Version)` | Las columnas Version / IsDeleted son opcionales |
| `SummingMergeTree` | `HasSummingMergeTreeEngine(…)` o `HasSummingMergeTreeEngine<T>(e => new { … })` | Las columnas que se suman son opcionales |
| `AggregatingMergeTree` | `HasAggregatingMergeTreeEngine()` | — |
| `CollapsingMergeTree` | `HasCollapsingMergeTreeEngine("Sign")` o `HasCollapsingMergeTreeEngine<T>(e => e.Sign)` | La columna `Sign` debe ser de tipo `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 añaden al constructor de motores que devuelve `HasXxxEngine()`.

**Funciones a nivel de columna:** `HasCodec`, `HasTtl`, `HasComment`, `HasDefault` — todas se tienen en cuenta en 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);

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

Los índices estándar (que no son de omisión de datos) se ignoran sin previo aviso porque no tienen un equivalente en ClickHouse. Los índices únicos provocan una excepción porque 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 compatibles:

| Operación | Emite |
| - | - |
| `CREATE TABLE` | Incluye la cláusula del motor, ORDER BY, PARTITION BY, SETTINGS, códecs de columna, TTL, comentarios y valores predeterminados |
| `ALTER TABLE ADD COLUMN` | — |
| `ALTER TABLE DROP COLUMN` | — |
| `ALTER TABLE MODIFY COLUMN` | Gestiona los cambios de tipo y la adición o 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 exige el cumplimiento de las claves foráneas. Las migraciones rechazan `AddForeignKey`; el validador emite una advertencia al generar el modelo. |
| Restricciones de unicidad / índices únicos | ClickHouse no exige la unicidad. Los índices únicos provocan una excepción durante la migración. |
| Valores generados por el servidor (autoincremento / `IDENTITY`) | ClickHouse no ofrece ninguna función equivalente. |
| Columnas `Nested(…)` | Aún no se admiten como tipo CLR mapeado. |
| Entidades dependientes como JSON (`.ToJson()`) | El mapeo estructural de JSON para entidades dependientes aún no está implementado. En su lugar, use `JsonNode` / `string` en una columna `Json` (consulte [Columnas JSON](#ef-core-json)). |

Al margen de las migraciones, el provider tampoco admite todavía:

* **`UPDATE` / `DELETE`**
* **Transacciones**: `BeginTransaction` no tiene 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 y examine el JSON en memoria.

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

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

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

Esto genera una ambigüedad cuando una columna de ClickHouse contiene una tupla de 8 elementos y el último es, a su vez, una tupla; por ejemplo, `Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String))`. El controlador no puede distinguir entre:

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

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

El controlador interpreta el octavo argumento como TRest (es decir, lo aplana), por lo que serializa incorrectamente la tupla de 8 elementos con una tupla anidada.

Esto afecta tanto a `System.Tuple` como a `ValueTuple`, ya que ambos utilizan el anidamiento de TRest cuando tienen más de 7 elementos. Las tuplas de 7 elementos o menos y aquellas cuyo último elemento no es una tupla no se ven afectadas.

**Solución alternativa:** Envuelva la tupla interna en un nivel adicional de anidamiento para que el controlador pueda distinguirla del anidamiento de TRest:

```csharp theme={null}
// Instead of this (ambiguous — is it 8 elements or 9 flat?):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create("a", "b"))

// Do this (unambiguous — inner tuple is wrapped):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(Tuple.Create("a", "b")))
```

***

<h3 id="aggregatefunction-columns">
  Columnas de tipo AggregateFunction
</h3>

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

Para insertar datos:

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

Para seleccionar:

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

***
