El client oficial de Rust para conectarse a ClickHouse, desarrollado originalmente por Paul Loyd. El código fuente del client está disponible en el repositorio de GitHub.
Descripción general
- Usa
serdepara codificar y decodificar filas. - Admite atributos de
serde:skip_serializing,skip_deserializing,rename. - Usa el formato
RowBinarya través de HTTP.- Está previsto cambiar a
Nativesobre TCP.
- Está previsto cambiar a
- Admite TLS (mediante las features
native-tlsyrustls-tls). - Admite compresión y descompresión (LZ4).
- Proporciona APIs para consultar o insertar datos, ejecutar DDLs y realizar agrupación por lotes en el cliente.
- Proporciona mocks útiles para pruebas unitarias.
Instalación
Para usar este crate, añade lo siguiente a tu Cargo.toml:
[dependencies]
clickhouse = "0.12.2"
[dev-dependencies]
clickhouse = { version = "0.12.2", features = ["test-util"] }Véase también: la página de crates.io.
Características de Cargo
lz4(habilitada de forma predeterminada) — habilita las variantesCompression::Lz4yCompression::Lz4Hc(_). Si está habilitada,Compression::Lz4se usa de forma predeterminada para todas las consultas, excepto paraWATCH.native-tls— admite URL con el esquemaHTTPSmediantehyper-tls, que enlaza con OpenSSL.rustls-tls— admite URL con el esquemaHTTPSmediantehyper-rustls, que no enlaza con OpenSSL.inserter— habilitaclient.inserter().test-util— agrega mocks. Consulta el ejemplo. Úsalo solo endev-dependencies.watch— habilita la funcionalidadclient.watch. Consulta la sección correspondiente para más detalles.uuid— agregaserde::uuidpara trabajar con el crate uuid.time— agregaserde::timepara trabajar con el crate time.
Compatibilidad de versiones de ClickHouse
El client es compatible con las versiones LTS de ClickHouse y posteriores, así como con ClickHouse Cloud.
ClickHouse server anterior a la v22.6 procesa RowBinary de forma incorrecta en algunos casos poco frecuentes.
Puede usar la v0.11+ y habilitar la feature wa-37420 para solucionar este problema. Nota: esta feature no debe usarse con versiones más recientes de ClickHouse.
Ejemplos
Nuestro objetivo es abarcar varios escenarios de uso del client mediante los ejemplos del repositorio del client. La información general está disponible en el README de ejemplos.
Si algo no está claro o falta en los ejemplos o en la documentación siguiente, no dude en contactarnos.
Uso
Crear una instancia de client
use clickhouse::Client;
let client = Client::default()
// should include both protocol and port
.with_url("http://localhost:8123")
.with_user("name")
.with_password("123")
.with_database("test");Conexión HTTPS o ClickHouse Cloud
HTTPS funciona con las features de Cargo rustls-tls o native-tls.
Luego, cree el client de la forma habitual. En este ejemplo, se usan variables de entorno para almacenar los detalles de conexión:
fn read_env_var(key: &str) -> String {
env::var(key).unwrap_or_else(|_| panic!("{key} env variable should be set"))
}
let client = Client::default()
.with_url(read_env_var("CLICKHOUSE_URL"))
.with_user(read_env_var("CLICKHOUSE_USER"))
.with_password(read_env_var("CLICKHOUSE_PASSWORD"));Vea también:
- Ejemplo de HTTPS con ClickHouse Cloud en el repositorio del client. Esto también debería aplicarse a las conexiones HTTPS on-premise.
Selección de filas
use serde::Deserialize;
use clickhouse::Row;
use clickhouse::sql::Identifier;
#[derive(Row, Deserialize)]
struct MyRow<'a> {
no: u32,
name: &'a str,
}
let table_name = "some";
let mut cursor = client
.query("SELECT ?fields FROM ? WHERE no BETWEEN ? AND ?")
.bind(Identifier(table_name))
.bind(500)
.bind(504)
.fetch::<MyRow<'_>>()?;
while let Some(row) = cursor.next().await? { .. }- El marcador
?fieldsse reemplaza porno, name(campos deRow). - El marcador
?se reemplaza por los valores de las siguientes llamadas abind(). - Se pueden usar los prácticos métodos
fetch_one::<Row>()yfetch_all::<Row>()para obtener la primera fila o todas las filas, respectivamente. sql::Identifierse puede usar para vincular nombres de tablas.
Nota: como toda la respuesta se envía en streaming, los cursores pueden devolver un error incluso después de producir algunas filas. Si esto ocurre en su caso de uso, puede probar query(...).with_option("wait_end_of_query", "1") para habilitar el búfer de respuesta en el servidor. Más detalles. La opción buffer_size también puede ser útil.
Insertar filas
use serde::Serialize;
use clickhouse::Row;
#[derive(Row, Serialize)]
struct MyRow {
no: u32,
name: String,
}
let mut insert = client.insert("some")?;
insert.write(&MyRow { no: 0, name: "foo".into() }).await?;
insert.write(&MyRow { no: 1, name: "bar".into() }).await?;
insert.end().await?;- Si no se llama a
end(), elINSERTse cancela. - Las filas se envían progresivamente en flujo para distribuir la carga de red.
- ClickHouse inserta lotes de forma atómica solo si todas las filas caben en la misma partición y su número es inferior a
max_insert_block_size.
Async insert (agrupación por lotes del lado del servidor)
Puede usar las inserciones asíncronas de ClickHouse para evitar la agrupación por lotes en el cliente de los datos de entrada. Esto puede hacerse simplemente pasando la opción async_insert al método insert (o incluso a la propia instancia de Client, para que afecte a todas las llamadas a insert).
let client = Client::default()
.with_url("http://localhost:8123")
.with_option("async_insert", "1")
.with_option("wait_for_async_insert", "0");Véase también:
- Ejemplo de async insert en el repositorio del client.
Feature Inserter (agrupación por lotes en el cliente)
Requiere la feature inserter de Cargo.
let mut inserter = client.inserter("some")?
.with_timeouts(Some(Duration::from_secs(5)), Some(Duration::from_secs(20)))
.with_max_bytes(50_000_000)
.with_max_rows(750_000)
.with_period(Some(Duration::from_secs(15)));
inserter.write(&MyRow { no: 0, name: "foo".into() })?;
inserter.write(&MyRow { no: 1, name: "bar".into() })?;
let stats = inserter.commit().await?;
if stats.rows > 0 {
println!(
"{} bytes, {} rows, {} transactions have been inserted",
stats.bytes, stats.rows, stats.transactions,
);
}
// don't forget to finalize the inserter during the application shutdown
// and commit the remaining rows. `.end()` will provide stats as well.
inserter.end().await?;Inserterfinaliza la inserción activa encommit()si se alcanza cualquiera de los umbrales (max_bytes,max_rows,period).- El intervalo entre la finalización de
INSERTactivas puede ajustarse mediantewith_period_biaspara evitar picos de carga causados por insertores en paralelo. Inserter::time_left()puede usarse para detectar cuándo termina el período actual. Llame aInserter::commit()de nuevo para comprobar los límites si su flujo emite elementos con poca frecuencia.- Los umbrales de tiempo se implementan usando el crate quanta para acelerar
inserter. No se usa sitest-utilestá habilitado (por tanto, el tiempo puede gestionarse contokio::time::advance()en pruebas personalizadas). - Todas las filas entre llamadas a
commit()se insertan en la misma sentenciaINSERT.
Ejecutar DDLs
Con una implementación de un solo nodo, basta con ejecutar los DDLs de esta forma:
client.query("DROP TABLE IF EXISTS some").execute().await?;Sin embargo, en implementaciones en clúster con un balanceador de carga o en ClickHouse Cloud, se recomienda esperar a que el DDL se aplique en todas las réplicas mediante la opción wait_end_of_query. Esto puede hacerse así:
client
.query("DROP TABLE IF EXISTS some")
.with_option("wait_end_of_query", "1")
.execute()
.await?;Ajustes de ClickHouse
Puede aplicar varios ajustes de ClickHouse mediante el método with_option. Por ejemplo:
let numbers = client
.query("SELECT number FROM system.numbers")
// This setting will be applied to this particular query only;
// it will override the global client setting.
.with_option("limit", "3")
.fetch_all::<u64>()
.await?;Además de query, funciona de forma similar con los métodos insert e inserter; asimismo, se puede llamar al mismo método en la instancia Client para establecer la configuración global de todas las consultas.
Query ID
Con .with_option, puedes configurar la opción query_id para identificar las consultas en el registro de consultas de ClickHouse.
let numbers = client
.query("SELECT number FROM system.numbers LIMIT 1")
.with_option("query_id", "some-query-id")
.fetch_all::<u64>()
.await?;Además de query, funciona de forma similar con los métodos insert e inserter.
Véase también: ejemplo de query_id en el repositorio del client.
ID de sesión
Al igual que query_id, puedes establecer session_id para ejecutar las sentencias en la misma sesión. session_id puede establecerse de forma global en el nivel de client, o en cada llamada a query, insert o inserter.
let client = Client::default()
.with_url("http://localhost:8123")
.with_option("session_id", "my-session");Consulta también: ejemplo de session_id en el repositorio del client.
Encabezados HTTP personalizados
Si usas autenticación mediante proxy o necesitas enviar encabezados personalizados, puedes hacerlo así:
let client = Client::default()
.with_url("http://localhost:8123")
.with_header("X-My-Header", "hello");Véase también: ejemplo de encabezados HTTP personalizados en el repositorio del client.
client HTTP personalizado
Esto puede ser útil para ajustar la configuración del pool de conexiones HTTP subyacente.
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client as HyperClient;
use hyper_util::rt::TokioExecutor;
let connector = HttpConnector::new(); // or HttpsConnectorBuilder
let hyper_client = HyperClient::builder(TokioExecutor::new())
// For how long keep a particular idle socket alive on the client side (in milliseconds).
// It is supposed to be a fair bit less that the ClickHouse server KeepAlive timeout,
// which was by default 3 seconds for pre-23.11 versions, and 10 seconds after that.
.pool_idle_timeout(Duration::from_millis(2_500))
// Sets the maximum idle Keep-Alive connections allowed in the pool.
.pool_max_idle_per_host(4)
.build(connector);
let client = Client::with_http_client(hyper_client).with_url("http://localhost:8123");Consulte también: ejemplo de client HTTP personalizado en el repositorio del client.
Tipos de datos
(U)Int(8|16|32|64|128)se corresponde con los tipos(u|i)(8|16|32|64|128)equivalentes, y viceversa, o con newtypes basados en ellos.(U)Int256no se admite directamente, pero hay una solución alternativa.Float(32|64)se corresponde con los tiposf(32|64)equivalentes, y viceversa, o con newtypes basados en ellos.Decimal(32|64|128)se corresponde con los tiposi(32|64|128)equivalentes, y viceversa, o con newtypes basados en ellos. Es más práctico usarfixnumu otra implementación de números de punto fijo con signo.Booleanse corresponde conbool, y viceversa, o con newtypes basados en él.Stringse corresponde con cualquier tipo de cadena o bytes, y viceversa; por ejemplo,&str,&[u8],String,Vec<u8>oSmartString. Los newtypes también son compatibles. Para almacenar bytes, considere usarserde_bytes, ya que es más eficiente.
#[derive(Row, Debug, Serialize, Deserialize)]
struct MyRow<'a> {
str: &'a str,
string: String,
#[serde(with = "serde_bytes")]
bytes: Vec<u8>,
#[serde(with = "serde_bytes")]
byte_slice: &'a [u8],
}FixedString(N)se admite como un array de bytes, p. ej.,[u8; N].
#[derive(Row, Debug, Serialize, Deserialize)]
struct MyRow {
fixed_str: [u8; 16], // FixedString(16)
}Enum(8|16)se admite medianteserde_repr.
use serde_repr::{Deserialize_repr, Serialize_repr};
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
level: Level,
}
#[derive(Debug, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
enum Level {
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
}UUIDse mapea desde y haciauuid::Uuidmedianteserde::uuid. Requiere la featureuuid.
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
#[serde(with = "clickhouse::serde::uuid")]
uuid: uuid::Uuid,
}IPv6se corresponde constd::net::Ipv6Addr.IPv4se corresponde constd::net::Ipv4Addrmedianteserde::ipv4.
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
#[serde(with = "clickhouse::serde::ipv4")]
ipv4: std::net::Ipv4Addr,
}Datese puede convertir a/desdeu16o unnewtypebasado en este, y representa la cantidad de días transcurridos desde1970-01-01. Además,time::Datetambién es compatible usandoserde::time::date, para lo cual se requiere la featuretime.
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
days: u16,
#[serde(with = "clickhouse::serde::time::date")]
date: Date,
}Date32se mapea desde/haciai32o unnewtypeque lo envuelve, y representa una cantidad de días transcurridos desde1970-01-01. Además,time::Datees compatible medianteserde::time::date32, lo que requiere la funcionalidadtime.
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
days: i32,
#[serde(with = "clickhouse::serde::time::date32")]
date: Date,
}DateTimese corresponde conu32o con unnewtypebasado en él, y representa un número de segundos transcurridos desde la época de Unix. Además,time::OffsetDateTimees compatible medianteserde::time::datetime, lo que requiere lafeaturetime.
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
ts: u32,
#[serde(with = "clickhouse::serde::time::datetime")]
dt: OffsetDateTime,
}DateTime64(_)se convierte a/desdei32o unnewtypebasado en este, y representa el tiempo transcurrido desde la época Unix. Además,time::OffsetDateTimees compatible mediante el uso deserde::time::datetime64::*, lo que requiere activar la featuretime.
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
ts: i64, // elapsed s/us/ms/ns depending on `DateTime64(X)`
#[serde(with = "clickhouse::serde::time::datetime64::secs")]
dt64s: OffsetDateTime, // `DateTime64(0)`
#[serde(with = "clickhouse::serde::time::datetime64::millis")]
dt64ms: OffsetDateTime, // `DateTime64(3)`
#[serde(with = "clickhouse::serde::time::datetime64::micros")]
dt64us: OffsetDateTime, // `DateTime64(6)`
#[serde(with = "clickhouse::serde::time::datetime64::nanos")]
dt64ns: OffsetDateTime, // `DateTime64(9)`
}Tuple(A, B, ...)se corresponde con(A, B, ...), y viceversa, o con unnewtypeque lo envuelve.Array(_)se corresponde con cualquierslice, y viceversa; por ejemplo,Vec<_>,&[_]. También se admiten tipos nuevos.Map(K, V)se comporta comoArray((K, V)).LowCardinality(_)se admite de forma transparente.Nullable(_)se corresponde conOption<_>, y viceversa. Para las funciones auxiliaresclickhouse::serde::*, agregue::option.
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
#[serde(with = "clickhouse::serde::ipv4::option")]
ipv4_opt: Option<Ipv4Addr>,
}Nestedse admite si se proporcionan varios arrays con cambio de nombre.
// CREATE TABLE test(items Nested(name String, count UInt32))
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
#[serde(rename = "items.name")]
items_name: Vec<String>,
#[serde(rename = "items.count")]
items_count: Vec<u32>,
}- Se admiten los tipos
Geo.Pointse comporta como una tupla(f64, f64), y el resto de los tipos no son más que slices de puntos.
type Point = (f64, f64);
type Ring = Vec<Point>;
type Polygon = Vec<Ring>;
type MultiPolygon = Vec<Polygon>;
type LineString = Vec<Point>;
type MultiLineString = Vec<LineString>;
#[derive(Row, Serialize, Deserialize)]
struct MyRow {
point: Point,
ring: Ring,
polygon: Polygon,
multi_polygon: MultiPolygon,
line_string: LineString,
multi_line_string: MultiLineString,
}- Los tipos de datos
Variant,DynamicyJSON(nuevo) aún no son compatibles.
Simulación
El crate proporciona utilidades para simular el servidor de CH y probar consultas DDL, SELECT, INSERT y WATCH. Esta funcionalidad puede habilitarse con la feature test-util. Úsela solo como dependencia de desarrollo.
Consulte el ejemplo.
Resolución de problemas
CANNOT_READ_ALL_DATA
La causa más común del error CANNOT_READ_ALL_DATA es que la definición de la fila del lado de la aplicación no coincide con la de ClickHouse.
Considere la siguiente tabla:
CREATE OR REPLACE TABLE event_log (id UInt32)
ENGINE = MergeTree
ORDER BY timestampEntonces, si EventLog está definido en la aplicación con tipos que no coinciden, por ejemplo:
#[derive(Debug, Serialize, Deserialize, Row)]
struct EventLog {
id: String, // <- should be u32 instead!
}Al insertar los datos, puede aparecer el siguiente error:
Error: BadResponse("Code: 33. DB::Exception: Cannot read all data. Bytes read: 5. Bytes expected: 23.: (at row 1)\n: While executing BinaryRowInputFormat. (CANNOT_READ_ALL_DATA)")En este ejemplo, se soluciona definiendo correctamente la struct EventLog:
#[derive(Debug, Serialize, Deserialize, Row)]
struct EventLog {
id: u32
}Limitaciones conocidas
- Los tipos de datos
Variant,DynamicyJSON(nuevo) aún no se admiten. - El enlace de parámetros del lado del servidor aún no se admite; consulta este issue para seguir su evolución.
Contáctanos
Si tienes alguna pregunta o necesitas ayuda, no dudes en contactarnos en el Slack de la comunidad o a través de GitHub issues.