microrm 0.6.3

Lightweight ORM using sqlite as a backend
Documentation
//! 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](https://www.sea-ql.org/SeaORM/) or [Diesel](https://diesel.rs/). The major limitations
//! of microrm are clumsy migrations 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:
//! - Schema modelling (mostly by the [`Datum`](schema/datum/trait.Datum.html) and
//!   [`Entity`](schema/entity/trait.Entity.html) traits)
//! - Database querying (via [`query::Queryable`], [`query::RelationInterface`] and
//!   [`query::Insertable`] traits)
//! - Command-line interface generation via the [`clap`](https://docs.rs/clap/latest/clap/) crate
//!   (see [`cli::Autogenerate`] and [`cli::EntityInterface`]; requires the optional crate feature `clap`)
//!
//! microrm pushes the Rust type system somewhat to provide better ergonomics, so the MSRV is
//! currently 1.82. 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.
//!
//! ### A note on async support
//!
//! microrm does not currently support dedicated SQLite threads, something done by e.g. Python's
//! [aiosqlite](https://pypi.org/project/aiosqlite/). Instead, all SQLite processing is done
//! on-thread. Whether this will mix well with single-threaded async runtimes under heavy load is
//! application-dependent; in particular, running in an environment where I/O with the SQLite
//! database can hang (e.g. over NFS) this may result in stalling the worker thread.
//!
//! ### Book
//!
//! The [`_book`] module contains some extra tutorial-like documentation that may be of interest
//! (requires the `_book` feature).
//!
//! ### 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>,
//! }
//!
//! # fn main() -> Result<(), microrm::Error> {
//! 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()?;
//!
//! # Ok(())
//! # }
//! ```
//!
//! #### 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.
//!
//! ```rust
//! 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>,
//! }
//! # fn main() -> Result<(), microrm::Error> {
//! // 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()?;
//!
//! # Ok(())
//! # }
//! ```

#![warn(missing_docs)]
// this requires clippy 1.78
#![warn(clippy::empty_docs)]

// to make the proc_macros work inside the microrm crate; needed for tests and the metaschema.
extern crate self as microrm;

// add documentation book to docs.rs, but don't bother during normal builds ---
// unless specifically requested.
#[cfg(any(docsrs, feature = "_book"))]
#[doc = include_str!("../book.md")]
pub mod _book {}

/// SQLite database interaction functions.
pub mod db;
pub mod query;
pub mod schema;

mod glue;

// re-exports
pub use db::{ConnectionPool, Transaction};
pub use schema::index::{SearchIndex, UniqueIndex};
pub use schema::relation::{Relation, RelationDomain, RelationMap, RelationRange};
pub use schema::{IDMap, Serialized, Stored};

#[cfg(feature = "clap")]
pub mod cli;

/// Re-exported traits and macros for easy access.
pub mod prelude {
    pub use crate::query::{Insertable, Queryable, RelationInterface};
    pub use crate::schema::{relation::Relation, Schema, Serializable};
    pub use microrm_macros::{index_cols, Entity, Schema, Value};
}

// ----------------------------------------------------------------------
// Generically-useful database types
// ----------------------------------------------------------------------

/// microrm error type, returned from most microrm methods.
#[derive(Debug)]
pub enum Error {
    /// No result was present where one was expected.
    EmptyResult,
    /// Stored value encountered that is incompatible with the schema.
    UnknownValue(String),
    /// Schema mismatch between on-disk database and current schema.
    IncompatibleSchema,
    /// Internal error occured, likely is a bug in microrm or very unexpected behaviour.
    InternalError(&'static str),
    /// Non-UTF8 data was encountered somewhere that UTF8 data was expected.
    EncodingError(std::str::Utf8Error),
    /// Primitive conversion failed
    OutOfRange(std::num::TryFromIntError, &'static str),
    /// User error in API usage.
    LogicError(&'static str),
    /// Attempted to insert an entity with values that violate a database constraint, probably
    /// due to a uniqueness requirement.
    ConstraintViolation(String),
    /// Sqlite internal error that has not been translated into a more specific error.
    Sqlite {
        /// SQLite error code.
        code: i32,
        /// SQLite extended error code
        extended_code: i32,
        /// SQLite's error message for the error.
        msg: String,
        /// The SQL that triggered the error, if known.
        sql: Option<String>,
    },
    /// Something strange happened with JSON serialization/deserialization.
    JSON(serde_json::Error),
    /// One of the mutexes failed in a spectacular way.
    LockError(String),
    /// An empty database was found when an existing one was expected.
    EmptyDatabase,
    /// The database schema is in an inconsistent state, probably either from corruption or an
    /// incomplete migration.
    ConsistencyError(String),
    /// The transaction aborted unexpectedly.
    TransactionAbort,
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        <Self as std::fmt::Debug>::fmt(self, f)
    }
}

impl<T> From<std::sync::PoisonError<T>> for Error {
    fn from(value: std::sync::PoisonError<T>) -> Self {
        Self::LockError(value.to_string())
    }
}

impl From<std::ffi::NulError> for Error {
    fn from(_value: std::ffi::NulError) -> Self {
        Self::InternalError("NULL pointer encountered in unexpected location")
    }
}

impl From<std::str::Utf8Error> for Error {
    fn from(value: std::str::Utf8Error) -> Self {
        Self::EncodingError(value)
    }
}

impl From<std::num::TryFromIntError> for Error {
    fn from(value: std::num::TryFromIntError) -> Self {
        Self::OutOfRange(value, "(no context)")
    }
}

impl std::error::Error for Error {}

/// Shorthand alias.
pub type DBResult<T> = Result<T, Error>;