ScopeDB SDK for Rust
scopedb-client is an async Rust client for ScopeDB. It supports statements, declarative catalog reads and conditional updates, direct NDJSON table appends, and bounded concurrent streaming writes.
SDK 0.4 requires ScopeDB 0.6.0 or later and uses its native Statement, declarative Catalog, and table ingest APIs. See the migration guide when upgrading from SDK 0.3.
Installation
Requires Rust 1.91.0 or later. From your application crate:
Quickstart
Set SCOPEDB_ENDPOINT and SCOPEDB_API_KEY using the connection guide. Save this program as src/main.rs and run cargo run:
use Client;
async
Documentation
- SDK guide
- API reference
- Runnable examples are documented in
examples/README.md. - Release history is recorded in the repository's
CHANGELOG.md. - Report an issue
This SDK sends ScopeQL statements to ScopeDB; the language is documented separately. Use these canonical entry points:
Client configuration
Applications that own TLS, proxy, timeout, or pooling settings can pass a compatible HTTP client through .http_client(...). The SDK-owned HTTP client disables redirects and transport retries to avoid replaying writes; custom clients must use .redirect(reqwest::redirect::Policy::none()).retry(reqwest::retry::never()) for the same guarantee. 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.
Every table append or Catalog mutation HTTP request has a client timeout of 30 seconds by default. Its budget starts after local encoding, compression, and request preparation and covers one HTTP attempt through reading the complete response body. Configure it with Client::builder(endpoint).write_timeout(Duration::from_secs(60)) before build(). Client::new uses the same default. Streams inherit the client timeout unless an append stream explicitly sets attempt_timeout. Both settings require a finite, positive duration; the Rust SDK has no option to disable this deadline. Increase the duration when a write needs more time. Shorter timeouts on a custom HTTP client still apply. Admission, retry backoff, and an entire flush()/shutdown() barrier are outside this budget.
Append client timeouts after an HTTP attempt begins preserve the existing error kind and source and add Error::write_details() with reason: WriteErrorReason::Timeout and outcome: AppendState::Unknown; is_retryable() is false. The structured details, source, and HTTP diagnostics remain available on subsequent errors returned by the stream and batch failure events. A timeout does not roll back a write: reconcile a client-interrupted write before replaying it. A server HTTP 408 is still a server response; an explicitly rejected append follows the existing safe retry policy. Streams retry temporary rejected batches by default. Unknown outcomes require explicit .retry_unknown(true); this permits at-least-once delivery and can duplicate rows. Catalog interruptions use catalog_commit_outcome_unknown and are never replayed automatically.
use ;
#
Statement JSON request bodies and all table append requests use zstd compression by default. The default HTTP client also negotiates compressed responses. Append limits are based on the uncompressed NDJSON body.
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.
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.
Typed values support PartialEq, so applications can compare cells and result rows directly. Array, object, any, and binary values retain their encoded strings and compare those strings; floating-point values follow Rust's usual equality rules.
For lifecycle control, call client.statement(scopeql).submit() and retain the returned handle. snapshot() reads the latest cached status without a network request, refresh().await fetches one current status, wait() polls to a terminal result, and cancel() requests cancellation. The statement ID and initial snapshot are available immediately after submission. cancel() returns the observed status: a pending or running response still needs wait(); completion retains results and failure retains the original error. Cancellation is a distinct ErrorKind::StatementCancelled when waiting. Terminal snapshots are cached. Dropping a query, refresh, or wait future stops local waiting and does not cancel server execution; keep a handle and call cancel() explicitly.
If query() or execute() fails after submission, Error::statement_id() identifies the existing execution. Resume it with client.statement_handle(id).wait(); the error does not advertise retrying the entire query as safe. Temporary errors from a handle's refresh() or wait() can still be retried on that handle. A lost submission response, HTTP 408, or server/gateway 5xx leaves submission uncertain and is not classified as retryable. Use with_statement_id(id) before submission when you need to look up that execution after a lost response; a known ID alone does not confirm acceptance.
Browse the catalog
Catalog reads return complete declarative definitions and an opaque ETag. Select the smallest subtree you need. Apply replaces that entire subtree, including its children; omitted fields are cleared. Preview with dry_run, apply with if_match to reject concurrent changes, and opt into allow_destructive only after reviewing data loss. Mutations never retry automatically. An interrupted or malformed mutation response has code catalog_commit_outcome_unknown; read the current state before deciding whether to retry. Typed definitions reject unknown fields, including nested fields, to prevent silent loss during replacement. Use raw() to read and apply newer definitions as serde_json::Value; arbitrary-precision JSON numbers are retained.
use CatalogOptions;
# async
See examples/catalog.rs for read-only catalog discovery.
Writing rows
The destination table must already exist. Table uses scopedb and public when the database or schema is not specified.
Write with a stream
Use append_stream() to send Rust values. The SDK converts them to NDJSON and writes them in batches automatically.
# async
send() adds one row to the SDK's pending writes; send_all() adds rows from an iterator. These calls do not wait for the rows to be written. Call shutdown() when you are finished to wait for writing to complete and close the stream. If send_all() fails partway, its error includes the accepted count and stats() retains cumulative counts; still settle that accepted prefix with shutdown().
To wait for pending writes while keeping the stream open, call flush():
# async
A report covers activity since the previous barrier. With the default Stop policy, a failed flush() or shutdown() returns the complete error, and stats().last_report retains partial settlement counts. Earlier batches may have committed. Reports and failure events expose aggregate counts without identifying individual input records; use application-owned business keys to reconcile stored rows before selecting records to replay. stats().last_failure.error preserves the code, details, timeout reason, HTTP metadata, and source in Continue mode as well.
Dropping an enqueued flush or started shutdown future stops waiting, but settlement continues. Call shutdown() again to observe the final result. Dropping all producer handles drains accepted rows while the Tokio runtime remains alive; runtime termination cannot guarantee delivery.
Continue after a batch fails
To keep a log or telemetry sink running after batch failures, use AppendFailurePolicy::Continue and keep the circuit breaker enabled. The default Stop policy instead closes the stream after a batch failure. Continue mode is best effort: read flush()/shutdown() reports and stats() to account for failed, unknown, and locally dropped rows.
For producers running on multiple application replicas, start with one concurrent batch per stream and tune against the shared service's capacity. The default is four per stream: ten replicas with one stream each can issue up to 40 concurrent batches, while .max_concurrent_batches(1) reduces that total to ten. Account for additional streams in each replica as well.
use AppendCircuitBreakerOptions;
use AppendFailurePolicy;
# async
The circuit breaker is enabled by default. It pauses new batch dispatch after five consecutive availability failures and waits 30 seconds before probing recovery. try_send() returns immediately if the circuit is open or local capacity is full; use it when collecting telemetry must not wait for capacity. send().await provides backpressure instead. Send diagnostics to a separate sink and monitor dropped rows, failed batches, and unknown outcomes. See examples/telemetry.rs for a complete example.
Append NDJSON
Use append() when your data is already newline-delimited JSON: one JSON object per line.
ScopeDB 0.6's NDJSON decoder accepts integer inputs only within the signed 64-bit range, including inputs for uint columns and nested JSON values. Larger integers are rejected by the server; the SDK does not silently narrow them. Query results still support the full u64 range.
# async
More examples
| Task | Example |
|---|---|
| Write NDJSON | examples/append.rs |
| Send rows with a stream | examples/append_stream.rs |
| Import many rows | examples/bulk_append.rs |
| Write logs or events | examples/telemetry.rs |
| Import an NDJSON file | examples/import_ndjson.rs |
Inspecting errors
Use the error's methods to read its message, HTTP status, and request ID:
#
When a statement reaches the failed state, Error::statement_details() preserves the server-provided code, message, and optional code-specific JSON:
#
Table helper
# async
Migrate to ScopeDB 0.6
- Replace
with_exec_timeoutwithwith_max_elapsed(Duration).with_max_parallelismis removed;with_max_estimated(Duration)independently limits estimated execution time and does not control parallelism. Durations must be positive. Elapsed time includes queueing; omitted limits use server defaults. - Replace
list_*,fetch_*, anditerate_*Catalog calls withclient.catalog().get()or a database/schema/table/view selector followed by.get(). Reads return the completedefinitionand anetag, without pagination. - Replace
Table::describe()andtable_schema()withtable.catalog().get(), returning bothdefinitionandetag. Usedistinctandretention_days, replacingdistinct_onanddata_retention_days. Typed definitions reject unsupported fields;.raw()preserves them.Table::drop()remains available; use Catalogdelete()when a precondition or preview is needed. cancel()returns and caches the completeStatementStatus, including any finished result or original failure. Failed states requireerror; cancelled states have no server message.Error::code()anddetails()retain structured errors, including unknown codes. Progress includescompute_seconds.- Table appends use
/v1/catalog/databases/{db}/schemas/{schema}/tables/{table}/ingest. RenameAppendRowsResult::append_stateandAppendErrorDetails::append_statetostatus, matching the native response. Missing or unreadable outcomes remain unknown. The default still retries only temporary rejections. The new.retry_unknown(true)option permits replaying transient unknown outcomes and may insert duplicates; client timeouts are never replayed. - Replace
last_status()withsnapshot(),status()/fetch_once()withrefresh(), andfetch()withwait(). Read progress fromsnapshot().map(StatementStatus::progress)and matchStatementStatus::Finishedto inspect cached results.wait()also returns cached results without another request. - Replace the removed
json_rows()alias withraw_rows(), which directly returns the slice. Replace stream builder aliasesbatch_bytes,max_pending_bytes, andmax_in_flight_requestswithtarget_batch_bytes,max_buffered_bytes, andmax_concurrent_batches. - Read
stats().last_failure.errorinstead of separate message/status fields. Its completeErrorretains diagnostics across barriers.AppendStreamStatsno longer implements equality because errors carry sources. Table::identifier()always includes the database and schema defaults, so SQL queries anddrop()select the same table as Append and Catalog, including when only the database is configured. The legacyu_inttype spelling is removed; useuint.
Upgrading the server from 0.5 clears statement history and saved results. Previously saved statement handles cannot be resumed. The removed transform Ingest API requires the migration below.
Migrate from Ingest
Client::insert, Client::ingest_stream, IngestStream, IngestStreamBuilder, and IngestResult have been removed. Use Table::append for one NDJSON request or Table::append_stream to batch Rust values. This SDK version requires ScopeDB 0.6.0 or later.
Append accepts rows matching the destination columns, not a ScopeQL statement. Move field renaming, type conversion, filtering, and routing into the producer before calling send. For transformations that must run in ScopeQL, append the original event into an object column in a staging table, then execute a separate FROM ... SELECT ... INSERT INTO ... statement. Staging and transformation are separate commits; track processed batches before retrying the transformation.
# async
Review these behavior changes when migrating:
- Append batches allow up to 8 MiB of uncompressed NDJSON and 200,000 rows. Reduce an old ingest batch-byte target above 8 MiB.
- Append stream
build()returns aResult; handle configuration errors with?before sending records. flush()andshutdown()return anAppendDeliveryReportfor activity since the previous report. Readcommitted_rowsinstead ofnum_rows_inserted, and checkoutcomewhen using the continue policy. Directappend()still returnsnum_rows_inserted.- Append streams send up to four batches concurrently by default. Use
.max_concurrent_batches(1)for sequential requests; queries still requireORDER BYfor ordered results. - Append streams retry temporary rejections, where no rows committed. Use
.retry_unknown(true)only if duplicate rows are acceptable, or.max_retries(0)to disable all retries. Client timeouts are never retried. Directappend()makes one attempt.
Examples
The example guide in examples/README.md includes setup instructions and commands to run each example from the repository root.
The wire-level endpoint and payload reference is in docs/rust-http-api.md. Release history is in the repository's CHANGELOG.md.
License
Licensed under the Apache License, Version 2.0.