Kestrel 1 долоо хоног өмнө
parent
commit
95c0e5c9b3

+ 6 - 5
README.md

@@ -1,15 +1,16 @@
 ![docs.rs](https://img.shields.io/docsrs/microrm)
 
-microrm is a simple object relational manager (ORM) for sqlite.
+microrm is a simple object relational manager (ORM) for sqlite that requires no
+external tooling, keeping all specifications in natural Rust types with no DSLs.
 
 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
+- slightly clumsy 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!
+microrm pushes the Rust type system somewhat, 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!

+ 10 - 5
microrm/Cargo.toml

@@ -6,6 +6,7 @@ license = "BSD-4-Clause"
 authors = ["Kestrel <kestrel@flying-kestrel.ca>"]
 repository = "https://git.flying-kestrel.ca/kestrel/microrm"
 description = "Lightweight ORM using sqlite as a backend."
+rust-version = "1.75"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
@@ -16,22 +17,26 @@ all-features = true
 [features]
 clap = ["dep:clap"]
 bundled_sqlite = ["libsqlite3-sys/bundled"]
+time = ["dep:time"]
 
 [dependencies]
+# Core data-processing dependencies
 libsqlite3-sys = "0.28"
 serde = { version = "1.0" }
 serde_json = { version = "1.0" }
-time = "0.3"
-itertools = "0.12"
-thread_local = "1.1"
 sha2 = "0.10"
+
+# Implementation helpers
 lazy_static = "1.5"
 
+# UX
 microrm-macros = { path = "../microrm-macros", version = "0.5.0-dev" }
-log = "0.4.17"
-
+log = { version = "0.4" }
 clap = { version = "4", optional = true }
 
+# Interop support
+time = { version = "0.3", optional = true }
+
 [dev-dependencies]
 test-log = "0.2.15"
 

+ 2 - 3
microrm/src/db.rs

@@ -245,7 +245,7 @@ impl ConnectionPoolData {
 
         Ok(())
     }
-    fn acquire(self: &Arc<Self>) -> Result<ConnectionLease, Error> {
+    fn acquire<'a>(self: &'a Arc<Self>) -> Result<ConnectionLease<'a>, Error> {
         let mut alock = self.available.lock()?;
 
         while alock.is_empty() {
@@ -437,8 +437,7 @@ pub(crate) fn get_raw_schema(lease: &mut ConnectionLease) -> DBResult<Vec<RawSch
     unsafe {
         let sql = "SELECT * FROM sqlite_schema";
         let c_sql = CString::new(sql)?;
-        let entries_mut = &mut schema_entries;
-        let entries_ptr = std::ptr::from_mut(entries_mut);
+        let entries_ptr = (&mut schema_entries) as *mut Vec<RawSchemaRow>;
         check_rcode(
             || Some(sql),
             sq::sqlite3_exec(

+ 0 - 2
microrm/src/query.rs

@@ -2,8 +2,6 @@
 //!
 //!
 
-use itertools::Itertools;
-
 use crate::{
     db::{ConnectionLease, StatementContext, Transaction},
     schema::{

+ 25 - 17
microrm/src/schema/datum/datum_common.rs

@@ -1,7 +1,7 @@
 use crate::{
     db::{self, Bindable, StatementContext, StatementRow},
     schema::{datum::Datum, relation::RelationData},
-    DBResult, Error,
+    DBResult,
 };
 
 use super::{
@@ -21,25 +21,33 @@ impl<'l> Datum for StringQuery<'l> {
 // impl<'l, 'b, T: ConcreteDatum> QueryEquivalent<StringQuery<'l>> for &'b T {}
 impl<'l, T: ConcreteDatum> QueryEquivalent<T> for StringQuery<'l> {}
 
-impl ConcreteDatum for time::OffsetDateTime {}
-impl Datum for time::OffsetDateTime {
-    fn sql_type() -> &'static str {
-        "text"
-    }
+#[cfg(feature = "time")]
+const _: () = {
+    use crate::Error;
+    impl ConcreteDatum for time::OffsetDateTime {}
+    impl Datum for time::OffsetDateTime {
+        fn sql_type() -> &'static str {
+            "text"
+        }
 
-    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
-        let ts = self.unix_timestamp();
-        ts.bind_to(stmt, index)
-    }
+        fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+            let ts = self.unix_timestamp();
+            ts.bind_to(stmt, index)
+        }
 
-    fn build_from(rdata: RelationData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
-    where
-        Self: Sized,
-    {
-        let unix = i64::build_from(rdata, stmt, index)?;
-        Self::from_unix_timestamp(unix).map_err(|e| Error::UnknownValue(e.to_string()))
+        fn build_from(
+            rdata: RelationData,
+            stmt: &mut StatementRow,
+            index: &mut i32,
+        ) -> DBResult<Self>
+        where
+            Self: Sized,
+        {
+            let unix = i64::build_from(rdata, stmt, index)?;
+            Self::from_unix_timestamp(unix).map_err(|e| Error::UnknownValue(e.to_string()))
+        }
     }
-}
+};
 
 impl ConcreteDatum for String {}
 impl Datum for String {

+ 0 - 164
microrm/src/schema/migrate.rs

@@ -1,164 +0,0 @@
-// this is overcomplicated.
-// just give a function that has access to each entity type and allow it to copy whatever it likes?
-
-#![allow(missing_docs)]
-
-use std::collections::HashSet;
-
-use crate::{
-    db::ConnectionLease,
-    query::Queryable,
-    schema::Schema,
-    DBResult,
-};
-
-mod new;
-
-pub use new::New;
-use new::WrapSchema;
-
-use super::{
-    build::{collect_from_database, DatabaseSchema},
-    meta::MetadataDB,
-};
-
-pub trait Migration: 'static + Default {
-    type OldSchema: Schema + Default;
-    type NewSchema: Schema + Default;
-}
-
-pub trait MigrationRef {
-    // fn with_new_schema(&self, changed: HashSet<String>) -> Box<&dyn Schema>;
-    fn collect_intermediate_schema(&self, towrap: HashSet<String>) -> Box<&dyn Schema>;
-}
-impl<M: Migration> MigrationRef for M {
-    fn collect_intermediate_schema(&self, towrap: HashSet<String>) -> Box<&dyn Schema> {
-        let wrap_schema = WrapSchema::<M::NewSchema>::new(M::NewSchema::default(), towrap);
-
-        println!("collection: {:?}", collect_from_database(&wrap_schema));
-        todo!()
-    }
-}
-
-struct MigrationEntry {
-    old_schema: DatabaseSchema,
-    old_signature: String,
-    new_schema: DatabaseSchema,
-    new_signature: String,
-    migration: Box<dyn MigrationRef>,
-}
-
-#[derive(microrm_macros::Schema, Default)]
-struct EmptySchema {}
-
-pub struct Migrator {
-    base_schema: DatabaseSchema,
-    migrations: Vec<MigrationEntry>,
-}
-
-impl Migrator {
-    pub fn new<Base: Schema + Default>() -> Self {
-        Self {
-            base_schema: collect_from_database(&Base::default()),
-            migrations: vec![],
-        }
-    }
-
-    pub fn add<M: Migration>(&mut self) {
-        let old_schema = collect_from_database(&M::OldSchema::default());
-        let new_schema = collect_from_database(&M::NewSchema::default());
-        self.migrations.push(MigrationEntry {
-            old_signature: old_schema.signature().into(),
-            new_signature: new_schema.signature().into(),
-            old_schema,
-            new_schema,
-            migration: Box::new(M::default()),
-        });
-    }
-
-    pub fn apply(&self, lease: &mut ConnectionLease) -> DBResult<()> {
-        log::debug!("Applying migrations");
-        // try getting data from the metaschema
-        let existing_schema = MetadataDB::default()
-            .metastore
-            .keyed(DatabaseSchema::SCHEMA_SIGNATURE_KEY)
-            .get(lease)
-            .ok()
-            .flatten()
-            .map(|v| v.wrapped().value);
-
-        // early-out: do we match the latest migration?
-        if existing_schema.is_some()
-            && self.migrations.last().map(|v| &v.new_signature) == existing_schema.as_ref()
-        {
-            log::trace!("no migrations to apply");
-            return Ok(());
-        }
-
-        // early-out: do we have any existing schema at all?
-        if existing_schema.is_none() {
-            log::trace!("no existing schema found");
-            return self.create_from_scratch(lease);
-        }
-
-        // we have work to do, time to get started
-        let to_apply = self
-            .migrations
-            .iter()
-            .skip_while(|m| Some(&m.old_signature) != existing_schema.as_ref());
-
-        for migration in to_apply {
-            log::debug!(
-                "Applying migration from {}... to {}...",
-                &migration.old_signature[0..8],
-                &migration.new_signature[0..8]
-            );
-
-            // what entities have *changed* between the two schemas? exclude new/removed
-            let table_differences = migration
-                .old_schema
-                .table_queries()
-                .iter()
-                .map(|(tname, oq)| {
-                    Some((
-                        tname,
-                        oq,
-                        migration.new_schema.table_queries().get(tname.as_str())?,
-                    ))
-                })
-                .flatten()
-                .collect::<Vec<_>>();
-
-            let changed = table_differences
-                .iter()
-                .map(|v| v.0)
-                .cloned()
-                .collect::<HashSet<_>>();
-
-            let nschema = migration.migration.collect_intermediate_schema(changed);
-
-            // collect_from_database(nschema.as_ref());
-
-            log::trace!("nschema built");
-
-            println!("table_differences: {table_differences:?}");
-            todo!()
-        }
-
-        log::info!("Database migrations applied");
-
-        Ok(())
-    }
-
-    fn create_from_scratch(&self, lease: &mut ConnectionLease) -> DBResult<()> {
-        let latest_schema = self
-            .migrations
-            .last()
-            .map(|m| &m.new_schema)
-            .unwrap_or(&self.base_schema);
-
-        latest_schema.create(lease)?;
-
-        Ok(())
-    }
-}

+ 0 - 311
microrm/src/schema/migrate/new.rs

@@ -1,311 +0,0 @@
-use std::{
-    collections::{HashMap, HashSet},
-    hash::Hash,
-    marker::PhantomData,
-    sync::Mutex,
-};
-
-use crate::{
-    db::{StatementContext, StatementRow},
-    schema::{
-        datum::{ConcreteDatum, Datum, DatumList},
-        entity::{Entity, EntityID, EntityPart, EntityPartList, EntityPartVisitor},
-        relation::RelationData,
-        DatabaseItemVisitor, Schema,
-    },
-    DBResult,
-};
-
-// ----------------------------------------------------------------------
-// Entity-related New<> adapter
-// ----------------------------------------------------------------------
-
-pub struct NewID<OE: Entity> {
-    value: OE::ID,
-}
-
-impl<OE: Entity> std::fmt::Debug for NewID<OE> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_fmt(format_args!("NewID({})", self.value.into_raw()))
-    }
-}
-
-impl<OE: Entity> Clone for NewID<OE> {
-    fn clone(&self) -> Self {
-        Self {
-            value: self.value.clone(),
-        }
-    }
-}
-
-impl<OE: Entity> Copy for NewID<OE> {}
-
-impl<OE: Entity> Hash for NewID<OE> {
-    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        self.value.hash(state)
-    }
-}
-
-impl<OE: Entity> PartialEq for NewID<OE> {
-    fn eq(&self, other: &Self) -> bool {
-        self.value.eq(&other.value)
-    }
-}
-
-impl<OE: Entity> PartialOrd for NewID<OE> {
-    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        self.value.partial_cmp(&other.value)
-    }
-}
-
-impl<OE: Entity> EntityID for NewID<OE> {
-    type Entity = New<OE>;
-
-    fn from_raw(id: i64) -> Self {
-        Self {
-            value: OE::ID::from_raw(id),
-        }
-    }
-    fn into_raw(self) -> i64 {
-        self.value.into_raw()
-    }
-}
-
-impl<OE: Entity> Datum for NewID<OE> {
-    fn sql_type() -> &'static str {
-        OE::ID::sql_type()
-    }
-    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
-        self.value.bind_to(stmt, index)
-    }
-    fn build_from(adata: RelationData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
-    where
-        Self: Sized,
-    {
-        OE::ID::build_from(adata, stmt, index).map(|value| Self { value })
-    }
-}
-
-impl<OE: Entity> ConcreteDatum for NewID<OE> {}
-
-pub struct NewIDPart<OE: Entity> {
-    _ghost: std::marker::PhantomData<&'static OE>,
-}
-
-impl<OE: Entity> Default for NewIDPart<OE> {
-    fn default() -> Self {
-        Self {
-            _ghost: std::marker::PhantomData,
-        }
-    }
-}
-
-impl<OE: Entity> Clone for NewIDPart<OE> {
-    fn clone(&self) -> Self {
-        Self::default()
-    }
-}
-
-impl<OE: Entity> EntityPart for NewIDPart<OE> {
-    type Entity = New<OE>;
-    type Datum = NewID<OE>;
-
-    fn desc() -> Option<&'static str> {
-        None
-    }
-    fn unique() -> bool {
-        false
-    }
-    fn part_name() -> &'static str {
-        "id"
-    }
-    fn get_datum(_from: &Self::Entity) -> &Self::Datum {
-        unreachable!()
-    }
-}
-
-pub struct NewPart<OE: Entity, OP: EntityPart<Entity = OE>> {
-    _ghost: std::marker::PhantomData<&'static (OE, OP)>,
-}
-
-impl<OE: Entity, OP: EntityPart<Entity = OE>> Default for NewPart<OE, OP> {
-    fn default() -> Self {
-        Self {
-            _ghost: std::marker::PhantomData,
-        }
-    }
-}
-
-impl<OE: Entity, OP: EntityPart<Entity = OE>> Clone for NewPart<OE, OP> {
-    fn clone(&self) -> Self {
-        Self::default()
-    }
-}
-
-impl<OE: Entity, OP: EntityPart<Entity = OE>> EntityPart for NewPart<OE, OP> {
-    type Entity = New<OE>;
-    type Datum = OP::Datum;
-
-    fn desc() -> Option<&'static str> {
-        OP::desc()
-    }
-    fn unique() -> bool {
-        OP::unique()
-    }
-    fn part_name() -> &'static str {
-        OP::part_name()
-    }
-    fn get_datum(from: &Self::Entity) -> &Self::Datum {
-        OP::get_datum(&from.value)
-    }
-}
-
-pub struct NewPartList<OE: Entity, OPL: EntityPartList> {
-    _ghost: std::marker::PhantomData<&'static (OE, OPL)>,
-}
-
-impl<OE: Entity, OPL: EntityPartList<Entity = OE>> EntityPartList for NewPartList<OE, OPL> {
-    type Entity = New<OE>;
-    type ListHead = NewPart<OE, OPL::ListHead>;
-    type ListTail = NewPartList<OE, OPL::ListTail>;
-    type DatumList = OPL::DatumList;
-
-    const IS_EMPTY: bool = OPL::IS_EMPTY;
-
-    fn build_datum_list(stmt: &mut StatementRow) -> DBResult<Self::DatumList> {
-        OPL::build_datum_list(stmt)
-    }
-    fn accept_part_visitor(visitor: &mut impl EntityPartVisitor<Entity = Self::Entity>) {
-        if Self::IS_EMPTY {
-            return;
-        }
-        Self::ListHead::accept_part_visitor(visitor);
-        Self::ListTail::accept_part_visitor(visitor);
-    }
-    fn accept_part_visitor_ref(
-        datum_list: &Self::DatumList,
-        visitor: &mut impl EntityPartVisitor<Entity = Self::Entity>,
-    ) {
-        if Self::IS_EMPTY {
-            return;
-        }
-
-        visitor.visit_datum::<Self::ListHead>(datum_list.list_head());
-
-        let tail = datum_list.list_tail();
-        Self::ListTail::accept_part_visitor_ref(&tail, visitor);
-    }
-}
-
-#[derive(Debug)]
-pub struct New<E: Entity> {
-    value: E,
-}
-
-lazy_static::lazy_static! {
-    static ref NEWNAMES : Mutex<HashMap<std::any::TypeId, &'static str>> = {
-        Default::default()
-    };
-}
-
-impl<E: Entity> Entity for New<E> {
-    type ID = NewID<E>;
-    type Keys = NewPartList<E, E::Keys>;
-    type Parts = NewPartList<E, E::Parts>;
-    type IDPart = NewIDPart<E>;
-
-    fn entity_name() -> &'static str {
-        let mut nn = NEWNAMES.lock().unwrap();
-        let e = nn.entry(std::any::TypeId::of::<Self>());
-        e.or_insert_with(|| Box::leak(format!("NEW_{}", E::entity_name()).into_boxed_str()))
-    }
-
-    fn build(values: <Self::Parts as EntityPartList>::DatumList) -> Self {
-        Self {
-            value: E::build(values),
-        }
-    }
-
-    fn accept_part_visitor(visitor: &mut impl EntityPartVisitor<Entity = Self>) {
-        Self::Parts::accept_part_visitor(visitor)
-    }
-
-    fn accept_part_visitor_ref(&self, visitor: &mut impl EntityPartVisitor<Entity = Self>) {
-        struct PassthroughVisitor<'l, E: Entity, EPV: EntityPartVisitor<Entity = New<E>>>(
-            &'l mut EPV,
-        );
-        impl<'l, E: Entity, EPV: EntityPartVisitor<Entity = New<E>>> EntityPartVisitor
-            for PassthroughVisitor<'l, E, EPV>
-        {
-            type Entity = E;
-            fn visit_datum<EP: EntityPart<Entity = Self::Entity>>(&mut self, datum: &EP::Datum) {
-                self.0.visit_datum::<NewPart<E, EP>>(datum);
-            }
-        }
-
-        let mut pv = PassthroughVisitor(visitor);
-        self.value.accept_part_visitor_ref(&mut pv);
-    }
-
-    fn accept_part_visitor_mut(&mut self, visitor: &mut impl EntityPartVisitor<Entity = Self>) {
-        struct PassthroughVisitor<'l, E: Entity, EPV: EntityPartVisitor<Entity = New<E>>>(
-            &'l mut EPV,
-        );
-        impl<'l, E: Entity, EPV: EntityPartVisitor<Entity = New<E>>> EntityPartVisitor
-            for PassthroughVisitor<'l, E, EPV>
-        {
-            type Entity = E;
-            fn visit_datum_mut<EP: EntityPart<Entity = Self::Entity>>(
-                &mut self,
-                datum: &mut EP::Datum,
-            ) {
-                self.0.visit_datum_mut::<NewPart<E, EP>>(datum);
-            }
-        }
-
-        let mut pv = PassthroughVisitor(visitor);
-        self.value.accept_part_visitor_mut(&mut pv);
-    }
-}
-
-// ----------------------------------------------------------------------
-// Schema-level adaptation
-// ----------------------------------------------------------------------
-pub struct WrapSchema<S: Schema> {
-    base: S,
-    towrap: HashSet<String>,
-}
-
-impl<S: Schema> WrapSchema<S> {
-    pub fn new(base: S, towrap: HashSet<String>) -> Self {
-        Self { base, towrap }
-    }
-}
-
-impl<S: Schema> Schema for WrapSchema<S> {
-    fn accept_item_visitor(&self, visitor: &mut impl DatabaseItemVisitor)
-    where
-        Self: Sized,
-    {
-        struct InterceptingVisitor<'a, S: Schema, V: DatabaseItemVisitor>(
-            &'a WrapSchema<S>,
-            &'a mut V,
-        );
-        impl<'a, S: Schema, V: DatabaseItemVisitor> DatabaseItemVisitor for InterceptingVisitor<'a, S, V> {
-            fn visit_idmap<T: Entity>(&mut self)
-            where
-                Self: Sized,
-            {
-            }
-
-            fn visit_index<T: Entity, PL: EntityPartList<Entity = T>>(&mut self)
-            where
-                Self: Sized,
-            {
-            }
-        }
-
-        let mut iv = InterceptingVisitor(self, visitor);
-        self.base.accept_item_visitor(&mut iv);
-    }
-}