Selaa lähdekoodia

Expand documentation somewhat.

Kestrel 7 kuukautta sitten
vanhempi
commit
4711b79330
5 muutettua tiedostoa jossa 192 lisäystä ja 117 poistoa
  1. 12 115
      README.md
  2. 1 0
      microrm/Cargo.toml
  3. 137 1
      microrm/src/lib.rs
  4. 1 1
      microrm/src/query.rs
  5. 41 0
      microrm/src/schema/datum/datum_common.rs

+ 12 - 115
README.md

@@ -1,118 +1,15 @@
-`microrm` is a crate providing a lightweight ORM on top of SQLite.
+![docs.rs](https://img.shields.io/docsrs/microrm)
 
-Unlike fancier ORM systems, microrm is intended to be extremely lightweight
-and code-light, which means that by necessity it is opinionated, and thus
-lacks the power and flexibility of, say, SeaORM or Diesel. In particular,
-`microrm` currently makes no attempts to provide database migration support.
+microrm is a simple object relational manager (ORM) for sqlite.
 
-`microrm` provides two components: modeling and querying. The intention is
-that the modelling is built statically; dynamic models are not directly
-supported though are possible. However, since by design microrm does not
-touch database contents for tables not defined in its model, using raw SQL
-for any needed dynamic components may be a better choice.
+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/). Major limitations of microrm are:
+- lack of database migration support
+- limited vocabulary for describing object-to-object relations
 
-Querying supports a small subset of SQL expressed as type composition; see
-[`QueryInterface`](query/struct.QueryInterface.html) for more details.
-
-A simple example using an SQLite table as an (indexed) key/value store
-might look something like this:
-```rust
-use microrm::prelude::*;
-use microrm::{Entity,make_index};
-#[derive(Debug,Entity,serde::Serialize,serde::Deserialize)]
-pub struct KVStore {
-    pub key: String,
-    pub value: String
-}
-
-// the !KVStoreIndex here means a type representing a unique index named KVStoreIndex
-make_index!(!KVStoreIndex, KVStore::Key);
-
-let schema = microrm::Schema::new()
-    .entity::<KVStore>()
-    .index::<KVStoreIndex>();
-
-// dump the schema in case you want to inspect it manually
-for create_sql in schema.create() {
-    println!("{};", create_sql);
-}
-
-let db = microrm::DB::new_in_memory(schema).unwrap();
-let qi = db.query_interface();
-
-qi.add(&KVStore {
-    key: "a_key".to_string(),
-    value: "a_value".to_string()
-});
-
-// because KVStoreIndex indexes key, this is a logarithmic lookup
-let qr = qi.get().by(KVStore::Key, "a_key").one().expect("No errors encountered");
-
-assert_eq!(qr.is_some(), true);
-assert_eq!(qr.as_ref().unwrap().key, "a_key");
-assert_eq!(qr.as_ref().unwrap().value, "a_value");
-```
-
-The schema output from the loop is (details subject to change based on internals):
-```sql
-CREATE TABLE IF NOT EXISTS "kv_store" (id integer primary key,"key" text,"value" text);
-CREATE UNIQUE INDEX "kv_store_index" ON "kv_store" ("key");
-```
-
-If you're using `microrm` in a threaded or async environment, you'll need to
-use a [`DBPool`](struct.DBPool.html). You can then write code like this:
-
-```rust
-# use microrm::prelude::*;
-# use microrm::{Entity,make_index};
-# #[derive(Debug,Entity,serde::Serialize,serde::Deserialize)]
-# pub struct KVStore {
-    # pub key: String,
-    # pub value: String
-# }
-
-async fn insert_a(dbp: &microrm::DBPool<'_>) {
-    let qi = dbp.query_interface();
-    qi.add(&KVStore {
-        key: "a_key".to_string(),
-        value: "a_value".to_string()
-    });
-}
-
-async fn insert_b(dbp: &microrm::DBPool<'_>) {
-    let qi = dbp.query_interface();
-    qi.add(&KVStore {
-        key: "b_key".to_string(),
-        value: "b_value".to_string()
-    });
-}
-
-# async_std::task::block_on(async { main().await });
-// running in your favourite async runtime
-async fn main() {
-    # let schema = microrm::Schema::new().entity::<KVStore>();
-    let db = microrm::DB::new_in_memory(schema).unwrap();
-    let dbp = microrm::DBPool::new(&db);
-
-    let a = insert_a(&dbp);
-    let b = insert_b(&dbp);
-
-    b.await;
-    a.await;
-
-    let qi = dbp.query_interface();
-
-    let qr = qi.get().by(KVStore::Key, "a_key").one().unwrap();
-    assert_eq!(qr.is_some(), true);
-    assert_eq!(qr.as_ref().unwrap().key, "a_key");
-    assert_eq!(qr.as_ref().unwrap().value, "a_value");
-
-    let qr = qi.get().by(KVStore::Key, "b_key").one().unwrap();
-    assert_eq!(qr.is_some(), true);
-    assert_eq!(qr.as_ref().unwrap().key, "b_key");
-    assert_eq!(qr.as_ref().unwrap().value, "b_value");
-}
-```
-
-Note that between acquiring a [`QueryInterface`] reference and dropping it, you
-must not `.await` anything; the compiler will (appropriately) complain.
+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!

