Rust Quickstart

August 11, 2026 · View on GitHub

This guide shows how to define a schema, run migrations, and perform CRUD with the mongreldb-kit crate.

Add the dependency

[dependencies]
mongreldb-kit = "0.64.16"
serde_json = "1"

Complete example

use mongreldb_kit::{
    Column, ColumnType, Database, DefaultKind, ForeignKey, ForeignKeyAction, Index, Migration,
    MigrationOp, Schema, Table, UniqueConstraint,
};
use serde_json::{json, Map};
use std::path::Path;

fn users_table() -> Table {
    Table {
        id: 1,
        name: "users".into(),
        columns: vec![
            Column::new(1, "id", ColumnType::Int64),
            Column::new(2, "email", ColumnType::Text),
            Column {
                nullable: true,
                ..Column::new(3, "name", ColumnType::Text)
            },
        ],
        primary_key: vec!["id".into()],
        indexes: vec![Index {
            name: "uq_user_email".into(),
            columns: vec!["email".into()],
            unique: true,
        }],
        foreign_keys: vec![],
        unique_constraints: vec![UniqueConstraint {
            name: "uq_user_email".into(),
            columns: vec!["email".into()],
        }],
        check_constraints: vec![],
    }
}

fn posts_table() -> Table {
    let mut published = Column::new(5, "published", ColumnType::Bool);
    published.default = Some(mongreldb_kit::DefaultKind::Static(json!(false)));

    Table {
        id: 2,
        name: "posts".into(),
        columns: vec![
            Column::new(1, "id", ColumnType::Int64),
            Column::new(2, "user_id", ColumnType::Int64),
            Column::new(3, "title", ColumnType::Text),
            Column {
                nullable: true,
                ..Column::new(4, "body", ColumnType::Text)
            },
            published,
        ],
        primary_key: vec!["id".into()],
        indexes: vec![],
        foreign_keys: vec![ForeignKey {
            name: "fk_posts_user".into(),
            columns: vec!["user_id".into()],
            references_table: "users".into(),
            references_columns: vec!["id".into()],
            on_delete: ForeignKeyAction::Cascade,
        }],
        unique_constraints: vec![],
        check_constraints: vec![],
    }
}

fn schema() -> Schema {
    Schema::new(vec![users_table(), posts_table()]).unwrap()
}

fn main() -> mongreldb_kit::Result<()> {
    let dir = std::env::temp_dir().join("kit-rust-demo");
    let _ = std::fs::remove_dir_all(&dir);

    // Create the database.
    let mut db = Database::create(&dir, schema())?;

    // Run migrations.
    let migrations = vec![
        Migration {
            version: 1,
            name: "initial".into(),
            ops: vec![
                MigrationOp::CreateTable { name: "users".into() },
                MigrationOp::CreateTable { name: "posts".into() },
            ],
        },
    ];
    mongreldb_kit::migrate(&mut db, &migrations)?;

    // Insert users.
    let mut txn = db.begin()?;
    let mut alice = Map::new();
    alice.insert("id".into(), json!(1));
    alice.insert("email".into(), json!("alice@example.com"));
    alice.insert("name".into(), json!("Alice"));
    txn.insert("users", alice)?;

    let mut bob = Map::new();
    bob.insert("id".into(), json!(2));
    bob.insert("email".into(), json!("bob@example.com"));
    txn.insert("users", bob)?;
    txn.commit()?;

    // Insert a post.
    let mut txn = db.begin()?;
    let mut post = Map::new();
    post.insert("id".into(), json!(1));
    post.insert("user_id".into(), json!(1));
    post.insert("title".into(), json!("Hello Kit"));
    post.insert("body".into(), json!("First post."));
    txn.insert("posts", post)?;
    txn.commit()?;

    // Query posts by user, ordered by id descending.
    let txn = db.begin()?;
    let query = mongreldb_kit::Query::Select(mongreldb_kit::Select {
        table: "posts".into(),
        columns: vec![
            mongreldb_kit::Expr::Column("id".into()),
            mongreldb_kit::Expr::Column("title".into()),
        ],
        filter: Some(mongreldb_kit::Expr::Eq(
            Box::new(mongreldb_kit::Expr::Column("user_id".into())),
            Box::new(mongreldb_kit::Expr::Literal(mongreldb_kit::Literal::Int(1))),
        )),
        order_by: vec![mongreldb_kit::OrderBy {
            expr: mongreldb_kit::Expr::Column("id".into()),
            direction: mongreldb_kit::Direction::Desc,
        }],
        limit: Some(10),
        offset: None,
    });
    let rows = txn.select(&query)?;
    for row in &rows {
        println!("{:?}", row.values);
    }

    // Update the post.
    let mut txn = db.begin()?;
    let mut patch = Map::new();
    patch.insert("published".into(), json!(true));
    txn.update("posts", &json!(1), patch)?;
    txn.commit()?;

    // Deleting Alice cascades to her posts.
    let mut txn = db.begin()?;
    txn.delete("users", &json!(1))?;
    txn.commit()?;

    Ok(())
}

