Rust (Embedded)
The reifydb crate is the whole engine as a library. Add one dependency and your process owns a database: no server, no ports, no login, no serialization across a wire. This is the reference surface - every other client ultimately reaches the same engine this crate hands you directly. The API is synchronous; you do not need tokio or any async runtime to use it.
Installation
One line in Cargo.toml. The crate is versioned in lockstep with the engine, because it is the engine:
[dependencies]
reifydb = "0.8.1"The default feature set is the plain embedded engine: storage, transactions, RQL, events and handlers. Larger subsystems are opt-in Cargo features: sub_flow enables the streaming flow engine, which powers views and the in-process subscriptions shown below; sub_tracing, sub_profiler, sub_replication, and sub_raft add their respective subsystems. The sub_server_* features turn the same crate into the network server (server::memory() and friends) - that surface is covered on Clients and Connect, not here. If you want live subscriptions in-process, enable the flow engine:
[dependencies]
reifydb = { version = "0.8.1", features = ["sub_flow"] }Building a database
Everything starts in the embedded module. Three factories choose the storage backend, each returning an EmbeddedBuilder whose build() produces a running Database:
use reifydb::{SqliteConfig, embedded};
// In memory only. Fast, and nothing survives the process.
let db = embedded::memory().build()?;
// Buffered SQLite persistence: commits are visible immediately,
// a background flusher migrates them to disk.
let db = embedded::sqlite(SqliteConfig::new("/var/lib/myapp/data")).build()?;
// No in-memory buffer: every commit is written to SQLite
// before it returns. Durable at commit, slower to write.
let db = embedded::sqlite_without_buffer(SqliteConfig::new("/var/lib/myapp/data")).build()?;SqliteConfig::new(path) is the sensible default (WAL journal, normal synchronous mode). SqliteConfig::safe(path) trades speed for full synchronous writes, SqliteConfig::fast(path) does the opposite, and every field (journal mode, cache size, page size, mmap size, read pool size, and more) has a builder-style setter if the presets do not fit. What each backend actually persists, the write path, and exactly what survives a restart are documented on Durability & Storage - the short version is that RQL behaves identically on all three; only the fate of committed bytes differs.
Commands, queries, and admin
Statements come in the same three kinds every ReifyDB surface uses: a query reads, a command writes data, and an admin statement changes schema or identities. The Database exposes one method per kind, each taking an RQL string and parameters and returning Result<Vec<Frame>>:
use reifydb::{Params, embedded};
fn main() -> Result<(), reifydb::Error> {
let mut db = embedded::memory().build()?;
// Admin: schema changes.
db.admin_as_root("CREATE NAMESPACE app", Params::None)?;
db.admin_as_root(
"CREATE TABLE app::users { id: Int4, name: Utf8 }",
Params::None,
)?;
// Command: writes.
db.command_as_root(
r#"INSERT app::users [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]"#,
Params::None,
)?;
// Query: reads.
let frames = db.query_as_root("FROM app::users", Params::None)?;
for frame in frames {
println!("{}", frame);
}
db.stop()?;
Ok(())
}The _as_root variants run as the root identity, which is the right default for a process that owns its database. Each has an _as(identity, ...) twin - query_as, command_as, admin_as - that executes as a specific IdentityId instead. There is no login anywhere: authentication gates the network boundary, and an embedded database has none. Your process holds the engine and simply chooses which identity each operation runs as.
Errors are reifydb::Error, a wrapper around the engine's diagnostic: a stable code (TXN_001 is a transaction conflict), a message, the offending RQL fragment, and often a help text. Display renders the full diagnostic, and it implements std::error::Error, so ? composes with whatever error handling your application already has.
Parameters
Every execution method takes impl Into<Params>. Params::None means no parameters; positional parameters are referenced as $1, $2, ... (1-based) and built from a Vec or array of Values; named parameters are referenced as $name and built from a HashMap<String, Value>:
use std::collections::HashMap;
use reifydb::Value;
// Positional: $1, $2, ...
let frames = db.query_as_root(
"MAP { total: $1 + $2 }",
[Value::Int4(40), Value::Int4(2)],
)?;
// Named: $name
let mut params = HashMap::new();
params.insert("name".to_string(), Value::Utf8("Alice".to_string()));
let frames = db.query_as_root(
"FROM app::users FILTER name == $name",
params,
)?;Use parameters for anything that originates outside your program. String concatenation into RQL has the same injection problem it has in SQL; parameters are typed values and never re-parsed as syntax.
Sessions and conflict retries
The direct command_as_root call executes exactly once: if two concurrent transactions conflict, the loser gets a TXN_001 error back and it is your problem. A Session bundles an identity with a retry strategy so contended write paths handle this for you. db.root_session() creates one as root, db.session(identity) as anyone else:
use reifydb::{Params, RetryStrategy};
let session = db.root_session();
// Commands and admin statements retry on transaction conflicts
// (up to 10 attempts with jittered exponential backoff by default).
let result = session.command(
r#"UPDATE app::users { name: "Alicia" } FILTER id == 1"#,
Params::None,
);
if let Some(e) = result.error {
eprintln!("command failed: {}", e);
} else {
for frame in result.frames {
println!("{}", frame);
}
}
// Opt out, or tune the strategy:
let session = db.root_session().with_retry(RetryStrategy::no_retry());Session methods return an ExecutionResult with frames, an optional error, and execution metrics, rather than a Result. Only genuine conflicts (TXN_001) are retried - any other error returns immediately, and query never retries because reads do not conflict. The default strategy is 10 attempts with jittered exponential backoff from 5ms up to a 200ms cap; RetryStrategy also offers no_retry(), with_fixed_backoff, with_exponential_backoff, and with_jittered_backoff constructors, and the Backoff enum if you want to assemble one by hand.
Mapping rows to structs
Results arrive as Frames: columnar tables with named, typed columns. Display renders one as the ASCII table you see in every example, frame.columns gives raw columnar access, and frame.to_rows() pivots to per-row (name, Value) pairs. For application code, derive FromFrame and skip the column-walking entirely:
use reifydb::{FromFrame, Params};
#[derive(FromFrame, Debug)]
struct User {
id: i32,
name: String,
}
let frames = db.query_as_root("FROM app::users", Params::None)?;
for frame in &frames {
let users: Vec<User> = User::from_frame(frame).unwrap();
for user in &users {
println!("{}: {}", user.id, user.name);
}
}Field names must match column names and field types must match column types (Int4 to i32, Utf8 to String, and so on). Attributes adjust the mapping: #[frame(column = "other_name")] reads a differently named column, #[frame(optional)] makes the field an Option that tolerates missing columns and none values, #[frame(coerce)] allows widening type coercion, and #[frame(skip)] fills the field from Default instead of the frame. Failures return a FromFrameError naming the exact column and row that did not fit.
In-process subscriptions
With the sub_flow feature enabled, the embedded database delivers live changes without any socket. subscribe_as_root(query, params, hydration) (and subscribe_as for other identities) creates a subscription over an RQL body and returns a handle you drain:
use reifydb::{HydrationConfig, Params};
let sub = db.subscribe_as_root(
"from app::users | map { id, name }",
Params::None,
HydrationConfig::default(),
)?;
db.command_as_root(
r#"INSERT app::users [{ id: 3, name: "Carol" }]"#,
Params::None,
)?;
// Non-blocking: returns whatever has been delivered so far.
for frame in sub.drain(usize::MAX) {
println!("{}", frame);
}With HydrationConfig::default() the current snapshot of the source is delivered first, so subscribing to an already-populated table still observes every existing row before forward changes; set enabled: false to receive only changes committed after the subscription exists. Each delivered row carries an _op column (insert = 1, update = 2, remove = 3). drain(max) is non-blocking and delivery is asynchronous - a real consumer polls it in a loop. Subscriptions are persistent objects: after a stop() and reopen, re-attach to the same subscription with db.subscription(id) (the old handle is stale; the id comes from sub.id()).
Builder options
EmbeddedBuilder configures everything the engine loads before build(). The full surface today:
- --
with_migrations(source)- register schema migrations, applied duringbuild(). See below. - --
with_config(key, value)/with_configs(...)- set system configuration at bootstrap, such as theConfigKey::Threads*pool sizes. Applied on every build, overwriting previously persisted values. - --
with_runtime_config(RuntimeConfig)- the process runtime (clock and rng); the default is right unless you are injecting a mock clock in tests. - --
with_routines(...)andwith_handlers(...)- register native Rust functions and handlers that RQL can call. - --
with_transforms(...)- register custom transform operators with the extension registry. - --
with_procedure_dir(dir)(native targets) andwith_wasm_procedure_dir(dir)- load procedures from a directory. - --
with_auth(...)- configure the auth service (relevant mostly when the same builder later grows server subsystems). - --
with_fast_shutdown()- skip the drain-and-flush pass on stop and drop. Right for throwaway test databases, wrong for anything whose data you want back. - --Feature-gated subsystem hooks:
with_flow(...)(sub_flow, custom flow operators),with_tracing(...)(sub_tracing),with_profiler(...)(sub_profiler),with_replication(...)(sub_replication), and the genericwith_subsystem(factory). Transaction interceptors hook in through the same builder as well.
Notably absent: WebSocket or HTTP options. Those belong to the server:: builder, which is the same crate behind the sub_server_* features. Swapping embedded:: for server:: keeps your in-process calls working and additionally serves remote clients.
Migrations are worth showing. A Migration is a named list of statements (optionally with a rollback body); a directory of .rql files works too and is loaded in file-name order. Migrations are recorded in the database on first encounter and applied in name order; already-applied ones are skipped, so the same builder code is safe to run on every start:
use reifydb::{Migration, SqliteConfig, embedded};
let db = embedded::sqlite(SqliteConfig::new("/var/lib/myapp/data"))
.with_migrations(vec![
Migration::new("001_create_app", vec![
"CREATE NAMESPACE app",
"CREATE TABLE app::users { id: Int4, name: Utf8 }",
]),
])
.build()?;
// Or from a directory of .rql files, applied in file-name order:
// embedded::sqlite(config).with_migrations("migrations/").build()?;Shutting down
db.stop() is the clean shutdown: it drains the change-log consumers, flushes every committed write to the persistent store, and stops all subsystems. After a clean stop, a reopen of a SQLite-backed database sees every commit - the flush interval only matters for crashes, as Durability & Storage explains. Dropping a running Database attempts the same graceful shutdown, so even drop(db) or falling off the end of main is safe; calling stop() yourself just makes the intent (and any error) explicit. stop_fast() and the with_fast_shutdown() builder flag skip the drain for throwaway databases.
For a long-running process, db.start_and_await_signal() blocks until SIGINT, SIGTERM, SIGQUIT, or SIGHUP arrives and then runs the clean shutdown; await_signal_with_shutdown(closure) lets you run your own teardown first.
examples directory in the ReifyDB repository is a set of runnable programs covering everything on this page and more - tables, enums, events and handlers, storage backends, interceptors, and export. If your architecture outgrows a single process, the Rust client speaks to a server with the same three statement kinds, and Connect shows the shortest path to a running server.