+ 1 - 0
microrm/Cargo.toml

@@ -30,6 +30,7 @@ clap = { version = "4", optional = true }
 [dev-dependencies]
 test-log = "0.2.15"
 
+serde = { version = "1.0", features = ["derive"] }
 clap = { version = "4", features = ["derive"] }
 # criterion = "0.5"
 # rand = "0.8.5"

+ 137 - 1
microrm/src/lib.rs

@@ -22,7 +22,7 @@
 //! 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:
 //!
@@ -74,7 +74,142 @@
 //!
 //! # 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,
+//! }
+//!
+//! 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
+//!     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>,
+//! }
+//! # fn main() -> Result<(), microrm::Error> {
+//! // 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, promise".into()
+//!     }
+//! );
+//!
+//! // now synchronize the entity changes back into the database
+//! order1.sync()?;
+//!
+//! # Ok(())
+//! # }
 //! ```
 
 #![warn(missing_docs)]
@@ -90,6 +225,7 @@ pub mod db;
 mod query;
 pub mod schema;
 
+pub use schema::index::Index;
 pub use schema::relation::{Relation, RelationDomain, RelationMap, RelationRange};
 pub use schema::{IDMap, Serialized, Stored};
 

+ 1 - 1
microrm/src/query.rs

@@ -142,7 +142,7 @@ pub(crate) fn update_entity<E: Entity>(conn: &Connection, value: &Stored<E>) ->
 
             E::accept_part_visitor(&mut PartNameVisitor(&mut set_columns, Default::default()));
             format!(
-                "UPDATE {entity_name} SET {set_columns} WHERE `id` = ?",
+                "UPDATE `{entity_name}` SET {set_columns} WHERE `id` = ?",
                 entity_name = E::entity_name()
             )
         },

+ 41 - 0
microrm/src/schema/datum/datum_common.rs

@@ -233,6 +233,47 @@ impl<'l> Datum for &'l [u8] {
 // a byte slice and an owned byte slice are equivalent for the purposes of a query
 impl<'l> QueryEquivalent<Vec<u8>> for &'l [u8] {}
 
+impl Datum for f32 {
+    fn sql_type() -> &'static str {
+        "numeric"
+    }
+
+    fn build_from(_adata: RelationData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        let val = stmt.read(*index)?;
+        *index += 1;
+        Ok(val)
+    }
+
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+        stmt.bind(index, *self).expect("couldn't bind f32");
+    }
+}
+impl ConcreteDatum for f32 {}
+
+impl Datum for f64 {
+    fn sql_type() -> &'static str {
+        "numeric"
+    }
+
+    fn build_from(_adata: RelationData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
+    where
+        Self: Sized,
+    {
+        let val = stmt.read(*index)?;
+        *index += 1;
+        Ok(val)
+    }
+
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+        stmt.bind(index, *self).expect("couldn't bind f64");
+    }
+}
+impl ConcreteDatum for f64 {}
+
+// Generic Datum implementation for references to a Datum
 impl<'l, T: Datum> Datum for &'l T {
     fn sql_type() -> &'static str {
         T::sql_type()