CRUD Operations
RouchDB provides a familiar document database API for creating, reading, updating, and deleting JSON documents. Every document has an _id (string identifier) and a _rev (revision string) that tracks its edit history.
Creating a Database
#![allow(unused)]
fn main() {
use rouchdb::Database;
// In-memory (data lost when dropped; great for tests)
let db = Database::memory("mydb");
// Persistent storage backed by redb
let db = Database::open("path/to/mydb.redb", "mydb")?;
// Remote CouchDB instance
let db = Database::http("http://localhost:5984/mydb");
}
Put (Create)
Use put() to create a new document with a given ID. The data is any serde_json::Value.
#![allow(unused)]
fn main() {
use serde_json::json;
let result = db.put("user:alice", json!({
"name": "Alice",
"email": "alice@example.com",
"age": 30
})).await?;
assert!(result.ok);
assert_eq!(result.id, "user:alice");
// The new revision string (e.g. "1-a3f2b...")
let rev = result.rev.unwrap();
}
The returned DocResult contains:
ok– whether the write succeeded.id– the document ID.rev– the new revision string (if successful).error/reason– error details (if failed).
Get (Read)
Retrieve a document by its ID.
#![allow(unused)]
fn main() {
let doc = db.get("user:alice").await?;
println!("Name: {}", doc.data["name"]);
println!("Rev: {}", doc.rev.as_ref().unwrap());
}
The Document struct contains:
id– the document ID.rev– the currentRevision(pos + hash).deleted– whether this document is deleted.data– the JSON body asserde_json::Value.attachments– a map ofAttachmentMetaentries.
Get with Options
Use get_with_opts() when you need a specific revision or conflict information.
#![allow(unused)]
fn main() {
use rouchdb::GetOptions;
let doc = db.get_with_opts("user:alice", GetOptions {
rev: Some("1-a3f2b...".to_string()),
conflicts: true,
..Default::default()
}).await?;
}
GetOptions fields:
rev– fetch a specific revision instead of the winner.conflicts– include conflicting revision IDs.open_revs– return all open (leaf) revisions.revs– include the full revision history chain.
Update
To update a document you must supply the current revision. This prevents lost updates when multiple writers are active.
#![allow(unused)]
fn main() {
let r1 = db.put("user:alice", json!({"name": "Alice", "age": 30})).await?;
let rev = r1.rev.unwrap();
let r2 = db.update("user:alice", &rev, json!({
"name": "Alice",
"age": 31,
"email": "alice@newdomain.com"
})).await?;
assert!(r2.ok);
// r2.rev is now "2-..."
}
If you pass a stale revision, RouchDB returns RouchError::Conflict.
Remove (Delete)
Deletion in CouchDB-compatible databases is a soft delete: the document is marked with _deleted: true and a new revision is created.
#![allow(unused)]
fn main() {
let doc = db.get("user:alice").await?;
let rev = doc.rev.unwrap().to_string();
let result = db.remove("user:alice", &rev).await?;
assert!(result.ok);
}
After deletion, db.get("user:alice") returns RouchError::NotFound. The document still participates in replication so that deletions propagate to other replicas.
Bulk Docs
Write multiple documents in a single atomic operation. This is more efficient than individual puts, and it is the only way to write documents with explicit revision control.
#![allow(unused)]
fn main() {
use rouchdb::{Document, BulkDocsOptions};
use std::collections::HashMap;
let docs = vec![
Document {
id: "user:bob".into(),
rev: None,
deleted: false,
data: json!({"name": "Bob"}),
attachments: HashMap::new(),
},
Document {
id: "user:carol".into(),
rev: None,
deleted: false,
data: json!({"name": "Carol"}),
attachments: HashMap::new(),
},
];
let results = db.bulk_docs(docs, BulkDocsOptions::new()).await?;
for r in &results {
println!("{}: ok={}", r.id, r.ok);
}
}
BulkDocsOptions has one key field:
new_edits(defaulttrue) – when true, the adapter generates new revision IDs and checks for conflicts. Set tofalseviaBulkDocsOptions::replication()during replication, where revisions are accepted as-is.
All Docs
Query all documents in the database, optionally filtered by key range.
#![allow(unused)]
fn main() {
use rouchdb::AllDocsOptions;
let response = db.all_docs(AllDocsOptions {
include_docs: true,
start_key: Some("user:a".into()),
end_key: Some("user:d".into()),
limit: Some(10),
..AllDocsOptions::new()
}).await?;
for row in &response.rows {
println!("{} rev={}", row.id, row.value.rev);
if let Some(ref doc) = row.doc {
println!(" data: {}", doc);
}
}
}
AllDocsOptions fields:
start_key/end_key– define the key range (string-sorted).key– fetch a single key.keys– fetch a specific set of keys.include_docs– embed full document bodies in results.conflicts– include conflict information for each document.update_seq– include the database update sequence in the response.descending– reverse the sort order.skip– number of rows to skip.limit– maximum number of rows.inclusive_end– whether to includeend_key(defaulttrue).
The response is an AllDocsResponse with total_rows, offset, and a rows vector of AllDocsRow.
Error Handling
RouchDB uses the RouchError enum for all errors. The most common ones in CRUD operations:
#![allow(unused)]
fn main() {
use rouchdb::RouchError;
match db.get("nonexistent").await {
Ok(doc) => println!("Found: {}", doc.id),
Err(RouchError::NotFound(msg)) => {
println!("Document not found: {}", msg);
}
Err(e) => println!("Other error: {}", e),
}
// Conflict when updating with a stale revision
match db.update("user:alice", "1-stale", json!({})).await {
Err(RouchError::Conflict) => {
println!("Conflict! Re-read and retry.");
}
_ => {}
}
}
Key error variants:
RouchError::NotFound(String)– document or resource does not exist.RouchError::Conflict– document update conflict (stale revision).RouchError::BadRequest(String)– malformed input.RouchError::InvalidRev(String)– unparseable revision string.RouchError::MissingId– document ID was not provided.RouchError::Unauthorized/RouchError::Forbidden(String)– access denied.RouchError::DatabaseError(String)– storage-level errors.
Idiomatic Update-Retry Pattern
Because conflicts are expected in a multi-replica system, a common pattern is to re-read and retry:
#![allow(unused)]
fn main() {
loop {
let doc = db.get("counter").await?;
let rev = doc.rev.unwrap().to_string();
let mut count = doc.data["count"].as_u64().unwrap_or(0);
count += 1;
match db.update("counter", &rev, json!({"count": count})).await {
Ok(_) => break,
Err(RouchError::Conflict) => continue, // retry
Err(e) => return Err(e),
}
}
}