Collections are namespaces for organizing related documents within a Sentinel store. Each collection corresponds to a directory on your filesystem, making it easy to manage and browse your data using standard tools.
Creating and Accessing Collections
Collections are created automatically when you access them through a store:
use sentinel_dbms::Store;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new("./my-data", None).await?;
// Creates the collection directory if it doesn't exist
let users = store.collection("users").await?;
let products = store.collection("products").await?;
let audit_logs = store.collection("audit_logs").await?;
Ok(())
}Each call to collection() returns a Collection instance that provides CRUD operations for documents within that namespace.
Collection Structure
A collection is represented as a directory within your store’s data/ folder:
my-data/
└── data/
└── users/ # Collection directory
├── alice.json # Document: "alice"
├── bob.json # Document: "bob"
└── charlie.json # Document: "charlie"Document IDs become filenames with a .json extension. This means you can browse your data with any file manager or command-line tool.
CRUD Operations
Collections provide four primary operations for document management.
Insert
The insert method creates a new document or overwrites an existing one:
use sentinel_dbms::Store;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new("./my-data", None).await?;
let users = store.collection("users").await?;
// Insert a new document
users.insert("alice", json!({
"name": "Alice Johnson",
"email": "[email protected]",
"role": "admin"
})).await?;
// Overwriting is allowed - same ID replaces the document
users.insert("alice", json!({
"name": "Alice Smith", // Updated name
"email": "[email protected]",
"role": "admin"
})).await?;
Ok(())
}When you insert a document, Sentinel automatically computes a BLAKE3 hash of the data and optionally signs it if the store was created with a passphrase.
Get
The get method retrieves a document by its ID:
use sentinel_dbms::Store;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new("./my-data", None).await?;
let users = store.collection("users").await?;
// Insert first
users.insert("alice", json!({"name": "Alice"})).await?;
// Get returns Option<Document>
match users.get("alice").await? {
Some(doc) => {
println!("Found: {}", doc.data()["name"]);
println!("ID: {}", doc.id());
println!("Hash: {}", doc.hash());
}
None => {
println!("Document not found");
}
}
// Non-existent documents return None, not an error
let missing = users.get("nonexistent").await?;
assert!(missing.is_none());
Ok(())
}Update
The update method replaces a document’s contents:
use sentinel_dbms::Store;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new("./my-data", None).await?;
let users = store.collection("users").await?;
// Create initial document
users.insert("alice", json!({
"name": "Alice",
"level": 1
})).await?;
// Update with new data (full replacement)
users.update("alice", json!({
"name": "Alice",
"level": 2, // Leveled up!
"badges": ["early_adopter"]
})).await?;
Ok(())
}Note that update performs a full replacement, not a partial merge. If you want to preserve existing fields, read the document first, modify the data, then update.
Delete
The delete method removes a document from the collection:
use sentinel_dbms::Store;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new("./my-data", None).await?;
let users = store.collection("users").await?;
users.insert("temp-user", json!({"name": "Temporary"})).await?;
// Delete the document
users.delete("temp-user").await?;
// Deleting again is safe (idempotent)
users.delete("temp-user").await?;
// Verify it's gone
assert!(users.get("temp-user").await?.is_none());
Ok(())
}The delete operation is idempotent, deleting a non-existent document succeeds without error.
Document ID Validation
Document IDs must be valid filenames across all platforms. Sentinel validates IDs before any operation:
Valid characters include:
- Letters:
a-z,A-Z - Numbers:
0-9 - Underscore:
_ - Hyphen:
-
Invalid characters include:
- Path separators:
/,\ - Windows reserved characters:
< > : " | ? * - Control characters: ASCII 0x00-0x1F, 0x7F
- Dots (to avoid confusion with file extensions)
Examples of valid IDs:
user-123aliceDOCUMENT_001config-v2-backup
Examples of invalid IDs:
user/admin(contains path separator)file.name(contains dot)CON(Windows reserved name)- empty string
use sentinel_dbms::Store;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new("./my-data", None).await?;
let users = store.collection("users").await?;
// This works
users.insert("valid-id-123", json!({})).await?;
// This fails with InvalidDocumentId error
match users.insert("invalid/id", json!({})).await {
Err(sentinel_dbms::SentinelError::InvalidDocumentId { id }) => {
println!("Invalid ID: {}", id);
}
_ => {}
}
Ok(())
}Working with JSON Data
Sentinel uses serde_json::Value for document data, giving you flexibility in what you store:
use sentinel_dbms::Store;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new("./my-data", None).await?;
let data = store.collection("data").await?;
// Store various JSON types
data.insert("string-example", json!("just a string")).await?;
data.insert("number-example", json!(42)).await?;
data.insert("array-example", json!([1, 2, 3, 4, 5])).await?;
data.insert("nested-example", json!({
"level1": {
"level2": {
"level3": "deep value"
}
}
})).await?;
// Read and navigate nested data
if let Some(doc) = data.get("nested-example").await? {
let deep = &doc.data()["level1"]["level2"]["level3"];
println!("Deep value: {}", deep);
}
Ok(())
}Collection Names
Like document IDs, collection names must be valid directory names:
use sentinel_dbms::Store;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let store = Store::new("./my-data", None).await?;
// Valid collection names
store.collection("users").await?;
store.collection("audit_logs").await?;
store.collection("products-v2").await?;
store.collection("config.backup").await?; // Dots allowed in collections
// Invalid - would fail with InvalidCollectionName
// store.collection("users/admin").await?;
// store.collection("CON").await?;
Ok(())
}Note that collection names allow dots (for patterns like config.backup), while document IDs do not (to avoid confusion with the .json extension).
Error Handling
Collection operations return Result<T, SentinelError>. Handle errors appropriately:
use sentinel_dbms::{Store, SentinelError};
use serde_json::json;
#[tokio::main]
async fn main() {
let store = Store::new("./my-data", None).await.unwrap();
let users = store.collection("users").await.unwrap();
match users.insert("test", json!({})).await {
Ok(()) => println!("Document created"),
Err(SentinelError::InvalidDocumentId { id }) => {
eprintln!("Invalid document ID: {}", id);
}
Err(SentinelError::Io { source }) => {
eprintln!("I/O error: {}", source);
}
Err(SentinelError::Json { source }) => {
eprintln!("JSON error: {}", source);
}
Err(e) => {
eprintln!("Other error: {}", e);
}
}
}Best Practices
When working with collections, consider these recommendations:
Use meaningful collection names. Choose names that describe the data:
users,audit_logs,certificates,api_keys.Keep related documents together. Group documents by type and access patterns in the same collection.
Use consistent ID schemes. Adopt a naming convention for IDs:
user-{uuid},log-{timestamp},cert-{fingerprint}.Validate data before insertion. Sentinel stores whatever JSON you give it, validate your data first.
Consider using prefixes for large collections. If you expect millions of documents, use ID prefixes to help with manual navigation:
a-alice,b-bob.
Next Steps
Now that you understand collections, learn about:
- Document: Understanding document structure, metadata, and verification
- Cryptography: Configuring hashing, signing, and encryption algorithms
- Error Handling: Comprehensive guide to Sentinel errors