24 releases

0.5.0-rc.2 Nov 12, 2024
0.4.4 Oct 2, 2024
0.4.0-rc.2 Apr 14, 2024
0.3.12 Nov 23, 2023
0.3.5 Jul 29, 2022

#1705 in Database interfaces

Download history 224/week @ 2024-08-27 82/week @ 2024-09-03 22/week @ 2024-09-10 17/week @ 2024-09-17 189/week @ 2024-09-24 333/week @ 2024-10-01 20/week @ 2024-10-08 75/week @ 2024-11-05 142/week @ 2024-11-12 12/week @ 2024-11-19

229 downloads per month

BSD-4-Clause

210KB
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. The major limitations of microrm are lack of migrations (though support is planned!) and somewhat limited vocabulary for describing object-to-object relations. Despite this, microrm is usually powerful enough for most usecases where sqlite is appropriate.

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 most of them unless you're doing schema reflection.

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(Default, Schema)]
struct KVSchema {
    kvs: microrm::IDMap<KVEntry>,
}

let cpool = microrm::ConnectionPool::new(":memory:")?;
let mut lease = cpool.acquire()?;
let schema = KVSchema::default();
schema.install(&mut lease)?;
schema.kvs.insert(&mut lease, KVEntry {
    key: "example_key".to_string(),
    value: "example_value".to_string()
})?;

// can get with a String reference
assert_eq!(
    schema.kvs.keyed(&String::from("example_key")).get(&mut lease)?.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!(
    schema.kvs.keyed("example_key").get(&mut lease)?.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!(schema.kvs.keyed("another_example_key").get(&mut lease)?.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!(
    schema
        .kvs
        // note that the column constant uses CamelCase
        .with(KVEntry::Value, "example_value")
        .get(&mut lease)?
        .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>,
    // mark as part of the primary key
    #[key]
    pub email: String,
    // enforce uniqueness of legal names
    #[unique]
    pub legal_name: String,

    // elide the secrets from Debug output
    #[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 {
    // use an ordinary type and do transparent JSON de/serialization
    pub order_state: microrm::Serialized<Vec<OrderState>>,
    pub customer: microrm::RelationRange<CustomerOrders>,
    pub shipping_address: String,

    // we may not have a billing address
    pub billing_address: Option<String>,

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

#[derive(Default, Schema)]
pub struct ECommerceDB {
    pub products: microrm::IDMap<Product>,
    pub customers: microrm::IDMap<Customer>,
    pub orders: microrm::IDMap<Order>,
}
// open a database instance
let cpool = microrm::ConnectionPool::new(":memory:")?;
let mut lease = cpool.acquire()?;
let schema = ECommerceDB::default();
schema.install(&mut lease);

// add an example product
let widget1 = schema.products.insert_and_return(&mut lease, 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(&mut lease, ProductImage {
    product: widget1.id(),
    img_data: [/* image data goes here */].into(),
});

// sign us up for this most excellent ecommerce website
let customer1 = schema.customers.insert_and_return(&mut lease, 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 = schema.orders.insert_and_return(&mut lease, 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(&mut lease, widget1.id())?;
order1.customer.connect_to(&mut lease, customer1.id())?;

// Now get all products that customer1 has ever ordered
let all_ordered = customer1.orders.join(Order::Contents).get(&mut lease)?;
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(&mut lease)?;

Dependencies

~24MB
~468K SLoC