22 releases

0.4.4 Oct 2, 2024
0.4.2 Sep 24, 2024
0.4.0-rc.2 Apr 14, 2024
0.3.12 Nov 23, 2023
0.3.5 Jul 29, 2022

#1684 in Database interfaces

Download history 156/week @ 2024-07-24 41/week @ 2024-07-31 258/week @ 2024-08-28 48/week @ 2024-09-04 24/week @ 2024-09-11 131/week @ 2024-09-18 81/week @ 2024-09-25 326/week @ 2024-10-02 19/week @ 2024-10-09

353 downloads per month

BSD-4-Clause

185KB
4.5K SLoC

docs.rs

microrm is a simple object relational manager (ORM) for sqlite.

Unlike many fancier ORM systems, microrm is designed to be lightweight, both in terms of runtime overhead and developer LoC. By necessity, it sacrifices flexibility towards these goals, and so can be thought of as more opinionated than, say, SeaORM or Diesel. Major limitations of microrm are:

  • lack of database migration support
  • limited vocabulary for describing object-to-object relations

microrm pushes the Rust type system somewhat to provide better ergonomics, so the MSRV is currently 1.75. Don't be scared off by the web of traits in the schema module --- you should never need to interact with any of them!


lib.rs:

microrm is a simple object relational manager (ORM) for sqlite.

Unlike many fancier ORM systems, microrm is designed to be lightweight, both in terms of runtime overhead and developer LoC. By necessity, it sacrifices flexibility towards these goals, and so can be thought of as more opinionated than, say, SeaORM or Diesel. Major limitations of microrm are:

  • lack of database migration support
  • limited vocabulary for describing object-to-object relations

There are three externally-facing components in microrm:

microrm pushes the Rust type system somewhat to provide better ergonomics, so the MSRV is currently 1.75. Don't be scared off by the web of traits in the schema module --- you should never need to interact with any of them!

Examples

KV-store

For the simplest kind of database schema, a key-value store, one possible microrm implementation of it might look like the following:

use microrm::prelude::*;

#[derive(Entity)]
struct KVEntry {
    #[key]
    key: String,
    value: String,
}

#[derive(Database)]
struct KVDB {
    kvs: microrm::IDMap<KVEntry>,
}

let db = KVDB::open_path(":memory:")?;
db.kvs.insert(KVEntry {
    key: "example_key".to_string(),
    value: "example_value".to_string()
})?;

// can get with a String reference
assert_eq!(
    db.kvs.keyed(&String::from("example_key")).get()?.map(|v| v.value.clone()),
    Some("example_value".to_string()));
// thanks to the QueryEquivalent trait, we can also just use a plain &str
assert_eq!(
    db.kvs.keyed("example_key").get()?.map(|v| v.value.clone()),
    Some("example_value".to_string()));

// obviously, if we get another KV entry with a missing key, it doesn't come back...
assert_eq!(db.kvs.keyed("another_example_key").get()?.is_some(), false);

// note that the above all return an Option<Stored<T>>. when using filters on arbitrary
// columns, a Vec<Stored<T>> is returned:
assert_eq!(
    db
        .kvs
        // note that the column constant uses CamelCase
        .with(KVEntry::Value, "example_value")
        .get()?
        .into_iter()
        .map(|v| v.wrapped().value).collect::<Vec<_>>(),
    vec!["example_value".to_string()]);

Simple e-commerce schema

The following is an example of what a simple e-commerce website's schema might look like, tracking products, users, and orders.

use microrm::prelude::*;

#[derive(Entity)]
pub struct ProductImage {
    // note that because this references an entity's autogenerated ID type,
    // this is a foreign key. if the linked product is deleted, the linked
    // ProductImages will also be deleted.
    pub product: ProductID,
    pub img_data: Vec<u8>,
}

#[derive(Entity, Clone)]
pub struct Product {
    #[key]
    pub title: String,
    pub longform_body: String,
    pub images: microrm::RelationMap<ProductImage>,
    pub cost: f64,
}

// define a relation between customers and orders
pub struct CustomerOrders;
impl microrm::Relation for CustomerOrders {
    type Domain = Customer;
    type Range = Order;
    const NAME: &'static str = "CustomerOrders";
    // at most one customer per order
    const INJECTIVE: bool = true;
}

#[derive(Entity)]
pub struct Customer {
    pub orders: microrm::RelationDomain<CustomerOrders>,

    #[key]
    pub email: String,

    #[unique]
    pub legal_name: String,

    #[elide]
    pub password_salt: String,
    #[elide]
    pub password_hash: String,

}

#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub enum OrderState {
    AwaitingPayment,
    PaymentReceived { confirmation: String },
    ProductsReserved,
    Shipped { tracking_no: String },
    OnHold { reason: String },
}

#[derive(Entity)]
pub struct Order {
    pub order_state: microrm::Serialized<Vec<OrderState>>,
    pub customer: microrm::RelationRange<CustomerOrders>,
    pub shipping_address: String,

    pub billing_address: Option<String>,

    // we'll assume for now that there's no product multiplicities, i.e. this is not a multiset
    pub contents: microrm::RelationMap<Product>,
}

#[derive(Database)]
pub struct ECommerceDB {
    pub products: microrm::IDMap<Product>,
    pub customers: microrm::IDMap<Customer>,
    pub orders: microrm::IDMap<Order>,
}
// open a database instance
let db = ECommerceDB::open_path(":memory:")?;

// add an example product
let widget1 = db.products.insert_and_return(Product {
    title: "Widget Title Here".into(),
    longform_body: "The first kind of widget that WidgetCo produces.".into(),
    cost: 100.98,
    images: Default::default()
})?;

// add an image for the product
widget1.images.insert(ProductImage {
    product: widget1.id(),
    img_data: [/* image data goes here */].into(),
});

// sign us up for this most excellent ecommerce website
let customer1 = db.customers.insert_and_return(Customer {
    email: "your@email.here".into(),
    legal_name: "Douglas Adams".into(),
    password_salt: "pepper".into(),
    password_hash: "browns".into(),

    orders: Default::default(),
})?;

// put in an order for the widget!
let mut order1 = db.orders.insert_and_return(Order {
    order_state: vec![OrderState::AwaitingPayment].into(),
    customer: Default::default(),
    shipping_address: "North Pole, Canada, H0H0H0".into(),
    billing_address: None,
    contents: Default::default(),
})?;
order1.contents.connect_to(widget1.id())?;
order1.customer.connect_to(customer1.id())?;

// Now get all products that customer1 has ever ordered
let all_ordered = customer1.orders.join(Order::Contents).get()?;
assert_eq!(all_ordered, vec![widget1]);

// process the payment for our order by updating the entity
order1.order_state.as_mut().push(
    OrderState::PaymentReceived {
        confirmation: "money received in full, i promise".into()
    }
);

// now synchronize the entity changes back into the database
order1.sync()?;

Dependencies

~23MB
~455K SLoC