Schema construction

A Table is built from Column values and assembled into a validated Schema with Schema::new.

let mut col = Column::new(1, "name", ColumnType::Text);
col.nullable = true;
col.default = Some(DefaultKind::Uuid);

Transactions

let mut txn = db.begin()?;
txn.insert("users", row)?;
let row = txn.get_by_pk("users", &json!(1))?;
let rows = txn.select(&query)?;
txn.update("users", &json!(1), patch)?;
txn.delete("users", &json!(1))?;
txn.commit()?;

Use txn.rollback() to abort.

Batch insert

txn.insert_many(table, rows) stages a whole Vec of rows in the open transaction and returns the stored Vec<Row> in order - the same per-row defaults, validation, sequence ids, and guards as insert, but staged in one pass so a single commit() writes the batch. For a single-column primary key it preloads the existing keys once, so the per-row duplicate check stays O(1).

let mut a = Map::new();
a.insert("sku".into(), json!("A-1"));
a.insert("name".into(), json!("Anvil"));
let mut b = Map::new();
b.insert("sku".into(), json!("B-1"));
b.insert("name".into(), json!("Bucket"));

let mut txn = db.begin()?;
let rows = txn.insert_many("products", vec![a, b])?; // returns Vec<Row> in order
txn.commit()?; // all-or-nothing: any row erroring rolls the batch back

Query AST

The kit exposes a language-neutral AST in mongreldb_kit_core::query:

use mongreldb_kit::{Expr, Literal, Query, Select, Direction, OrderBy};

let query = Query::Select(Select {
    table: "posts".into(),
    columns: vec![Expr::Column("title".into())],
    filter: Some(Expr::And(vec![
        Expr::Eq(
            Box::new(Expr::Column("published".into())),
            Box::new(Expr::Literal(Literal::Bool(true))),
        ),
        Expr::Gt(
            Box::new(Expr::Column("id".into())),
            Box::new(Expr::Literal(Literal::Int(0))),
        ),
    ])),
    order_by: vec![OrderBy {
        expr: Expr::Column("id".into()),
        direction: Direction::Desc,
    }],
    limit: Some(10),
    offset: None,
});

Expr also carries the full predicate set: Like, Contains, BytesPrefix (anchored prefix on a bitmap-indexed Bytes column - exact pushdown, no residual), IsNull/IsNotNull, In/NotIn, InSubquery, Exists/ NotExists, and the logical combinators. The engine pushes BytesPrefix down to a bitmap key-prefix scan when the column has a bitmap index:

// Find events whose `key` (a Bytes column with a bitmap index) starts with the
// bytes of "user:". Resolves to an exact bitmap-prefix lookup - no full scan.
let query = Query::Select(Select {
    table: "events".into(),
    columns: vec![],
    filter: Some(Expr::BytesPrefix(
        Box::new(Expr::Column("key".into())),
        "user:".into(),
    )),
    order_by: vec![],
    limit: None,
    offset: None,
});
let matched: Vec<Row> = txn.select(&query)?;

Migrations

let migrations = vec![
    Migration {
        version: 1,
        name: "initial".into(),
        ops: vec![MigrationOp::CreateTable { name: "users".into() }],
    },
];
mongreldb_kit::migrate(&mut db, &migrations)?;

The Rust runner executes every migration op directly: table/column/index/ constraint changes run against the core engine, while procedure/trigger ops call the engine DDL, and SQL-backed ops - CreateView/ReplaceView/DropView, CreateVirtualTable/DropVirtualTable, and RawSql - run through the embedded MongrelSession (see Embedded SQL surface below). There is no longer any "requires a SQL-capable Kit surface" caveat - the Rust Kit is that surface.

Triggers and remote SQL

