# Rust SDK

[Read this page in HTML](https://www.scopedb.io/docs/guides/sdks/rust)

Query ScopeDB and write events from async Rust applications. This guide uses [scopedb-client 0.4.2](https://crates.io/crates/scopedb-client/0.4.2).

> **Note: Upgrading an existing application?**
>
> Review the [0.4.2 release notes and upgrade steps](https://www.scopedb.io/docs/release-notes/2026-10-27-rust-sdk-0-4-2.md#upgrade) before updating.

## Install and configure

Use Rust 1.91.0 or later. In your application's Cargo project, install the SDK, JSON support, and Tokio:

```bash
cargo add scopedb-client@0.4.2 serde_json
cargo add tokio --features macros,rt-multi-thread
```

The examples run inside Tokio's async runtime. Follow the [application connection guide](https://www.scopedb.io/docs/guides/connect-to-scopedb.md) to obtain an endpoint and API key, then set them in the shell where you run the examples:

```bash
export SCOPEDB_ENDPOINT="https://<endpoint>"
export SCOPEDB_API_KEY="<api-key>"
```

Keep the key on your server. SDKs do not reuse the CLI's interactive login.

## Run a query

Save this complete program as `src/main.rs` and run `cargo run`. It verifies the connection without creating a table or requiring sample data.

```rust
use std::error::Error;

use scopedb_client::Client;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
    let endpoint = std::env::var("SCOPEDB_ENDPOINT")?;
    let api_key = std::env::var("SCOPEDB_API_KEY")?;
    let client = Client::builder(endpoint)
        .api_key(api_key)
        .build()?;

    let result = client.query("SELECT 1 AS ready").await?;
    println!("ready: {}", result.into_objects()?[0]["ready"]);
    Ok(())
}
```

Expected output:

```text
ready: 1
```

`query()` waits for execution to finish; `into_objects()` returns rows keyed by column name. Reuse the client, and add the following examples inside `main` before `Ok(())`.

## Write rows

Create the `events` table used by the [quickstart](https://www.scopedb.io/docs/guides/quickstart.md#1-create-an-event-table). This statement leaves an existing table unchanged; if you already have `events`, check that its columns match before continuing.

```rust
client.query("
    CREATE TABLE IF NOT EXISTS events (
        time timestamp,
        service string,
        name string,
        message string,
        var object
    )
").await?;
```

Append one event and wait for it to commit:

```rust
let table = client.table("events");
let row = serde_json::json!({
    "time": "2026-10-23T09:00:00Z",
    "service": "checkout",
    "name": "payment_failed",
    "message": "Credit card gateway timed out",
    "var": {"region": "apac", "gateway": "credit_card"},
});
let stream = table.append_stream().build()?;
stream.send(&row).await?;
let report = stream.shutdown().await?;
println!("Written rows: {}", report.committed_rows);
```

`send()` accepts a row into the SDK's buffer. `shutdown()` sends remaining rows, waits for delivery, and closes the stream. With this default failure policy, a failed batch returns an error. A successful run prints `Written rows: 1`.

Read the event back after shutdown:

```rust
let written = client.query("
    FROM events
    WHERE time = '2026-10-23T09:00:00Z'::timestamp
      AND service = 'checkout'
      AND name = 'payment_failed'
    SELECT service, name, var['gateway']::string AS gateway
    LIMIT 10
").await?;
println!("{:?}", written.to_objects()?);
```

The result includes `checkout`, `payment_failed`, and `credit_card`. Each run appends another event. The examples use the default `scopedb` database and `public` schema; use `with_database(...)` or `with_schema(...)` on the table for another table location.

## Work with results

Use `to_objects()` for named columns or `to_values()` for positional rows. An empty result produces an empty vector. `first()` returns an optional row:

```rust
if let Some(first) = written.first()? {
    println!("{}", first["name"]);
}
```

Object conversion rejects duplicate column names; use unique aliases or positional results. The consuming `into_objects()` and `into_values()` methods move the result instead of borrowing it.

Cells use the SDK's [Value enum](https://docs.rs/scopedb-client/0.4.2/scopedb_client/enum.Value.html), including signed and unsigned 64-bit integers, Jiff timestamps and intervals, and `Value::Null`. Arrays, objects, and any values retain their textual representation; match the variant before parsing. `raw_rows()` exposes the original result text.

## Manage table definitions

Read the table's complete definition and ETag:

```rust
let catalog = table.catalog();
let snapshot = catalog.get().await?;
println!("{:?}", snapshot.definition.columns);
println!("{}", snapshot.etag);
```

To change its comment, preserve the definition and preview the replacement:

```rust
use scopedb_client::CatalogOptions;

let mut definition = snapshot.definition;
definition.comment = Some("Checkout events".to_string());
let preview = catalog.apply(&definition, CatalogOptions {
    dry_run: true,
    if_match: Some(snapshot.etag),
    ..Default::default()
}).await?;
println!("{:?}", preview);
```

Review `before`, `after`, and `destructive`. Apply the same definition with the preview's ETag:

```rust
let applied = catalog.apply(&definition, CatalogOptions {
    if_match: Some(preview.etag),
    ..Default::default()
}).await?;
if let Some(after) = applied.after {
    println!("{}", after.comment.unwrap_or_default());
}
```

`apply()` replaces the complete definition; omitted fields are cleared. Typed reads reject unknown fields; use `raw()` to retain them during exports or edits. An ETag conflict requires a fresh read and preview. Potentially destructive changes also require `allow_destructive: true`; this comment edit does not. See the [Catalog contract](https://www.scopedb.io/docs/api-reference/catalog.md) for resource selection and interrupted writes.

## Configure queries and writes

### Query execution

Use a statement builder to set execution limits or retain a handle for progress and cancellation:

```rust
use std::time::Duration;

let mut handle = client.statement("SELECT 1 AS ready")
    .with_max_elapsed(Duration::from_secs(60))
    .submit().await?;
println!("{:?}", handle.snapshot());
let query_result = handle.wait().await?;
println!("{:?}", query_result.into_objects()?);
```

`snapshot()` borrows cached state; `refresh().await` fetches an active statement's state. `client.statement_handle(id)` resumes tracking a known ID. `with_max_elapsed` includes queueing; `with_max_estimated` separately limits estimated duration.

Dropping or timing out a query future does not request server-side cancellation. Call `handle.cancel().await` and inspect its returned snapshot: execution may have finished already or may still be active. Confirmed cancellation makes `wait()` return `ErrorKind::StatementCancelled`.

### Batching and delivery

Use `flush()` to wait for buffered writes while keeping a stream open. Streams target 4 MiB batches and allow four concurrent requests by default. Each SDK request is capped at 8 MiB and 200,000 rows.

For a long-running producer that must continue after a failed batch:

```rust
use scopedb_client::AppendFailurePolicy;

let telemetry = table.append_stream()
    .failure_policy(AppendFailurePolicy::Continue)
    .max_concurrent_batches(1)
    .build()?;
if let Err(error) = telemetry.try_send(&row) {
    eprintln!("Not queued: {error}");
}

// Stop producers before shutting down the stream.
let delivery = telemetry.shutdown().await?;
println!("{:?}", delivery);
```

`try_send()` returns an error when a row cannot enter the local buffer. Continue mode reports failures without retaining failed payloads for replay; use durable storage when loss is unacceptable. Inspect `outcome`, `failed_rows`, and `unknown_rows` as well as committed rows. See [AppendStreamBuilder](https://docs.rs/scopedb-client/0.4.2/scopedb_client/struct.AppendStreamBuilder.html) for buffering, circuit breaking, and retry controls.

Streams retry confirmed temporary rejections by default. `retry_unknown(true)` explicitly allows replay of uncertain writes and can duplicate rows. `max_retries(0)` disables SDK retries; client timeouts are never replayed.

### Row encoding

Streams accept serializable structs or `serde_json::Value` objects through Serde. For existing NDJSON, use [Table::append](https://docs.rs/scopedb-client/0.4.2/scopedb_client/struct.Table.html#method.append), which makes one attempt. Shape values in the producer; Append does not run a ScopeQL transformation. See the shared [JSON input types](https://www.scopedb.io/docs/api-reference/append.md#json-input-types) for accepted field values and schema rules.

### Timeouts

Queries and Catalog reads have no SDK deadline by default. Bound local waiting in the application and use the statement's execution limit to bound server work.

Append attempts and Catalog mutations default to 30 seconds, including response-body reads. Configure `ClientBuilder::write_timeout` or a stream's `attempt_timeout` with `std::time::Duration`. Rust requires a finite, positive duration; this attempt deadline cannot be disabled. See [ClientBuilder](https://docs.rs/scopedb-client/0.4.2/scopedb_client/struct.ClientBuilder.html) when supplying an HTTP client with its own timeouts.

## Handle errors

Inspect the SDK error's stable code and diagnostics before returning it:

```rust
if let Err(error) = client.query("SELECT 1 AS ready").await {
    eprintln!("{:?} {:?} {:?}", error.code(), error.http_status(), error.request_id());
    eprintln!("{:?} {:?}", error.statement_details(), error.write_details());
    return Err(error.into());
}
```

For an Append failure, inspect `append_details()` and its `status` and row diagnostics. `write_details()` identifies a timeout after sending, with an unknown outcome. Reconcile an uncertain write before replaying it; `is_temporary()` alone does not establish that replay is safe.

Catalog writes never retry automatically. `catalog_commit_outcome_unknown` requires reading the current definition before another mutation. If a query fails after submission, use `error.statement_id()` with `client.statement_handle(id)` to resume tracking instead of resubmitting it. Follow the [HTTP error and retry contract](https://www.scopedb.io/docs/api-reference.md#errors-and-retries) for operation-specific decisions.

## Related contents

* [SDKs](https://www.scopedb.io/docs/guides/sdks.md)

* [Connect a client](https://www.scopedb.io/docs/guides/connect-to-scopedb.md)

* [API reference](https://www.scopedb.io/docs/api-reference.md)

* [Rust API reference](https://docs.rs/scopedb-client/0.4.2/scopedb_client/)
