Skip to content

vbasky/somnia

Repository files navigation

somnia

somnia — type-safe SurrealDB ORM for Rust

crates.io docs.rs CI MSRV license

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"

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 buildingPost::table().select(...).filter(Post::title().eq("hello"))
  • #[derive(SurrealRecord)] — typed column accessors, table metadata, and schema DDL generated from the struct.
  • Schema as codeup() / 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

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

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:

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:

migrations/
  2025-01-01-000000_create_posts/
    up.surql
    down.surql
  2025-01-01-000100_seed_defaults/
    up.surql
    down.surql
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

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).

CLI

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 / 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 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.

About

No description or website provided.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors