Crate microrm

Crate microrm 

Source
Expand description

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

let (cpool, schema) = microrm::ConnectionPool::open::<KVSchema>(":memory:")?;
let mut txn = cpool.start()?;
schema.kvs.insert(&mut txn, 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 txn)?.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 txn)?.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 txn)?.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 txn)?
        .into_iter()
        .map(|v| v.wrapped().value).collect::<Vec<_>>(),
    vec!["example_value".to_string()]);

// if we're done with the transaction, commit it.
txn.commit()?;
§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(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, schema) = microrm::ConnectionPool::open::<ECommerceDB>(":memory:")?;
let mut txn = cpool.start()?;

// add an example product
let widget1 = schema.products.insert_and_return(&mut txn, 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 txn, 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 txn, 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 txn, 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 txn, widget1.id())?;
order1.customer.connect_to(&mut txn, customer1.id())?;

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

// commit the transaction
txn.commit()?;

Re-exports§

pub use db::ConnectionPool;
pub use db::Transaction;
pub use schema::index::Index;
pub use schema::relation::Relation;
pub use schema::relation::RelationDomain;
pub use schema::relation::RelationMap;
pub use schema::relation::RelationRange;
pub use schema::IDMap;
pub use schema::Serialized;
pub use schema::Stored;

Modules§

_book
Non-API documentation for microrm.
cli
This module provides autogeneration of clap commands to manipulate entities that are stored in a microrm database.
db
SQLite database interaction functions.
prelude
Re-exported traits and macros for easy access.
query
Database query interface.
schema
Schema specification types.

Enums§

Error
microrm error type, returned from most microrm methods.

Type Aliases§

DBResult
Shorthand alias.