Engine-side triggers use TriggerSpec, a JSON wrapper that keeps the Kit API aligned with the engine trigger schema:

use mongreldb_kit::TriggerSpec;
use serde_json::json;

let spec = TriggerSpec::new(json!({
    "name": "users_ai",
    "target": { "kind": "table", "name": "users" },
    "timing": "after",
    "event": "insert",
    "program": { "steps": [] }
}));

db.create_trigger(&spec)?;
db.replace_trigger(&spec)?;
db.drop_trigger("users_ai")?;

With the remote feature, RemoteDatabase::sql_rows runs daemon SQL. Configure Bearer or Basic authentication once so capabilities, SQL, cancellation, status, and every other route share the same credentials:

use mongreldb_kit::{RemoteAuth, RemoteDatabase, RemoteOptions, SecretString};

let remote = RemoteDatabase::connect_with_options(
    "https://db.example.com",
    RemoteOptions {
        auth: Some(RemoteAuth::Bearer(SecretString::from(
            std::env::var("MONGRELDB_TOKEN")?,
        ))),
        ..RemoteOptions::default()
    },
)?;

RemoteSqlQueryHandle::cancel returns RemoteCancelOutcome, distinguishing accepted, already-cancelling, too-late, already-finished, not-found, and pre-cancelled results. status returns the durable statement, commit epoch, terminal error, and retryability fields. If transport fails after submission, the client checks status before cancelling; it returns KitError::CommitOutcome when the server proves a commit and KitError::OutcomeUnknown when no terminal outcome can be established.

The same typed outcome contract covers remote /kit/txn, procedure, and trigger writes. A COMMIT_OUTCOME response remains KitError::CommitOutcome with committed and exact last_commit_epoch available through KitError::query_outcome(), plus retryable through KitError::query_metadata(). QUERY_OUTCOME_UNKNOWN remains KitError::OutcomeUnknown; do not replay it.

RemoteQueryStatus::durable_commit_state() returns None for that unknown case. Its commit and counter fields remain Option values. Never treat None as false or zero.

Remote SQL always requires cancellation capability version 2. This lets the client assign a query ID even for default sql_rows calls and recover a durable status if Arrow or JSON decoding fails after a commit.

Use owner-bound, bounded pagination for large read-only results:

use mongreldb_kit::RemoteSqlPaginationOptions;

let page = remote.sql_page(
    "SELECT id, title FROM documents ORDER BY id",
    RemoteSqlPaginationOptions {
        page_size_rows: 500,
        projection: vec!["id".into(), "title".into()],
        query_id: None,
        timeout: None,
        max_page_bytes: Some(1_000_000),
        max_page_tokens: Some(100_000),
        max_output_rows: None,
        max_output_bytes: None,
    },
)?;
if let Some(cursor) = page.next_cursor.as_deref() {
    let next = remote.continue_sql_page(cursor)?;
}

For a retry-safe single write, call execute_idempotent_sql with RemoteIdempotentSqlOptions. Reuse the same idempotency key after transport loss. The returned RemoteSqlWriteReceipt contains the original query ID, replay state, committed statement count, and exact last_commit_epoch. It also preserves the first and last committed statement indexes.

VirtualTableSpec generates module-backed virtual-table DDL:

use mongreldb_kit::VirtualTableSpec;

remote.create_virtual_table(&VirtualTableSpec::new(
    "docs_fts",
    "fts_docs",
    ["content=docs"],
))?;
remote.drop_virtual_table("docs_fts")?;

Embedded SQL surface and maintenance

Database::sql, sql_arrow, and sql_rows run statements through the kit's embedded MongrelSession (the engine's DataFusion SQL frontend). The session is held for the database's lifetime, so session-scoped objects (views, prepared statements, the result cache) persist across calls - mirroring a long-lived database connection. After a migration that creates or drops tables, call refresh_sql_session so the session sees the new table set.

use mongreldb_kit::{Database, ViewSpec};
use mongreldb_kit_core::MigrationOp;

// Read path: returns Arrow RecordBatch, raw IPC bytes, or JSON-style rows.
let batches = db.sql("SELECT id, email FROM users ORDER BY id")?;
let ipc: Vec<u8> = db.sql_arrow("SELECT id FROM users ORDER BY id")?;
let rows = db.sql_rows("SELECT id, email FROM users ORDER BY id")?;

