# somnia

[](https://crates.io/crates/somnia)
[](https://docs.rs/somnia)
[](https://github.com/vbasky/somnia/actions/workflows/ci.yml)
[](#status)
[](#license)
**A type-safe [SurrealDB](https://surrealdb.com) 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.
```toml
[dependencies]
somnia = "0.4"
```
---
## 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"))`
- **`#[derive(SurrealRecord)]`** — typed column accessors, table metadata, and
schema DDL generated from the struct.
- **Schema as code** — `up()` / `down()` emit `DEFINE TABLE` / `DEFINE FIELD` /
`REMOVE TABLE` from the Rust type.
- **Diesel-style migrations** — a `Migrator` that applies `up.surql` /
reverts `down.surql` from 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.
## Quick start
### Define a record
```rust
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>,
}
```
### Build queries
```rust
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.
### Schema as code
`#[derive(SurrealRecord)]` also implements `SurrealSchema`:
```rust
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")]`.
### Migrations
Lay out migrations Diesel-style — one timestamped folder per migration with
`up.surql` and `down.surql`:
```bash
migrations/
2025-01-01-000000_create_posts/
up.surql
down.surql
2025-01-01-000100_seed_defaults/
up.surql
down.surql
```
```rust
use 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.
## Crates
| [`somnia`](https://github.com/vbasky/somnia/tree/main/crates/somnia) | Umbrella crate: client, migrator, re-exports. Start here. |
| [`somnia-core`](https://github.com/vbasky/somnia/tree/main/crates/somnia-core) | Query builder, expression tree, `SurrealRecord`/`SurrealSchema` traits. |
| [`somnia-derive`](https://github.com/vbasky/somnia/tree/main/crates/somnia-derive) | `#[derive(SurrealRecord)]` proc-macro. |
| [`somnia-cli`](https://github.com/vbasky/somnia/tree/main/crates/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):
```bash
cargo install somnia-cli # from crates.io
brew tap vbasky/somnia && brew install somnia # Homebrew (macOS / Linux)
```
Then:
```bash
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 / pending
```
Connection settings are read from flags or environment variables (`--help` for
the full list).
## Status
`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](https://github.com/vbasky/somnia/blob/main/ROADMAP.md) 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](https://github.com/vbasky/somnia/blob/main/LICENSE).
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.