Diesel Compile-Time ORM Integration
Type-safe statechart persistence with Diesel (30M+ downloads), Rustโs leading compile-time ORM and query builder.
๐ฏ Architecture
flowchart LR
D[Diesel Connection] -->|SELECT * FROM orders WHERE id = $1| Row[Database Row]
Row -->|Deserialize snapshot column| S[Snapshot v4]
S -->|Restore interpreter| I[SyncInterpreter]
I -->|send event| R[Receipt]
I -->|Snapshot update| S2[Updated Snapshot]
S2 -->|diesel::update(orders).set(...)| D
Diesel gives compile-time SQL safety. By creating a custom SQL type mapping for Snapshot, statechart updates become transactional Rust operations executed without allocations.
๐ฆ Dependencies
[dependencies]
diesel = { version = "2.2", features = ["postgres", "chrono", "serde_json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
xstate-statemachine = "0.1"
๐ ๏ธ Schema & Model Setup
1. Diesel Schema (schema.rs)
#![allow(unused)]
fn main() {
diesel::table! {
orders (id) {
id -> Int4,
order_ref -> Varchar,
current_state -> Varchar,
snapshot_data -> Jsonb,
updated_at -> Timestamptz,
}
}
}
2. Diesel Models
#![allow(unused)]
fn main() {
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use serde_json::Value;
#[derive(Queryable, Selectable, Identifiable, Debug)]
#[diesel(table_name = crate::schema::orders)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Order {
pub id: i32,
pub order_ref: String,
pub current_state: String,
pub snapshot_data: Value,
pub updated_at: DateTime<Utc>,
}
#[derive(AsChangeset)]
#[diesel(table_name = crate::schema::orders)]
pub struct OrderUpdate<'a> {
pub current_state: &'a str,
pub snapshot_data: &'a Value,
pub updated_at: DateTime<Utc>,
}
}
๐ Transactional State Transition in Diesel
#![allow(unused)]
fn main() {
use diesel::prelude::*;
use xstate_statemachine::prelude::*;
pub fn transition_order(
conn: &mut PgConnection,
target_order_id: i32,
machine: &Machine,
logic: &MachineLogic,
event: &str,
) -> Result<Order, Box<dyn std::error::Error>> {
use crate::schema::orders::dsl::*;
conn.transaction::<Order, Box<dyn std::error::Error>, _>(|conn| {
// 1. Fetch order with row-level lock (FOR UPDATE)
let order = orders
.find(target_order_id)
.for_update()
.first::<Order>(conn)?;
// 2. Restore statechart snapshot
let snapshot: Snapshot = serde_json::from_value(order.snapshot_data)?;
let mut interp = SyncInterpreter::from_snapshot(machine.clone(), logic.clone(), snapshot)?;
// 3. Process event
let receipt = interp.send(event)?;
if receipt.unhandled {
return Err(format!("Event '{}' rejected in state '{}'", event, order.current_state).into());
}
// 4. Serialize new snapshot
let new_snapshot = interp.snapshot()?;
let new_state = interp.state_ids().first().cloned().unwrap_or_default();
let new_snapshot_json = serde_json::to_value(&new_snapshot)?;
// 5. Update database record
let updated = diesel::update(orders.find(target_order_id))
.set(&OrderUpdate {
current_state: &new_state,
snapshot_data: &new_snapshot_json,
updated_at: Utc::now(),
})
.get_result::<Order>(conn)?;
Ok(updated)
})
}
}