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 and development
The example guide includes setup, safety guards, delivery contracts, and runnable commands.
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.