// DDL/DML return empty; views live in the session.
db.sql("CREATE VIEW active AS SELECT id FROM users WHERE active = TRUE")?;
db.sql_rows("SELECT * FROM active")?; // queries the view

// Maintenance (the engine's ANALYZE / VACUUM equivalents).
db.analyze()?;          // ensure_indexes_complete() on every table
let reclaimed = db.vacuum()?; // compact_all() + gc()

// Rename a table (engine + kit schema catalog + persisted).
db.rename_table("widgets", "things")?;

// SQL views (session-scoped - live in the kit's long-lived MongrelSession).
use mongreldb_kit::ViewSpec;
db.create_view(&ViewSpec::new("active", "SELECT id FROM users WHERE active = TRUE"))?;
db.drop_view("active")?;

// Reserve the next AUTO_INCREMENT id (parity with TS reserveAutoIncSync).
let next_id: Option<i64> = db.reserve_auto_inc("orders")?;

Writes through sql() bypass kit-level constraints (defaults, enums, min/max, length, regex, triggers) - use the Transaction API for constrained writes. The engine's own declarative constraints (unique, FK, check) still apply.

Advanced SQL (recursive CTEs, windows, regex, catalog, ATTACH, SAVEPOINTs)

The embedded DataFusion 54 session supports the full SQL stdlib:

// Recursive CTE.
db.sql_rows("WITH RECURSIVE tree AS (
    SELECT id, 0 AS depth FROM nodes WHERE parent IS NULL
    UNION ALL
    SELECT n.id, t.depth + 1 FROM nodes n JOIN tree t ON n.parent = t.id
) SELECT id, depth FROM tree ORDER BY id")?;

// Window function.
db.sql_rows("SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM users")?;

// Regex.
db.sql_rows("SELECT id FROM users WHERE regexp('^admin.*', name) = 1")?;

// Catalog.
db.sql_rows("SELECT type, name FROM information_schema.tables ORDER BY name")?;

// ATTACH (cross-database).
db.sql("ATTACH './other' AS other")?;
db.sql_rows("SELECT id FROM other_items")?;

// SAVEPOINTs.
db.sql("BEGIN")?;
db.sql("INSERT INTO logs VALUES (1, 'hello')")?;
db.sql("SAVEPOINT sp1")?;
db.sql("INSERT INTO logs VALUES (2, 'world')")?;
db.sql("ROLLBACK TO sp1")?; // discards 'world'
db.sql("COMMIT")?;

Savepoints require an explicit transaction. ROLLBACK TO name keeps the target active, removes savepoints created after it, and can recover an aborted transaction so work can continue.

Storage tuning & introspection

The Kit surfaces the engine's power-user knobs for production sizing and observability. Database-wide tunables (set_spill_threshold, set_recursive_triggers, trigger_config / set_trigger_config) are atomic &self; per-table tunables and introspection go through the engine's per-table Mutex:

// Database-wide.
db.set_spill_threshold(1_000_000);
db.set_recursive_triggers(true);
let cfg = db.trigger_config(); // TriggerConfig { recursive_triggers, max_depth, max_loop_iterations }
db.set_trigger_config(TriggerConfig {
    recursive_triggers: true, max_depth: 16, max_loop_iterations: 5000
})?;

// Per-table tuning.
db.set_table_compaction_zstd_level("widgets", 3)?;
db.set_table_result_cache_max_bytes("widgets", 64_000_000)?;
db.set_table_index_build_policy("widgets", IndexBuildPolicy::Eager)?;

// Per-table introspection (read-only).
let runs = db.table_run_count("widgets")?;          // compaction target: 1
let stats = db.table_page_cache_stats("widgets")?;  // CacheStats { hits, misses, try_lock_misses }
let memtable = db.table_memtable_len("widgets")?;

All methods are also reachable via the Database::raw() escape hatch (db.raw().table(name)?.lock()) for the full engine surface.

If a second process may briefly hold the database lock, opt in to retrying opens:

use std::path::Path;
use mongreldb_kit::{Database, OpenOptions};

let opts = OpenOptions::new().with_lock_timeout_ms(5_000);
let db = Database::open_with_options(Path::new("./store.kitdb"), opts)?;

Sequences and defaults

A column whose DefaultKind::Sequence(name) default is set is auto-assigned from a named sequence when the inserted row omits it. Sequences are 1-based (the first value is 1, matching SQL AUTO_INCREMENT). You can also draw values directly:

