A type-safe SurrealDB ORM for Rust — a typed query
builder, a #[derive(SurrealRecord)] macro, schema generation, and Diesel-style
migrations.
somnia — Latin for "dreams". SurrealDB is surreal (dreamlike); somnia is where your Rust types dream in SurrealQL.
[dependencies]
somnia = "0.4"Writing SurrealQL as hand-spliced strings is error-prone: typo'd table names,
unescaped values, record-link mistakes, and projection drift. somnia lets your
Rust types describe the schema once and gives you:
- Typed query building —
Post::table().select(...).filter(Post::title().eq("hello")) #[derive(SurrealRecord)]— typed column accessors, table metadata, and schema DDL generated from the struct.- Schema as code —
up()/down()emitDEFINE TABLE/DEFINE FIELD/REMOVE TABLEfrom the Rust type. - Diesel-style migrations — a
Migratorthat appliesup.surql/ revertsdown.surqlfrom timestamped folders, with applied-state tracking.
somnia inlines literals (with proper escaping) rather than relying on bind
parameters — to_surrealql() returns a ready-to-run statement string, which keeps
generated queries transparent and easy to log.
use somnia::{SurrealRecord, Thing};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, SurrealRecord)]
#[table("post")]
struct Post {
#[field(thing)]
id: Thing<Post>,
title: String,
body: String,
published_at: Option<String>,
}use somnia::{col, field, ident, RecordLink, Returning};
// SELECT with typed columns + function-wrapped projections
let sql = Post::table()
.project(vec![
field("record::id(id)", "id"),
col("title"),
field("type::string(published_at)", "published_at"),
])
.filter(Post::published_at().ne(None))
.order_desc(ident("published_at"))
.limit(20)
.to_surrealql();
// CREATE … with record links
let create = Post::table()
.create()
.record("post-1".to_string())
.set_lit("title", "Hello, world".to_string())
.set_expr("author", RecordLink::new("author", "bob".to_string()))
.set_raw("published_at", "time::now()")
.returning(Returning::After)
.to_surrealql();
// UPSERT — update the record if it exists, otherwise create it
let upserted = Post::table()
.upsert()
.record("post-1".to_string())
.set_lit("title", "Hello again".to_string())
.returning(Returning::After)
.to_surrealql();
// CREATE then SELECT back with typed projections
let batch = Post::table()
.create()
.record("post-1".to_string())
.set_lit("title", "Hello, world".to_string())
.set_expr("author", RecordLink::new("author", "bob".to_string()))
.set_raw("published_at", "time::now()")
.returning(Returning::After)
.then_select(
Post::table()
.project(vec![
field("record::id(id)", "id"),
col("title"),
field("type::string(published_at)", "published_at"),
])
.limit(1),
);
// UPDATE / DELETE with RETURN variants
let del = Post::table()
.delete()
.filter(ident("id").eq_expr(RecordLink::new("post", "post-1".to_string())))
.returning(Returning::Before)
.to_surrealql();For SurrealQL that isn't modeled as typed nodes (lambdas, IF/THEN/ELSE,
string::* chains), use the Raw(...) / field("…raw…", "alias") escape hatch —
the builder still owns the statement structure, table names, and record links.
#[derive(SurrealRecord)] also implements SurrealSchema:
use somnia::SurrealSchema;
#[derive(Debug, Clone, Serialize, Deserialize, SurrealRecord)]
#[table("comment")]
struct Comment {
#[field(thing)] id: Thing<Comment>,
#[field(record = "post")] post: serde_json::Value,
body: String,
#[field(ty = "datetime", default = "time::now()")] created_at: String,
}
Comment::up(); // DEFINE TABLE … ; DEFINE FIELD … ;
Comment::down(); // REMOVE TABLE IF EXISTS comment;Field attributes: #[field(thing)] (record id), record = "table"
(record<table>), default = "…", value = "…", ty = "…" (full type
override), flexible, name = "…", skip. Table attributes:
#[table("name")], #[table("name", schemaless, permissions = "NONE")].
Lay out migrations Diesel-style — one timestamped folder per migration with
up.surql and down.surql:
migrations/
2025-01-01-000000_create_posts/
up.surql
down.surql
2025-01-01-000100_seed_defaults/
up.surql
down.surqluse somnia::SomniaClient;
let client = SomniaClient::connect("ws://localhost:8000", "root", "root", "ns", "db").await?;
let migrator = client.migrator("migrations");
migrator.run().await?; // apply all pending up.surql in order
migrator.revert_last().await?; // run the latest down.surql
for m in migrator.status().await? {
println!("{} {}", if m.applied { "✓" } else { " " }, m.id);
}Applied migrations are tracked in a _somnia_migrations table, so re-running only
applies what's pending.
| Crate | Description |
|---|---|
somnia |
Umbrella crate: client, migrator, re-exports. Start here. |
somnia-core |
Query builder, expression tree, SurrealRecord/SurrealSchema traits. |
somnia-derive |
#[derive(SurrealRecord)] proc-macro. |
somnia-cli |
Diesel-cli-style migration runner (the somnia binary). |
A standalone migration runner, modeled on diesel-cli. Install it with Cargo or
Homebrew (both provide the somnia binary):
cargo install somnia-cli # from crates.io
brew tap vbasky/somnia && brew install somnia # Homebrew (macOS / Linux)Then:
somnia migration generate create_posts # scaffold a timestamped up/down folder
somnia migration run # apply all pending migrations
somnia migration revert # revert the latest
somnia migration redo # revert + re-apply the latest
somnia migration list # show applied / pendingConnection settings are read from flags or environment variables (--help for
the full list).
0.4.x — early but tested against SurrealDB 3.x (query builder, derive, schema
generation, and migrator all covered by integration tests that run on an
in-memory engine). The API may evolve before 1.0. See the
roadmap for what's covered today and what's planned on the way to
1.0.
MSRV: Rust 1.95 (set by the SurrealDB 3.x dependency tree). Bumping the minimum supported Rust version is treated as a minor-version change.
Licensed under the Apache License, Version 2.0.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.
