clickhouse-driver

    一个ClickHouse的驱动库

    clickhouse
    driver
    database
    sql
    Download zip
    Author
    Version
    0.4.0
    License
    Apache-2.0
    Last updated
    19 days ago
    Downloads
    60

    #liuhuo23/clickhouse-driver

    A ClickHouse HTTP driver for MoonBit — simple, portable, and stateless.

    #Overview

    • Uses the native ClickHouse HTTP interface (default port 8123).
    • Responses parsed as TabSeparatedWithNamesAndTypes so column names and types come back automatically — no manual type mapping on the client side.
    • Single unified API: execute_query(sql, params?) covers SELECT, DDL, and inline-VALUES INSERT.
    • Named-parameter substitution via ClickHouse's {name: Type} placeholder syntax — values are passed as param_<key>=<value> URL parameters and substituted server-side (quoted and escaped automatically).
    • Proper error handling via MoonBit try-catch with a DbError suberror (ServerError / ConnectionError).
    • Simple per-request connections by default — no persistent state to leak; opt into keep-alive reuse with conn.pool(size) for high-QPS workloads.
    • Compatible with ClickHouse 22.x+ (the HTTP interface has been stable since 21.x).

    #Installation

    Add the dependency to your moon.mod (or run moon add liuhuo23/clickhouse-driver):

    import { "liuhuo23/clickhouse-driver@0.4.0", }

    Then import it in your package's moon.pkg:

    import { "liuhuo23/clickhouse-driver" @lib, }

    #Quick start

    ///|
    async fn main {
    let conn = @lib.connect(
    host="127.0.0.1",
    port=8123,
    user="default",
    password="",
    database="default",
    client_name="my-app",
    )
    defer conn.close()

    // 1. Health check
    conn.ping()

    // 2. SELECT — columns come back automatically
    let result = conn.execute_query(
    "SELECT id, name FROM users WHERE created_at > {lo: DateTime} LIMIT {n: UInt32}",
    params=Map::from_array([("lo", "2024-01-01 00:00:00"), ("n", "10")]),
    )

    // 3. Inspect schema
    for col in result.columns {
    println(col.name + " : " + col.type_)
    }

    // 4. Iterate rows (each cell is a string)
    for row in result.rows {
    println(row.values)
    }

    // 5. Or convert to row maps keyed by column name
    for m in result.to_map() {
    println(m["name"])
    }

    // 6. INSERT via inline VALUES — all values come from params
    ignore(
    conn.execute_query(
    "INSERT INTO users (id, name) VALUES " +
    "({id: UInt32}, {name: String}), ({id2: UInt32}, {name2: String})",
    params=Map::from_array([
    ("id", "1"),
    ("name", "alice"),
    ("id2", "2"),
    ("name2", "bob"),
    ]),
    ),
    )
    }

    #Native protocol (TCP) driver

    In addition to the HTTP interface, this project ships an experimental ClickHouse native TCP protocol driver (port 9000) in the native/ sub-package. It speaks the same binary protocol as clickhouse-client:

    • Persistent connection: one handshake, then many queries over the same TCP socket (a natural fit for keep-alive reuse and connection pooling).
    • Binary transport: no text round-trip; values are decoded from their native column encodings, so types stay exact.
    • Aligned API: reuses the root package's DbError / ResultSet / Column / Row types, so migrating call sites is trivial.

    Current capabilities: connect + handshake, per-operation timeout_ms, ping, execute_query / execute (named {name: Type} and positional ? parameter binding), execute_stream (lazy block-by-block cursor), insert (batch Data-block upload) and cancel, with rich type support: UInt8–64, Int8–64, Float32/64, String, FixedString, Date, Date32, DateTime, DateTime64, Enum8/16, Nullable, Array (nested), Tuple, Map, Decimal32/64/128/256, UUID, IPv4, IPv6 (and LowCardinality via server-side unwrapping), LZ4 compression (Data blocks, compress=true), a connection pool (acquire/release, config.pool(size)), server exception mapping, and graceful close. TLS is planned for later stages.

    #Quick start

    ///|
    async fn main {
    let conn = @native.connect(
    @native.native_config(
    host="127.0.0.1",
    port=9000,
    user="default",
    password="",
    database="default",
    client_name="my-app",
    ),
    )
    defer conn.close()

    conn.ping()

    let rs = conn.execute_query("SELECT 1 AS one")
    println(rs.rows[0].values[0]) // 1
    }

    To use it, import the sub-package in your moon.pkg:

    import { "liuhuo23/clickhouse-driver/native" @native, }

    #Differences from the HTTP driver

    • @native.connect(config) actually connects (handshake included); the HTTP connect is config-only.
    • One connection executes queries serially — do not run concurrent reads on the same NativeConnection.
    • Errors use the same DbError: auth / syntax failures raise DbError::ServerError(code, name, message); I/O and protocol problems raise DbError::ConnectionError.

    #Demo

    CLICKHOUSE_NATIVE=1 CLICKHOUSE_NATIVE_PORT=9000 moon run cmd/main

    See docs/native-protocol-design.md for the full design and the staged implementation plan (P0–P4).

    #API reference

    #connect

    pub fn connect(
    host~ : String,
    port~ : Int,
    user~ : String,
    password~ : String,
    database~ : String,
    client_name~ : String,
    timeout_ms? : Int = 0,
    https? : Bool = false,
    skip_verify? : Bool = false,
    compress? : Bool = false,
    ) -> Connection

    Builds a Connection config from explicit parameters. Does not open a TCP connection — by default calls are stateless and open fresh HTTP connections per request. All new options are optional — existing call sites keep working. Wrap it with conn.pool(size) when you want to reuse connections (see Connection pooling).

    ParameterTypeDescription
    hostStringServer hostname or IP
    portIntHTTP port (default 8123)
    userStringUsername
    passwordStringPassword
    databaseStringDefault database
    client_nameStringSent via X-ClickHouse-Client-Name header
    timeout_msIntPer-request timeout in ms; 0 = no timeout
    httpsBoolUse TLS (https://); default false
    skip_verifyBoolSkip TLS cert verification; default false
    compressBoolRequest ClickHouse LZ4 compression; default false

    #Connection

    ///|
    pub struct Connection {
    host : String
    port : Int32
    user : String
    password : String
    database : String
    client_name : String
    timeout_ms : Int32
    https : Bool
    skip_verify : Bool
    compress : Bool
    }

    A lightweight config struct — no persistent socket. Every call opens a short-lived HTTP connection and closes it on return. For connection reuse across requests, see Connection pooling.

    #Connection::ping

    pub async fn ping(self : Connection) -> Unit raise

    Sends SELECT 1 and expects 200 OK. Lightweight health check.

    #Connection::execute_query

    pub async fn execute_query(
    self : Connection,
    sql : String,
    params? : Map[String, String] = {},
    ) -> ResultSet raise

    Executes any SQL statement (SELECT, DDL, inline-VALUES INSERT, ...) and returns the parsed result.

    params is an optional map of named parameters. Each entry is sent as a param_<key>=<value> URL parameter, and ClickHouse substitutes the value into matching {key: Type} placeholders server-side. Values are automatically quoted and escaped — pass them as raw strings.

    Examples:

    // No params
    let r = conn.execute_query("SELECT version()")

    // Single param
    let r = conn.execute_query(
    "SELECT * FROM events WHERE id = {id: UInt64}",
    params=Map::from_array([("id", "42")]),
    )

    // Multiple params, used in INSERT VALUES
    ignore(conn.execute_query(
    "INSERT INTO events (id, ts, msg) VALUES " +
    "({id: UInt64}, {ts: DateTime}, {msg: String})",
    params=Map::from_array([
    ("id", "1"),
    ("ts", "2024-01-01 00:00:00"),
    ("msg", "hello"),
    ]),
    ))

    Raises:
    • DbError::ServerError(code, name, message) — the server returned a non-2xx HTTP response (syntax error, unknown table, permission denied, …).
    • DbError::ConnectionError(String) — network / I/O error.

    #Connection::insert

    pub async fn insert(
    self : Connection,
    table : String,
    columns : Array[String],
    rows : Array[Array[String]],
    format? : InsertFormat = Tsv,
    ) -> Unit raise

    Streams a batch insert as the HTTP request body (instead of inlining VALUES literals). The body is streamed in chunks, so large inserts don't blow up the SQL text size. format selects Tsv (default) or Ndjson (FORMAT JSONEachRow); values are sent as text and coerced by ClickHouse.

    conn.insert("users", ["id", "name"], [["1", "alice"], ["2", "bob"]])

    #Connection::execute_stream

    pub async fn execute_stream(
    self : Connection,
    sql : String,
    params? : Map[String, String] = {},
    ) -> ResultSetCursor raise

    Executes a query and returns a streaming cursor over the result rows — the response body is consumed lazily, one row at a time. columns() gives the schema immediately; next() returns rows until None; always close() to release the connection. Not supported together with compress=true.

    let cur = conn.execute_stream("SELECT * FROM events")
    defer cur.close()
    while true {
    match cur.next() {
    None => break
    Some(row) => println(row.values)
    }
    }

    #Connection pooling

    Connection opens a fresh TCP/TLS connection for every request. For high-QPS workloads or remote/TLS servers, repeated handshakes add up — Connection::pool reuses keep-alive connections:

    let pool = conn.pool(size=4) // up to 4 concurrent connections
    defer pool.close()

    pool.ping() // same API as Connection
    let r = pool.execute_query("SELECT count() FROM events")
    pool.insert("events", ["id", "ts"], [["1", "2024-01-01 00:00:00"]])
    let cur = pool.execute_stream("SELECT * FROM events") // cursor returns the
    defer cur.close() // connection on close

    • size bounds concurrent connections; extra requests wait for a free one.
    • Connections that fail are discarded and replaced, never reused.
    • Idle connections are reused immediately; ones idle for more than 5s are validated with GET /ping before reuse, so a keep-alive socket closed by the server does not surface as a request failure.
    • pool.execute_stream returns its connection when the stream is fully drained; an abandoned stream drops the connection instead.
    • pool.close() closes idle connections; in-flight ones are closed on return.

    ConnectionPool mirrors the Connection API:

    pub async fn ConnectionPool::ping(Self) -> Unit
    pub async fn ConnectionPool::execute_query(Self, String, params? : Map[String, String]) -> ResultSet
    pub async fn ConnectionPool::execute(Self, String, Array[String]) -> ResultSet
    pub async fn ConnectionPool::insert(Self, String, Array[String], Array[Array[String]], format? : InsertFormat) -> Unit
    pub async fn ConnectionPool::execute_stream(Self, String, params? : Map[String, String]) -> ResultSetCursor
    pub fn ConnectionPool::max_size(Self) -> Int
    pub fn ConnectionPool::close(Self) -> Unit

    #Connection::cancel

    pub async fn cancel(self : Connection) -> Unit

    No-op over HTTP. Each query is a single short-lived request, so there is no persistent connection in which to send a cancel signal. Kept in the API for symmetry with the previous native-TCP design.

    #Connection::close

    pub fn close(self : Connection) -> Unit

    No-op over HTTP. Use with defer for symmetry:

    let conn = @lib.connect(...)
    defer conn.close()

    #ResultSet

    ///|
    pub struct ResultSet {
    columns : Array[Column]
    rows : Array[Row]
    }

    columns is populated when the response uses TabSeparatedWithNamesAndTypes (which is what execute_query requests by default). For DDL / INSERT statements the array is empty.

    #ResultSet::to_map

    pub fn to_map(self : ResultSet) -> Array[Map[String, String]]

    Converts the result to an array of per-row maps. Each element is a Map[String, String] where keys are column names and values are the string representation of the cell. Empty if columns is empty.

    for m in result.to_map() {
    let name = m.get_or_default("name", "")
    println(name)
    }

    #Row

    ///|
    pub struct Row {
    values : Array[String]
    }

    A single row. Each cell is a string representation of the underlying ClickHouse value (e.g. "42", "2025-01-01 00:00:00", "NULL").

    #Column

    ///|
    pub struct Column {
    name : String
    type_ : String
    }

    Column metadata parsed from TabSeparatedWithNamesAndTypes. type_ is the raw ClickHouse type string, e.g. "UInt32", "String", "Nullable(Int64)".

    #Error handling

    try {
    conn.execute_query("SELECT * FROM no_such_table")
    } catch {
    @lib.DbError::ServerError(code~, name=_, message~) =>
    println("server error: code=" + code.to_string() + " " + message)
    @lib.DbError::ConnectionError(msg) =>
    println("connection error: " + msg)
    _ => println("other error")
    }

    #DbError suberror

    ///|
    pub suberror DbError {
    ServerError(code~ : Int32, name~ : String, message~ : String)
    ConnectionError(String)
    } derive(Show)

    VariantFieldsWhen
    ServerErrorcode : Int, name : String, message : StringServer returned a non-2xx HTTP response with an error body.
    ConnectionErrorStringNetwork / I/O error (connection refused, malformed response, …).

    code is the HTTP status code (typically 400 for client errors like syntax / unknown table, 500 for server errors). name is "HTTPError". message is the first 500 chars of the response body (which contains the ClickHouse exception text).

    #How it works

    The driver issues one HTTP request per call:

    POST /?database=<db>&default_format=TabSeparatedWithNamesAndTypes &query=<url-encoded SQL> [&param_<key>=<url-encoded value>...] HTTP/1.1 Host: <host>:<port> Authorization: Basic <base64(user:password)> X-ClickHouse-Client-Name: <client_name> Content-Length: 0

    No Connection: close is sent, so connections can be kept alive: the plain Connection closes the socket after each call, while ConnectionPool reuses the same socket for subsequent requests.

    ClickHouse replies with TabSeparatedWithNamesAndTypes:

    <col1>\t<col2>\t<col3> <Type1>\t<Type2>\t<Type3> <val1>\t<val2>\t<val3> <val4>\t<val5>\t<val6> ...

    The driver parses this into ResultSet { columns, rows }.

    Why POST? ClickHouse's HTTP interface treats GET requests as readonly (For queries over HTTP, method GET implies readonly). POST works for every query type — SELECT, DDL, INSERT — so we use a single method.

    Why URL params for named parameters? ClickHouse substitutes {key: Type} placeholders in SQL with param_<key>=<value> URL parameters. The server takes care of quoting and type coercion, so the driver can pass values as raw strings without worrying about escaping.

    #ClickHouse transactions

    ClickHouse has no traditional ACID transactions. There is no BEGIN / COMMIT / ROLLBACK and no Serializable isolation. INSERT … SELECT is atomic at the part level. For data with versioning semantics, use one of the special engines:

    • ReplacingMergeTree(version_column) — keeps the row with the largest version_column after merge.
    • CollapsingMergeTree(sign_column) — uses a sign column (+1 insert, -1 cancel) to collapse pairs of rows on merge.
    • VersionedCollapsingMergeTree(version, sign) — like CollapsingMergeTree but order-independent.
    • SummingMergeTree / AggregatingMergeTree — for state-aggregation patterns.

    #Limitations

    1. No mid-query cancel — once a request is sent, the driver has no handle to cancel it. Drop the connection if you must abort.
    2. Streaming reads are cursor-only — execute_stream() streams rows lazily, but execute_query() still buffers the full result (capped at 256 MB) because the driver needs the complete body for LZ4 decompression and text parsing.
    3. Compression + streaming don't mix yet — execute_stream() raises ConnectionError when compress=true; use execute_query() for compressed responses.
    4. Response body size limit — buffered responses are capped at 256 MB to avoid runaway memory. Queries returning more should use filters, aggregation, or execute_stream().
    5. No insert progress / retries — insert() streams the body in one shot; on failure the whole request must be retried.

    #Run the example CLI

    # Default connection (127.0.0.1:8123, user `default`) moon run cmd/main # Point it at any ClickHouse via environment variables CLICKHOUSE_HOST=127.0.0.1 CLICKHOUSE_PORT=38123 \ CLICKHOUSE_USER=default CLICKHOUSE_PASSWORD=barn \ moon run cmd/main

    The demo (cmd/main/main.mbt) walks through every major API:

    • ping() health check and close() / cancel() semantics
    • execute_query() — SELECT / DDL with named {name: Type} params
    • execute() — positional ? binding (inline VALUES)
    • insert() — streamed bulk insert as the HTTP body, TSV and JSONEachRow formats
    • execute_stream() — row-by-row cursor with immediate schema access
    • to_map() — result rows as column-name maps
    • Error handling — ServerError (missing table, syntax error) and ConnectionError caught with try-catch
    • compress=true — LZ4-compressed responses decoded automatically
    • timeout_ms — a slow query (SELECT sleep(3)) is cancelled after the configured timeout
    • https=true — TLS connection (fails gracefully against a plain-HTTP server, succeeds against a TLS-enabled port)
    • conn.pool(size) — pooled keep-alive connections with concurrent queries

    DbError

    pub suberror DbError {
    ServerError(code~ : Int, name~ : String, message~ : String)
    ConnectionError(String)
    } derive(
    Debug
    )

    Errors raised by the driver.

    DbError::connection_error

    fn DbError::connection_error(message : String) -> DbError

    构造连接错误(供 native 等子包复用)。

    DbError::server_error

    fn DbError::server_error(code~ : Int, name~ : String, message~ : String) -> DbError

    构造服务器错误(供 native 等子包复用)。

    DbError::to_repr

    Re-export the derived Debug method explicitly (MoonBit no longer promotes trait impl methods to inherent methods implicitly).

    Column

    pub struct Column {
    name : String
    type_ : String
    }

    Column metadata returned from a query.

    Column::new

    fn Column::new(name~ : String, type_~ : String) -> Column

    构造列元数据(供 native 等子包复用)。

    Connection

    pub struct Connection {
    host : String
    port : Int
    user : String
    password : String
    database : String
    client_name : String
    timeout_ms : Int
    https : Bool
    skip_verify : Bool
    compress : Bool
    }

    Connection config for ClickHouse over HTTP. Each query opens its own short-lived HTTP connection.

    Connection::cancel

    fn Connection::cancel(self : Connection) -> Unit

    No-op for HTTP: queries can't be cancelled mid-flight without persistent TCP.

    Connection::close

    fn Connection::close(self : Connection) -> Unit

    No-op for HTTP: each call opens a fresh short-lived connection.

    Connection::execute

    async fn Connection::execute(self : Connection, sql : String, values : Array[String]) -> ResultSet

    Execute SQL with positional ? placeholders. Each ? in the SQL is bound to the next value in values, in order. This is the concise form for inline-VALUES INSERTs and other queries that repeat the same parameter shape across rows — no need to invent unique names per row and no Map::from_array(...) ceremony.

    All values are bound as String; ClickHouse coerces them to the target column type on the server side. This works for numbers, dates, and most common scalar types. For non-String columns where coercion is not enough (e.g. exotic parameterized types), fall back to execute_query with explicit {name: Type} named binding.

    Like apply_default_types, this is a byte scan — it does not parse SQL. If a literal ? appears inside a string, restructure the query to avoid the literal (ClickHouse's HTTP syntax has no escape form for ?).

    Example: execute("INSERT INTO t (a, b) VALUES (?, ?), (?, ?)", ["1", "alice", "2", "bob"])

    Connection::execute_query

    async fn Connection::execute_query(self : Connection, sql : String, params? : Map[String, String]) -> ResultSet

    Execute any SQL statement (SELECT, DDL, INSERT, ...) and return the parsed TabSeparatedWithNamesAndTypes result. For non-SELECT statements the returned rows is typically empty.

    params is an optional map of named parameters, substituted into the SQL via ClickHouse's {name: Type} placeholder syntax. Each key/value is passed as a param_<key>=<value> URL parameter; the server inserts the value (quoted and escaped) into matching placeholders.

    For the common case where a parameter is a string (table name, identifier, or string column value), the type can be omitted and is defaulted to String automatically — so {tn} and {tn: String} are equivalent. Use the explicit {name: Type} form when binding into a non-String column.

    Example: execute_query("SELECT * FROM events WHERE ts > {lo}", params=Map::from_array([("lo", "2024-01-01 00:00:00")]))

    Connection::execute_stream

    async fn Connection::execute_stream(self : Connection, sql : String, params? : Map[String, String]) -> ResultSetCursor

    Execute a query and return a streaming cursor over the result rows.

    Unlike execute_query (which buffers the whole result in memory), the response body is read lazily row-by-row via next(). This is the right tool for large SELECTs:

    let cur = conn.execute_stream("SELECT * FROM events") defer cur.close() while let Some(row) = cur.next() { println(row.values) }

    The cursor must be closed with close() (also via defer) to release the underlying HTTP connection. Named params work exactly like execute_query. Compressed responses (compress=true) are not supported for streaming yet and raise ConnectionError.

    Connection::insert

    async fn Connection::insert(self : Connection, table : String, columns : Array[String], rows : Array[Array[String]], format? : InsertFormat) -> Unit

    Stream a batch insert to the server over the HTTP body (POST), instead of inlining rows as VALUES literals in the SQL text.

    table and columns are interpolated verbatim into the SQL (identifiers — quote them yourself if needed). Each element of rows is one row whose fields correspond positionally to columns. All values are sent as text; ClickHouse coerces them to the target column types.

    format selects the wire format (Tsv by default, Ndjson for JSONEachRow). The body is streamed to the server in chunks, so large inserts do not blow up the SQL text size. Raises ServerError on a non-2xx response and ConnectionError on transport failures / timeouts.

    Example: conn.insert("users", ["id", "name"], [["1", "alice"], ["2", "bob"]])

    Connection::ping

    async fn Connection::ping(self : Connection) -> Unit

    Round-trip health check: sends SELECT 1 and expects 200 OK.

    Connection::pool

    fn Connection::pool(self : Connection, size? : Int) -> ConnectionPool

    Build a ConnectionPool from this connection config. size is the maximum number of concurrent connections (default 4; values <= 0 are clamped to 4).

    ConnectionPool

    pub struct ConnectionPool {
    // private fields
    }

    A pool of reusable HTTP connections.

    Connection opens a fresh TCP/TLS connection for every request; a pool keeps up to max_size keep-alive connections and reuses them across requests, avoiding repeated handshakes (especially valuable for TLS and remote servers). All query methods mirror Connection.

    The async runtime is single-threaded and cooperative, so the idle list is only touched inside short synchronous sections — no locking is needed. Requests beyond max_size wait for a free connection; connections that fail are discarded and replaced, never handed back to the pool.

    ConnectionPool::cancel

    fn ConnectionPool::cancel(self : ConnectionPool) -> Unit

    No-op: pool requests are independent; kept for API symmetry with Connection.

    ConnectionPool::close

    fn ConnectionPool::close(self : ConnectionPool) -> Unit

    Close the pool: mark it closed and close all idle connections. Connections still checked out are closed when they are returned.

    ConnectionPool::execute

    async fn ConnectionPool::execute(self : ConnectionPool, sql : String, values : Array[String]) -> ResultSet

    Same as Connection::execute (positional ? binding), pooled.

    ConnectionPool::execute_query

    async fn ConnectionPool::execute_query(self : ConnectionPool, sql : String, params? : Map[String, String]) -> ResultSet

    Same as Connection::execute_query (named {name: Type} params), but reuses pooled connections.

    ConnectionPool::execute_stream

    async fn ConnectionPool::execute_stream(self : ConnectionPool, sql : String, params? : Map[String, String]) -> ResultSetCursor

    Same as Connection::execute_stream (row-by-row cursor), pooled. The cursor returns its connection to the pool on close() when the stream was fully read, and drops it otherwise. compress=true is not supported for streaming.

    ConnectionPool::insert

    async fn ConnectionPool::insert(self : ConnectionPool, table : String, columns : Array[String], rows : Array[Array[String]], format? : InsertFormat) -> Unit

    Same as Connection::insert (streamed TSV/JSONEachRow body), pooled.

    ConnectionPool::max_size

    fn ConnectionPool::max_size(self : ConnectionPool) -> Int

    The maximum number of concurrent connections this pool will create.

    ConnectionPool::ping

    async fn ConnectionPool::ping(self : ConnectionPool) -> Unit

    Round-trip health check through the pool.

    InsertFormat

    pub(all) enum InsertFormat {
    Tsv
    Ndjson
    }

    Wire format used by Connection::insert for the request body.

    ResultSet

    pub struct ResultSet {
    columns : Array[Column]
    rows : Array[Row]
    }

    Complete result set containing column metadata and rows.

    ResultSet::new

    fn ResultSet::new(columns~ : Array[Column], rows~ : Array[Row]) -> ResultSet

    构造完整结果集(供 native 等子包复用)。

    ResultSet::to_map

    fn ResultSet::to_map(self : ResultSet) -> Array[Map[String, String]]

    Convert result to an array of row maps. Each element is a Map[String, String] mapping column name → value for that row.

    ResultSetCursor

    pub struct ResultSetCursor {
    // private fields
    }

    Streaming cursor returned by Connection::execute_stream.

    The HTTP response body is consumed lazily, one row at a time, so queries with large result sets do not need to be buffered in memory. Always call close() when done to release the underlying connection.

    columns() is available immediately after the cursor is created; rows are read with repeated next() calls (returns None at end of stream).

    ResultSetCursor::close

    fn ResultSetCursor::close(self : ResultSetCursor) -> Unit

    Close the streaming cursor and release the underlying connection.

    For cursors created through a ConnectionPool, a fully-drained stream returns the connection to the pool for reuse; a partially-read stream drops the connection (the server may still be streaming data). Idempotent — safe to call multiple times.

    ResultSetCursor::columns

    The columns (name + type) of the streamed query result.

    ResultSetCursor::next

    async fn ResultSetCursor::next(self : ResultSetCursor) -> Row?

    Read the next row from the stream, or None at end of stream.

    If the connection was configured with a positive timeout_ms, each next() call is bounded by that timeout; a timeout raises ConnectionError.

    Row

    pub struct Row {
    values : Array[String]
    }

    A single row of query results. Each value is a string representation.

    Row::new

    fn Row::new(values~ : Array[String]) -> Row

    构造单行结果(供 native 等子包复用)。

    connect

    fn connect(host~ : String, port~ : Int, user~ : String, password~ : String, database~ : String, client_name~ : String, timeout_ms? : Int, https? : Bool, skip_verify? : Bool, compress? : Bool) -> Connection

    Build a Connection config from explicit connection params. Does NOT open a TCP connection — calls are stateless.

    Optional params (all backward compatible): timeout_ms — request timeout in ms; 0 = no timeout (default) https — use TLS (https://); default false skip_verify— skip TLS certificate verification; default false compress — request ClickHouse LZ4 response compression; default false