let first = db.allocate_sequence("orders_id_seq", 1)?; // 1 on a fresh sequence
let block = db.allocate_sequence("orders_id_seq", 10)?; // reserve 10, returns the first

DefaultKind also covers Static(value), Now, Uuid, and CustomName(name).

Embeddings and providers

MongrelDB 0.60+ treats embedding generation as an optional pluggable layer. Core storage and ANN indexes never hard-code a vendor. Kit lowers optional column metadata and exposes the process-local registry on the embedded database.

Catalog: EmbeddingSource on columns

use mongreldb_kit::{Column, ColumnType, EmbeddingSource, Index, IndexKind, Schema, Table};

let mut emb = Column::new(2, "embedding", ColumnType::Embedding);
emb.embedding_dim = Some(4);
// Omit embedding_source → application-supplied (default).
emb.embedding_source = Some(EmbeddingSource::LocalModel {
    model_path: "/models/kit-mini".into(),
    model_id: "kit-mini".into(),
});
// Also: EmbeddingSource::SuppliedByApplication
//       EmbeddingSource::GeneratedColumn { provider: "my-provider".into() }

Create/alter lower this onto the engine ColumnDef.embedding_source field.

ANN representation and online replacement

Index::ann_quantization selects AnnQuantization::BinarySign (default, Hamming distance) or AnnQuantization::Dense (full finite f32, cosine distance). One physical online job operates on one column:

use mongreldb_kit::{AnnQuantization, Index, IndexKind};

let dense = Index {
    name: "idx_documents_vec".into(),
    columns: vec!["vec".into()],
    unique: false,
    kind: IndexKind::Ann,
    ann_quantization: AnnQuantization::Dense,
};
let job_id = db.start_replace_index("documents", "idx_documents_vec", &dense)?;
let job = db.wait_index_build(job_id, std::time::Duration::from_secs(60))?;
assert_eq!(job.state, mongreldb_core::JobState::Succeeded);

start_create_index, start_replace_index, index_build, wait_index_build, cancel_index_build, and resume_index_build expose the durable job lifecycle. Construction and committed-row catch-up run outside the final publication barrier.

Registry and explicit generation

use mongreldb_kit::{
    Database, EmbeddingNormalization, EmbeddingSource, FixedVectorProvider,
};
use std::sync::Arc;

// Register a provider (demo FixedVectorProvider is non-semantic — tests/plumbing only).
db.register_embedding_provider(Arc::new(FixedVectorProvider::new(
    "kit-mini",
    "kit-mini",
    "1",
    EmbeddingNormalization::None,
    vec![0.0, 1.0, 0.0, 0.0],
)));
assert!(db.embedding_providers().list_ids().contains(&"kit-mini".into()));

let source = EmbeddingSource::LocalModel {
    model_path: "/models/kit-mini".into(),
    model_id: "kit-mini".into(),
};
let vectors = db.embed_texts(&source, &["hello"], 4)?;
// Legacy source shapes use explicit generation. GeneratedColumnSpec writes
// materialize transactionally from their configured source columns.

embed_texts with SuppliedByApplication always refuses; mismatched dimensions error rather than inventing vectors. Core types (EmbeddingProvider, EmbeddingProviderRegistry, CoreEmbeddingSource, FixedVectorProvider) are re-exported from mongreldb_kit.

Non-goals

  • Guessing a provider or source columns when GeneratedColumnSpec is absent.
  • Remote Kit clients registering providers on mongreldb-server (server/operator config).
  • Hard-coded OpenAI/Anthropic/etc. clients inside Kit.
  • Hashed/random dense vectors presented as semantic search.

Error handling

KitError is a flat enum of stable categories, including Validation, Duplicate, ForeignKey, Restrict, TriggerValidation, Migration, Conflict, Storage, DatabaseLocked, and Integrity. Match on the variant you handle. DatabaseLocked identifies a database already owned by another live handle or process without parsing its message.

use mongreldb_kit::KitError;

match db.begin() {
    Ok(mut txn) => {
        if let Err(KitError::Duplicate(_)) = txn.insert("users", row) {
            println!("duplicate");
        }
    }
    Err(e) => eprintln!("{e}"),
}

Conflict is the retryable category; see Errors for the cross-language mapping.

Users, roles & permissions

