clickhouse-rs

repository·async-await·Indexed 43 days ago

https://github.com/suharev7/clickhouse-rs

An asynchronous ClickHouse client library for Rust (version 1.1.0-alpha.1) providing high-performance access to Yandex ClickHouse. It supports both Tokio and async-std runtimes, TLS via native-tls or rustls, and provides a Pool-based connection management system for executing DDL statements, inserting data via Blocks, and fetching query results.

Tokens
10.1K
Snippets
34
Records
41
Agent score
55%

What's inside clickhouse-rs

  1. Select an I/O runtime and TLS support

    async-await

    The library uses optional features to optimize compile times. You must choose between tokio_io and async_std, as they are mutually exclusive.

    • tokio_io (enabled by default): Uses the Tokio runtime.
    • async_std: Uses the async-std runtime.
    • tls: Enables TLS support (Note: tls is only compatible when using tokio_io).
  2. How Column types and data are structured

    async-await

    In clickhouse-rs, a Column<K> is a type-safe wrapper around ArcColumnData.

    • Column<K>: The primary user-facing structure. The generic parameter K is a ColumnType that acts as a marker for the expected SQL type.
    • ColumnType: A trait used to mark valid column types. The library provides Simple and Complex markers.
    • ArcColumnData: An atomically reference-counted container that holds the actual raw data and implements the logic for data access, casting, and iteration.

    This design allows columns to be cloned efficiently (via Arc) while maintaining type safety through the K marker.

  3. Manage data using the Block struct

    async-await

    A Block represents a collection of columns in ClickHouse, organized by rows. It is the primary container for data being sent to or received from the server. You can construct blocks manually, add columns, or push rows using a builder pattern.

    Key capabilities:

    • Column Access: Retrieve columns by index (usize) or by name (&str or String).
    • Cell Access: Use .get::<T>(row, col) to retrieve a specific value from a cell, where T must implement FromSql.
    • Row Iteration: Use .rows() to get an iterator over the rows in the block.
    • Data Insertion: Use .push(row) with a RowBuilder (like the row! macro) to append data.
    • Chunking: Use .chunks(n) to split a large block into smaller blocks of size n.
    // Creating a block and adding columns
    let block = Block::<Simple>::new()
        .column("hello_id", vec![5_u32, 6_u32])
        .column("value", vec!["lol", "zuz"]);
    
    // Accessing a specific cell
    let val: u32 = block.get(0, "hello_id").unwrap();
    
    // Iterating over rows
    for row in block.rows() {
        let id: u32 = row.get("hello_id").unwrap();
    }
  4. Configure the ClickHouse connection URL

    async-await

    The client uses a URL schema to define connection parameters. The format is:

    schema://user:password@host[:port]/database?param1=value1&...&paramN=valueN

    Connection Parameters

    ParameterDescriptionDefault
    compressionCompression algorithm. Options: none, lz4none
    connection_timeoutTimeout for establishing a connection500 ms
    query_timeoutTimeout for queries180 sec
    insert_timeoutTimeout for inserts180 sec
    execute_timeoutTimeout for execute operations180 sec
    keepaliveTCP keep alive timeout in millisecondsN/A
    nodelayWhether to enable TCP_NODELAYtrue
    pool_minLower bound of opened connections for Pool10
    pool_maxUpper bound of opened connections for Pool20
    ping_before_queryPing server before executing any querytrue
    send_retriesCount of retries to send request to server3
    retry_timeoutWait time before next retry5 sec
    ping_timeoutTimeout for ping operations500 ms
    alt_hostsComma separated list of single address hosts for load-balancingN/A

    Example URL: tcp://user:password@host:9000/clicks?compression=lz4&ping_timeout=42ms

    tcp://user:password@host:9000/clicks?compression=lz4&ping_timeout=42ms
  5. Execute queries and insert data using `Pool` and `ClientHandle`

    async-await

    The recommended way to interact with ClickHouse is to create a Pool and obtain a ClientHandle.

    Workflow

    1. Create a Pool: Initialize a Pool with a connection URL.
    2. Get a Handle: Use pool.get_handle().await? to get a ClientHandle.
    3. Execute DDL: Use client.execute(sql).await? for statements like CREATE TABLE.
    4. Insert Data: Use client.insert(table, block).await? to insert a Block of data.
    5. Query Data: Use client.query(sql).fetch_all().await? to retrieve results as a Block.
    6. Iterate Rows: Use block.rows() to iterate through the returned rows and row.get(column_name)? to extract values.

    Example

    use clickhouse_rs::{Block, Pool, errors::Error};
    
    #[tokio::main]
    async fn main() -> Result<(), Error> {
        let ddl = "CREATE TABLE IF NOT EXISTS payment (customer_id UInt32, amount UInt32, account_name Nullable(FixedString(3))) Engine=Memory";
    
        let block = Block::new()
            .column("customer_id", vec![1_u32, 3, 5, 7, 9])
            .column("amount", vec![2_u32, 4, 6, 8, 10])
            .column("account_name", vec![Some("foo"), None, None, None, Some("bar")]);
    
        let pool = Pool::new("tcp://localhost:9000?compression=lz4");
        let mut client = pool.get_handle().await?;
    
        client.execute(ddl).await?;
        client.insert("payment", block).await?;
    
        let block = client.query("SELECT * FROM payment").fetch_all().await?;
    
        for row in block.rows() {
            let id: u32 = row.get("customer_id")?;
            let amount: u32 = row.get("amount")?;
            let name: Option<&str> = row.get("account_name")?;
            println!("Found payment {id}: {amount} {name:?}");
        }
        Ok(())
    }
    # use std::env;
    # use clickhouse_rs::{Block, Pool, errors::Error};
    #
    # #[tokio::main]
    # async fn main() -> Result<(), Error> {
    #     let ddl = r"
    #         CREATE TABLE IF NOT EXISTS payment (
    #             customer_id  UInt32,
    #             amount       UInt32,
    #             account_name Nullable(FixedString(3))
    #         ) Engine=Memory";
    #
    #     let block = Block::new()
    #         .column("customer_id",  vec![1_u32,  3,  5,  7,  9])
    #         .column("amount",       vec![2_u32,  4,  6,  8, 10])
    #         .column("account_name", vec![Some("foo"), None, None, None, Some("bar")]);
    #
    #     let pool = Pool::new("tcp://localhost:9000?compression=lz4".into());
    #
    #     let mut client = pool.get_handle().await?;
    #     client.execute(ddl).await?;
    #     client.insert("payment", block).await?;
    #     let block = client.query("SELECT * FROM payment").fetch_all().await?;
    #
    #     for row in block.rows() {
    #         let id: u32             = row.get("customer_id")?;
    #         let amount: u32         = row.get("amount")?;
    #         let name: Option<&str>  = row.get("account_name")?;
    #         println!("Found payment {id}: {amount} {name:?}");
    #     }
    #     Ok(())
    # }
  6. Run ClickHouse via Docker Compose

    async-await

    To run a ClickHouse server instance for local development or testing with clickhouse-rs, you can use the provided docker-compose.yml configuration. This setup uses the yandex/clickhouse-server image and exposes the following ports:

    • 8123: HTTP interface (typically used for REST API interactions).
    • 9000: Native TCP interface (typically used by high-performance drivers like clickhouse-rs).
    version: '3'
    services:
      clickhouse:
        image: yandex/clickhouse-server
        ports:
          - 8123:8123
          - 9000:9000
  7. Select optional features for clickhouse-rs

    async-await

    You can optimize compile times and runtime behavior by enabling specific features:

    • tokio_io (default): Uses Tokio for asynchronous I/O.
    • async_std: Uses async-std for asynchronous I/O (cannot be used with tokio_io).
    • tls-native-tls: TLS support using native-tls (requires tokio_io).
    • tls-rustls: TLS support using rustls (requires tokio_io).

    Note: tls-native-tls and tls-rustls are mutually exclusive.

  8. Use the ClickHouse client to execute queries and inserts

    async-await

    To interact with ClickHouse, use a Pool to manage connections. You can create a Block to prepare data for insertion, execute DDL statements, and fetch query results as blocks.

    use clickhouse_rs::{Block, Pool};
    use std::error::Error;
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn Error>> {
        let database_url = "tcp://user:password@host:9000/db";
        let ddl = r"
            CREATE TABLE IF NOT EXISTS payment (
                customer_id  UInt32,
                amount       UInt32,
                account_name Nullable(FixedString(3))
            ) Engine=Memory";
    
        // Prepare data using Block
        let block = Block::new()
            .column("customer_id",  vec![1_u32,  3,  5,  7,  9])
            .column("amount",       vec![2_u32,  4,  6,  8, 10])
            .column("account_name", vec![Some("foo"), None, None, None, Some("bar")]);
    
        let pool = Pool::new(database_url);
    
        // Get a handle from the pool and perform operations
        let mut client = pool.get_handle().await?;
        client.execute(ddl).await?;
        client.insert("payment", block).await?;
        
        // Query data
        let block = client.query("SELECT * FROM payment").fetch_all().await?;
    
        // Iterate through rows and extract values
        for row in block.rows() {
            let id: u32             = row.get("customer_id")?;
            let amount: u32         = row.get("amount")?;
            let name: Option<&str>  = row.get("account_name")?;
            println!("Found payment {}: {} {:?}", id, amount, name);
        }
    
        Ok(())
    }
  9. Configure Pool size and hosts

    async-await

    The Pool configuration is derived from the Options object passed to Pool::new(). You can control the connection lifecycle using the following settings:

    • pool_min: The minimum number of idle connections to maintain in the pool (defaults to 5 if not specified).
    • pool_max: The maximum number of total connections allowed (defaults to 10 if not specified).
    • addr: The primary connection address.
    • alt_hosts: A list of alternative hosts for load balancing/failover. The pool uses a round-robin approach via get_addr() to distribute connections across these hosts.

    Note: Ensure you are using the binary protocol port (typically 9000). If you attempt to connect via port 8123 (the HTTP/text protocol), a warning will be issued as clickhouse-rs is designed for the binary protocol.

    use clickhouse_rs::{Pool, Options};
    use std::str::FromStr;
    
    // Example with custom pool limits and multiple hosts
    let options = Options::from_str("tcp://host1:9000?alt_hosts=host2:9000,host3:9000&pool_min=6&pool_max=12")
        .unwrap();
    let pool = Pool::new(options);
  10. Configure the ClickHouse connection via DNS URL

    async-await

    The client connects using a URL with the following schema:

    schema://user:password@host[:port]/database?param1=value1&...&paramN=valueN

    Available Parameters

    ParameterDescriptionDefault
    compressionCompression algorithm. Choices: none, lz4.none
    readonlyRestricts permissions. 0: All allowed, 1: Read only, 2: Read and change settings.0
    connection_timeoutTimeout for establishing a connection.500 ms
    keepaliveTCP keep alive timeout in milliseconds.N/A
    nodelayWhether to enable TCP_NODELAY.true
    pool_minLower bound of opened connections for Pool.10
    pool_maxUpper bound of opened connections for Pool.20
    ping_before_queryPing server before executing any query.true
    send_retriesNumber of retries to send a request.3
    retry_timeoutTime to wait before the next retry.5 sec
    ping_timeoutTimeout for the ping operation.500 ms
    alt_hostsComma-separated list of addresses for load-balancing.N/A

    Example URL: tcp://user:password@host:9000/clicks?compression=lz4&ping_timeout=42ms

    tcp://user:password@host:9000/clicks?compression=lz4&ping_timeout=42ms
  11. Supported ClickHouse data types

    async-await

    The clickhouse-rs client supports the following data types:

    • Numeric: UInt8, UInt16, UInt32, UInt64, UInt128, Int8, Int16, Int32, Int64, Int128, Float32, Float64, Decimal(P, S)
    • Strings: String, FixedString(N)
    • Date/Time: Date, DateTime
    • Specialized: IPv4, IPv6, UUID, Bool
    • Complex Types:
      • Nullable(T)
      • Array(UInt/Int/Float/String/Date/DateTime)
      • SimpleAggregateFunction(F, T)