ScopeDB SDK for Rust
scopedb-client is an async Rust client for ScopeDB. It supports statements, read-only REST catalog discovery, direct NDJSON table appends, bounded concurrent streaming writes, and transform-oriented JSON ingest.
Installation
scopedb-client requires Rust 1.91.0 or later.
Create a client
Use the builder for the common API-key path. An API key is a server credential: keep it in a trusted process and never compile or return it to an untrusted client.
use Client;
let client = builder
.api_key
.build?;
# Ok::
Applications that own TLS, proxy, timeout, or pooling settings can pass a compatible HTTP client through .http_client(...). Client::new(endpoint, http_client) remains available when authentication is already configured on that client. Use the reqwest version re-exported as scopedb_client::reqwest to avoid dependency-version mismatches.
Statement and transform-ingest JSON request bodies and streaming table append batches use zstd compression by default. The default HTTP client also negotiates compressed responses. Direct caller-encoded table appends remain identity-encoded.
The runnable examples read authentication from SCOPEDB_API_KEY. For backward compatibility, they fall back to SCOPEDB_TOKEN when the API key variable is unset or empty. The builder marks the resulting authorization header as sensitive so standard header and request Debug formatting redacts the credential.
ScopeQL
This SDK sends ScopeQL statements to ScopeDB; the language is documented separately. Use these canonical entry points:
Run a statement
# async
raw_rows() exposes the string-or-null wire cells without parsing. to_values() borrows the result while into_values() consumes it and moves owned string cells. to_objects() and into_objects() key values by output column name; they return an error when names are duplicated, so use the value form for intentionally duplicated columns. first() converts only the first row for point lookups and aggregates.
For lifecycle control, call client.statement(scopeql).submit() and retain the returned handle. last_status() reads the latest cached snapshot without a network request, status().await fetches one current status, wait() polls to a terminal result, and cancel() requests cancellation. The statement ID and initial status snapshot are available immediately after submission.
Browse the catalog
Catalog iterators follow opaque continuation tokens automatically and fetch the next page only when needed. List methods remain available when the application needs explicit page boundaries; fetch methods return one full resource.
use CatalogListOptions;
# async
See examples/catalog.rs for complete database pagination and table metadata discovery.
Streaming writes with NDJSON
The table write API accepts newline-delimited JSON only: each line is one JSON row object. It does not accept a JSON array. The destination table must already exist. Table uses scopedb and public when the database or schema is not specified.
Direct append
Use direct append when the caller already owns one exact NDJSON request boundary.
use Client;
# async
A failed append exposes structured details through Error::append_details():
use AppendState;
use ErrorKind;
#
Unknown is deliberately different from a rejection: replaying the same rows may insert duplicates.
All HTTP errors preserve the server message in Error::message() and expose operational metadata without message parsing:
#
The asynchronous append stream honors Retry-After only for an exact temporary batch explicitly reported as Rejected; the delay is capped by max_backoff. Unknown outcomes remain non-retryable because replay can duplicate rows.
Asynchronous append stream
Use append_stream() for continuous or large producers. The stream serializes records to NDJSON, batches by size or time, bounds pending bytes, and sends a bounded number of HTTP append requests concurrently.
use Duration;
# async
Each AppendStream request contains at most 8 MiB of uncompressed NDJSON and 200,000 rows. The stream splits automatically at either limit. Direct caller-encoded appends retain the endpoint's 16 MiB limit.
send() and send_all() wait for local admission capacity only; they do not wait for a remote commit. Feed an iterator sequentially instead of spawning one task per row, which would move the unbounded backlog outside the stream. flush() and shutdown() are remote delivery barriers.
Once a flush() future has enqueued its barrier, dropping that future does not cancel remote settlement. Keep it alive to receive the interval report; if a task is cancelled, inspect stats().last_report and lifetime counters before deciding how to reconcile the covered rows.
The default AppendFailurePolicy::Stop is strict: the first failed batch stops admission, and a successful barrier confirms that its accepted prefix committed. Use max_concurrent_batches(1) if request submission order matters; concurrent batches have no defined commit order.
Best-effort telemetry and logs
Telemetry producers often cannot wait for admission or stop permanently after one unavailable batch. Opt into Continue, use try_send() on the hot path, and inspect settlement reports and lifetime stats.
use Duration;
use AppendDeliveryOutcome;
use AppendFailurePolicy;
# async
Ok(()) from try_send() still means local admission, not remote commit. It returns immediately with an error when serialization fails, the row is invalid or too large, the buffer is full, the circuit is open, or the stream is closed. Continue mode releases failed batches after accounting for them; it is not an in-memory retry queue.
The circuit breaker rejects non-blocking try_send() calls while open. The backpressured send() path can still admit rows within the configured memory budget; their dispatch waits for the circuit probe, so use try_send() on latency-sensitive logging and telemetry paths.
A continue-mode report classifies rows as committed, failed, unknown, or locally dropped. For accepted rows in a completed report:
accepted_rows = committed_rows + failed_rows + unknown_rows
AppendDeliveryOutcome::Partial means at least one row committed while another row failed, was dropped, or remains unknown. With no committed rows, the outcome is Unknown when any batch may have committed and Failed otherwise. Only a loss-free report is Ok.
The stream retries only the exact temporary HTTP batch explicitly reported as Rejected. A timeout, transport failure, or invalid success response is Unknown and is never automatically retried. An in-memory stream is not a durable queue; use an outbox when payloads must survive process failure or be available for reconciliation.
Choose a delivery path
| Workload | Admission and delivery | Example |
|---|---|---|
| One exact NDJSON payload | Caller owns the request boundary | append.rs |
| Basic asynchronous batching | SDK owns batches; strict barriers | append_stream.rs |
| Backfill or file import | Bounded producer memory and concurrent strict batches | bulk_append.rs |
| Long-running logs and events | Non-blocking continue mode with observable loss | telemetry.rs |
| SQL transformation before insert | Transform-oriented ingest stream | ingest_transform.rs |
Table helper
# async
Transform-oriented ingest
IngestStream remains useful when input JSON needs a SQL transform before insertion. Prefer Table::append or Table::append_stream when records already match the destination table.
# async
Examples
The example guide includes setup, safety guards, delivery contracts, and runnable commands from the repository workspace root.
The wire-level endpoint and payload reference is in docs/rust-http-api.md. Release history and the maintainer runbook are in CHANGELOG.md and RELEASE.md.