The Kit forwards the engine's catalog-stored auth model - Argon2id-hashed users, roles that bundle permissions, and GRANT/REVOKE table-level access control. The Permission enum is re-exported from the kit crate so you do not need a direct mongreldb-core dependency.

use mongreldb_kit::{Database, Permission};

let db = Database::open("./store.kitdb")?;

// Users
db.create_user("alice", "s3cret-pw")?;
db.alter_user_password("alice", "new-pw")?;
assert!(db.verify_user("alice", "new-pw")?.is_some());
db.set_user_admin("alice", true)?; // admin bypasses all permission checks
let names: Vec<String> = db.users(); // ["alice"]

// Roles + permissions
db.create_role("analyst")?;
db.grant_permission("analyst", Permission::Select { table: "orders".into() })?;
db.grant_permission("analyst", Permission::Insert { table: "orders".into() })?;
db.grant_role("alice", "analyst")?;
let roles: Vec<String> = db.roles(); // ["analyst"]

// Reverse
db.revoke_role("alice", "analyst")?;
db.revoke_permission("analyst", Permission::Insert { table: "orders".into() })?;
db.drop_role("analyst")?;
db.drop_user("alice")?;

The full model (including SQL DDL like CREATE USER / GRANT and the HTTP daemon's Bearer + Basic auth modes) is documented in the engine Users, Roles & Permissions guide. The Kit CLI exposes the same operations as user and role subcommands.

Credential enforcement

A database with require_auth set rejects every open that does not supply valid credentials. Use the credentialed constructors to create or open such a database, and enable_auth/disable_auth to flip the flag in code.

use mongreldb_kit::Database;

// Create a new database with require_auth on, bootstrapping the first admin.
let db = Database::create_with_credentials(
    "./store.kitdb",
    schema,
    "alice",
    "s3cret-pw",
)?;

// Open an existing require_auth database.
let db = Database::open_with_credentials("./store.kitdb", "alice", "s3cret-pw")?;

assert!(db.require_auth_enabled());

// Turn require_auth on for an existing credentialless database.
db.enable_auth("alice", "s3cret-pw")?;

// Recovery: clear require_auth (needs an open handle).
db.disable_auth()?;
// Encrypted + credentialed: both layers in one call.
let db = Database::create_encrypted_with_credentials(
    "./store.kitdb", schema, "passphrase", "admin", "s3cret-pw",
)?;

// Long-lived handles call refresh_principal after a REVOKE to pick up
// permission changes made by other handles.
db.refresh_principal()?;

The full model and recovery flow are documented in the engine credential enforcement guide.

Running this example

cargo new kit-demo --bin
cd kit-demo
# Add mongreldb-kit and serde_json to Cargo.toml, then paste the code above.
cargo run

See also

History retention and time-travel reads

MongrelDB retains a configurable window of committed epochs for MVCC time-travel reads. Embedded databases initially keep only the latest epoch. The daemon defaults to 1024 epochs unless MONGRELDB_HISTORY_RETENTION_EPOCHS overrides it.

Embedded mode

use mongreldb_kit::Database;

let db = Database::open(&path, schema()).unwrap();

// Raise the window before writing if you need to read past the default.
db.set_history_retention_epochs(10_000).unwrap();

// Current window and earliest retained epoch.
let window = db.history_retention_epochs();
let earliest = db.earliest_retained_epoch();

// Insert and capture the epoch.
let mut tx = db.begin().unwrap();
tx.insert("t", [("id".into(), json!(1))].into_iter().collect()).unwrap();
tx.commit().unwrap();
let e1 = db.snapshot_epoch();

// Read a past snapshot — `rows_at_epoch` returns rows as of that epoch.
let past = db.rows_at_epoch("t", e1).unwrap();

Increasing the window cannot restore history that was already pruned, so earliest_retained_epoch never moves backward.

Remote mode

When using RemoteDatabase (the remote cargo feature), the same three controls are forwarded to the daemon's GET/PUT /history/retention endpoints:

use mongreldb_kit::RemoteDatabase;

let remote = RemoteDatabase::connect("http://127.0.0.1:8453").unwrap();
remote.set_history_retention_epochs(10_000).unwrap();
let window = remote.history_retention_epochs().unwrap();
let earliest = remote.earliest_retained_epoch().unwrap();

For SQL time-travel, use AS OF EPOCH in a query string:

let rows = remote.sql_rows(&format!("SELECT * FROM t AS OF EPOCH {e1}")).unwrap();