SeaORM Async ORM Integration

Persist and drive statecharts through SeaORM (15M+ downloads), the leading async, dynamic ORM for Rust built on SQLx.


🎯 Architecture: Statecharts as Entity ActiveModels

flowchart LR
    E[SeaORM Entity / Model] -->|Extract Snapshot JSON| S[Snapshot v4]
    S -->|Restore Configuration| I[SyncInterpreter]
    I -->|send event| R[Receipt]
    I -->|Serialize New Snapshot| S2[Updated Snapshot]
    S2 -->|Update state & snapshot fields| AM[ActiveModel]
    AM -->|save db transaction| DB[(PostgreSQL / MySQL / SQLite)]

In SeaORM applications, you store the high-level leaf state in a state: String column (for indexed SQL querying) and the full statechart execution configuration in a snapshot: Json column (to preserve parallel states, history, timers, and context).


📦 Dependencies

[dependencies]
sea-orm = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
xstate-statemachine = "0.1"

🛠️ Implementation: SeaORM Entity with Statechart Trait

1. Define the SeaORM Entity

#![allow(unused)]
fn main() {
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "orders")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub order_ref: String,
    pub state: String,
    pub snapshot: Json,
    pub total_cents: i64,
    pub updated_at: DateTimeUtc,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
}

2. Statechart Transition Extension Trait

#![allow(unused)]
fn main() {
use sea_orm::{DatabaseConnection, Set, ActiveModelTrait};
use xstate_statemachine::prelude::*;

pub trait StatechartModel {
    async fn send_event(
        self,
        db: &DatabaseConnection,
        machine: &Machine,
        logic: &MachineLogic,
        event: impl Into<Event>,
    ) -> Result<Model, Box<dyn std::error::Error>>;
}

impl StatechartModel for Model {
    async fn send_event(
        self,
        db: &DatabaseConnection,
        machine: &Machine,
        logic: &MachineLogic,
        event: impl Into<Event>,
    ) -> Result<Model, Box<dyn std::error::Error>> {
        // 1. Restore interpreter from entity's snapshot JSON
        let snapshot: Snapshot = serde_json::from_value(self.snapshot)?;
        let mut interp = SyncInterpreter::from_snapshot(machine.clone(), logic.clone(), snapshot)?;

        // 2. Drive the state transition
        let receipt = interp.send(event)?;
        if receipt.unhandled {
            return Err(format!("Event was unhandled in state: {}", self.state).into());
        }

        // 3. Serialize updated snapshot and primary leaf state
        let new_snapshot = interp.snapshot()?;
        let leaf_state = interp.state_ids().first().cloned().unwrap_or_default();

        // 4. Update the SeaORM ActiveModel
        let mut active: ActiveModel = self.into();
        active.state = Set(leaf_state.to_string());
        active.snapshot = Set(serde_json::to_value(new_snapshot)?);
        active.updated_at = Set(chrono::Utc::now());

        let updated_model = active.update(db).await?;
        Ok(updated_model)
    }
}
}

🚀 Driving Transitions in SeaORM

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("postgres://xsm:xsm@localhost/shop").await?;
    let machine = Machine::from_json(ORDER_JSON)?;
    let logic = MachineLogic::dynamic();

    // Find order by ID
    let order = Entity::find_by_id(42)
        .one(&db)
        .await?
        .ok_or("Order not found")?;

    println!("Current order state: {}", order.state);

    // Apply event via statechart extension
    let updated_order = order.send_event(&db, &machine, &logic, "CONFIRM_PAYMENT").await?;

    println!("New order state: {}", updated_order.state);
    Ok(())
}