somnia

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.
[]
= "0.6"
Why
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")) - Graph traversal — query across
RELATEedges with typed paths (Path::out::<Wrote>().to::<Post>()), including recursive@.{..}paths. #[derive(SurrealRecord)]— typed column accessors, table metadata, and schema DDL generated from the struct.- Schema as code —
up()/down()emitDEFINE TABLE/DEFINE FIELD/DEFINE INDEX/REMOVE TABLEfrom the Rust type. - Diesel-style migrations — a
Migratorthat appliesup.surql/ revertsdown.surqlfrom timestamped folders, with applied-state tracking. - The rest of SurrealQL, typed — atomic transactions,
$parambinding, subqueries,IF/FORcontrol flow, andDEFINE EVENT/FUNCTION/ANALYZER/PARAM— so you rarely drop toRaw(...).
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.
Quick start
Define a record
use ;
use ;
Build queries
use ;
// SELECT with typed columns + function-wrapped projections
let sql = table
.project
.filter
.order_desc
.limit
.to_surrealql;
// CREATE … with record links
let create = table
.create
.record
.set_lit
.set_expr
.set_raw
.returning
.to_surrealql;
// UPSERT — update the record if it exists, otherwise create it
let upserted = table
.upsert
.record
.set_lit
.returning
.to_surrealql;
// CREATE then SELECT back with typed projections
let batch = table
.create
.record
.set_lit
.set_expr
.set_raw
.returning
.then_select;
// UPDATE / DELETE with RETURN variants
let del = table
.delete
.filter
.returning
.to_surrealql;
// Graph traversal across RELATE edges (`Wrote`/`Knows` are `SurrealEdge` types)
use Path;
// SELECT ->wrote->post.title AS titles FROM author
let titles = table
.project_path
.to_surrealql;
// Recursive paths: every author within 3 "knows" hops
let network = table
.project_path
.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.
Schema as code
#[derive(SurrealRecord)] also implements SurrealSchema:
use SurrealSchema;
up; // DEFINE TABLE … ; DEFINE FIELD … ;
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")].
Field types are mapped from the Rust type — including typed arrays
(Vec<T> → array<…>), Option<…>, records, duration, and decimal.
Add indexes with a repeatable container attribute; they're emitted by up()
(after the fields) and exposed via SurrealSchema::define_indexes():
For ad-hoc or richer indexes (full-text SEARCH, vector HNSW/MTREE), use the
DefineIndex builder directly.
Migrations
Lay out migrations Diesel-style — one timestamped folder per migration with
up.surql and down.surql:
use SomniaClient;
let client = connect.await?;
let migrator = client.migrator;
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?
Applied migrations are tracked in a _somnia_migrations table, so re-running only
applies what's pending.
More query power
Beyond CRUD, somnia models much of SurrealDB's surface as typed builders:
use ;
// Atomic transaction — all statements commit, or none do
let tx = new
.push
.push
.to_surrealql; // BEGIN TRANSACTION; … ; COMMIT TRANSACTION;
// $param binding instead of inlined literals
let = table
.select
.filter
.to_surrealql_with_params; // ("… WHERE title = $p0", { p0: "hello" })
// Subqueries + IN — a Select is usable as an expression
let recent = table.project.value.filter;
let sql = table
.select
.filter
.to_surrealql;
// SELECT modifiers: VALUE / OMIT / SPLIT / WITH INDEX / TIMEOUT / EXPLAIN
let sql = table.select.omit.timeout.to_surrealql;
// Control flow as expressions
let label = new.else_;
let seed = new.push;
// Schema DDL beyond tables/fields/indexes
let ev = new
.when.then.to_surrealql;
let f = new.arg.returns
.body.to_surrealql;
Edge records can derive their SurrealEdge impl: #[derive(SurrealRecord, SurrealEdge)].
Crates
| Crate | Description |
|---|---|
somnia |
Umbrella crate: client, migrator, re-exports. Start here. |
somnia-core |
Query builder, expression tree, SurrealRecord/SurrealSchema traits. |
somnia-derive |
#[derive(SurrealRecord)] / #[derive(SurrealEdge)] proc-macros. |
somnia-cli |
Diesel-cli-style migration runner (the somnia binary). |
CLI
A standalone migration runner, modeled on diesel-cli. Install it with Cargo or
Homebrew (both provide the somnia binary):
&&
Then:
Connection settings are read from flags or environment variables (--help for
the full list).
Status
0.6.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.
License
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.