Browse Source

Many minor changes inspired by dogfooding.

Kestrel 1 year ago
parent
commit
766e957411

+ 1 - 0
microrm/Cargo.toml

@@ -17,6 +17,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_bytes = { version = "0.11.6" }
 serde_json = { version = "1.0" }
 lazy_static = { version = "1.4.0" }
+time = "0.3"
 
 microrm-macros = { path = "../microrm-macros", version = "0.2.5" }
 log = "0.4.17"

+ 18 - 4
microrm/src/db.rs

@@ -7,12 +7,17 @@ use std::{
 
 pub type DBConnection = std::sync::Arc<Connection>;
 
+pub(crate) struct CachedStatement {
+    stmt: sqlite::Statement<'static>,
+    sql: String,
+}
+
 pub struct Connection {
     // we leak the ConnectionThreadSafe and make sure that the only references to it are stored in
     // statement_cache, so as long as we drop the statement_cache first there are no correctness
     // issues, and we get to have static-lifetime statements for caching purposes.
     conn: &'static sqlite::ConnectionThreadSafe,
-    statement_cache: Mutex<HashMap<u64, sqlite::Statement<'static>>>,
+    statement_cache: Mutex<HashMap<u64, CachedStatement>>,
 }
 
 impl Connection {
@@ -47,10 +52,19 @@ impl Connection {
     ) -> DBResult<R> {
         match self.statement_cache.lock()?.entry(hash_key) {
             std::collections::hash_map::Entry::Vacant(e) => {
-                let q: sqlite::Statement<'static> = self.conn.prepare(build_query())?;
-                run_query(e.insert(q))
+                let sql = build_query();
+
+                log::trace!("built SQL query: {sql}");
+
+                let q: sqlite::Statement<'static> = self.conn.prepare(sql.as_str())?;
+                let inserted = e.insert(CachedStatement {
+                    sql: sql.clone(),
+                    stmt: q,
+                });
+                run_query(&mut inserted.stmt).map_err(|e| e.sqlite_to_query(sql.as_str()))
             }
-            std::collections::hash_map::Entry::Occupied(mut e) => run_query(e.get_mut()),
+            std::collections::hash_map::Entry::Occupied(mut e) => run_query(&mut e.get_mut().stmt)
+                .map_err(|err| err.sqlite_to_query(&e.get_mut().sql)),
         }
     }
 }

+ 12 - 0
microrm/src/lib.rs

@@ -21,13 +21,25 @@ pub mod prelude {
 #[derive(Debug)]
 pub enum DBError {
     EmptyResult,
+    UnknownValue(String),
     IncompatibleSchema,
     LogicError(&'static str),
     Sqlite(sqlite::Error),
+    Query(String, sqlite::Error),
     JSON(serde_json::Error),
     LockError(String),
 }
 
+impl DBError {
+    /// Converts a ::Sqlite error into a ::Query error by providing context
+    pub(crate) fn sqlite_to_query(self, ctx: &str) -> Self {
+        match self {
+            Self::Sqlite(e) => Self::Query(ctx.to_string(), e),
+            _ => self,
+        }
+    }
+}
+
 impl From<sqlite::Error> for DBError {
     fn from(value: sqlite::Error) -> Self {
         Self::Sqlite(value)

+ 205 - 92
microrm/src/query.rs

@@ -12,12 +12,34 @@ use std::hash::{Hash, Hasher};
 #[derive(Hash)]
 enum QueryType<'a> {
     ByID(&'a str),
+    Delete(std::any::TypeId),
+    DeleteAssoc(&'a str, &'a str, &'a str),
+    DeleteById,
     Select(std::any::TypeId),
     SelectJoin(&'a str, &'a str, &'a str),
+    SelectJoinFilter(&'a str, &'a str, &'a str, std::any::TypeId),
     Insert,
     InsertAssoc(&'a str, &'a str, &'a str),
 }
 
+fn bind_datum_to<DL: DatumList>(
+    stmt: &mut sqlite::Statement,
+    dl: &DL,
+    start_index: usize,
+) -> DBResult<()> {
+    struct BindDatum<'a, 'b>(&'a mut sqlite::Statement<'b>, usize);
+    impl<'a, 'b> DatumVisitor for BindDatum<'a, 'b> {
+        fn visit<ED: Datum>(&mut self, datum: &ED) {
+            datum.bind_to(self.0, self.1);
+            self.1 += 1
+        }
+    }
+    // note that this indexing starts at 1
+    dl.accept(&mut BindDatum(stmt, start_index));
+
+    Ok(())
+}
+
 fn query_hash<E: Entity>(qtype: QueryType) -> u64 {
     let mut hasher = std::collections::hash_map::DefaultHasher::new();
 
@@ -27,88 +49,6 @@ fn query_hash<E: Entity>(qtype: QueryType) -> u64 {
     hasher.finish()
 }
 
-/*
-pub(crate) fn select_assoc<E: Entity>(map: &AssocMap<E>) -> DBResult<Vec<E>> {
-    let adata = map
-        .data
-        .as_ref()
-        .ok_or(DBError::LogicError("Reading from AssocMap with no base ID"))?;
-    // equivalent SQL:
-    // SELECT
-    //     `target_table_name`.*
-    // FROM
-    //     `assoc_table_name`
-    // LEFT JOIN `target_table_name` ON `assoc_table_name`.`target` = `target_table_name`.`rowid`
-    // WHERE `base` = base_rowid
-
-    let base_name = adata.local_name;
-    let target_name = E::entity_name();
-    let part_name = adata.part_name;
-
-    map.conn().with_prepared(
-        query_hash::<E>(QueryType::SelectJoin(part_name)),
-        || {
-            let assoc_name = format!("{base_name}_assoc_{part_name}_{target_name}");
-            format!(
-                "select `{target_name}`.* from `{assoc_name}` \
-                    left join `{target_name}` on \
-                        `{assoc_name}`.`target` = `{target_name}`.`ID` \
-                    where `{assoc_name}`.`base` = ?"
-            )
-        },
-        |stmt| {
-            stmt.bind((1, adata.local_id))?;
-
-            // now we grab the statement outputs
-            let mut rows = vec![];
-            while stmt.next()? == sqlite::State::Row {
-                let datum_list = <E::Parts>::build_datum_list(&map.conn(), stmt)?;
-                rows.push(E::build(datum_list));
-            }
-
-            Ok(rows)
-        },
-    )
-}
-*/
-
-/*
-pub(crate) fn insert_assoc<E: Entity>(map: &AssocMap<E>, value: E) -> DBResult<()> {
-    // we're doing two things:
-    // - inserting the entity into the target table
-    // - adding thw association row into the assoc table
-
-    // so first, the target table
-    let target_id = insert(map, value)?;
-
-    let adata = map
-        .data
-        .as_ref()
-        .ok_or(DBError::LogicError("Reading from AssocMap with no base ID"))?;
-
-    // second, the assoc table
-    map.conn().with_prepared(
-        query_hash::<E>(QueryType::InsertAssoc(adata.part_name)),
-        || {
-            let local_name = adata.local_name;
-            let target_name = E::entity_name();
-            let part_name = adata.part_name;
-            let assoc_name = format!("{local_name}_assoc_{part_name}_{target_name}");
-            format!("insert into `{assoc_name}` (`base`, `target`) values (?, ?)")
-        },
-        |stmt| {
-            stmt.reset()?;
-            stmt.bind((1, adata.local_id))?;
-            stmt.bind((2, target_id.into_raw()))?;
-
-            stmt.next()?;
-
-            Ok(())
-        },
-    )
-}
-*/
-
 pub(crate) fn by_id<ID: EntityID>(
     conn: &DBConnection,
     id: ID,
@@ -141,19 +81,33 @@ pub(crate) fn by_id<ID: EntityID>(
     )
 }
 
+pub(crate) fn get_all<E: Entity>(conn: &DBConnection) -> DBResult<Vec<IDWrap<E>>> {
+    conn.with_prepared(
+        query_hash::<E>(QueryType::Select(std::any::TypeId::of::<E>())),
+        || format!("select * from `{}`", E::entity_name()),
+        |stmt| {
+            stmt.reset()?;
+
+            let mut rows = vec![];
+            while stmt.next()? == sqlite::State::Row {
+                let datum_list = <E::Parts>::build_datum_list(&conn, stmt)?;
+                rows.push(IDWrap::new(
+                    <E::ID>::from_raw(stmt.read::<i64, _>(0)?),
+                    E::build(datum_list),
+                ));
+            }
+
+            stmt.reset()?;
+
+            Ok(rows)
+        },
+    )
+}
+
 pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
     map: &IDMap<E>,
     by: &PL::DatumList,
 ) -> DBResult<Vec<IDWrap<E>>> {
-    struct HashDatumListTypes(std::collections::hash_map::DefaultHasher);
-    impl DatumVisitor for HashDatumListTypes {
-        fn visit<ED: Datum>(&mut self, _: &ED) {
-            std::any::TypeId::of::<ED>().hash(&mut self.0);
-        }
-    }
-    let mut ty = HashDatumListTypes(Default::default());
-    by.accept(&mut ty);
-
     map.conn().with_prepared(
         query_hash::<E>(QueryType::Select(std::any::TypeId::of::<PL>())),
         || {
@@ -173,7 +127,7 @@ pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
             PL::accept_part_visitor(&mut BuildConditions(&mut conditions));
 
             let table_name = format!("{}", E::entity_name());
-            format!("select rowid, * from `{}` where {}", table_name, conditions)
+            format!("select * from `{}` where {}", table_name, conditions)
         },
         |stmt| {
             struct BindDatum<'a, 'b>(&'a mut sqlite::Statement<'b>, usize);
@@ -203,6 +157,52 @@ pub(crate) fn select_by<E: Entity, PL: EntityPartList>(
     )
 }
 
+pub(crate) fn delete_by_id<E: Entity>(conn: &DBConnection, id: E::ID) -> DBResult<()> {
+    conn.with_prepared(
+        query_hash::<E>(QueryType::DeleteById),
+        || format!("delete from `{}` where `id` = ?", E::entity_name()),
+        |stmt| {
+            stmt.reset()?;
+            stmt.bind((1, id.into_raw()))?;
+            stmt.next()?;
+            Ok(())
+        },
+    )
+}
+
+pub(crate) fn delete_by<E: Entity, PL: EntityPartList>(
+    conn: &DBConnection,
+    values: &PL::DatumList,
+) -> DBResult<()> {
+    conn.with_prepared(
+        query_hash::<E>(QueryType::Delete(std::any::TypeId::of::<PL>())),
+        || {
+            let mut conditions = String::new();
+            struct BuildConditions<'a>(&'a mut String);
+            impl<'a> EntityPartVisitor for BuildConditions<'a> {
+                fn visit<EP: EntityPart>(&mut self) {
+                    if self.0.len() > 0 {
+                        self.0.push_str(" and ");
+                    }
+                    self.0.push_str("`");
+                    self.0.push_str(EP::part_name());
+                    self.0.push_str("`");
+                    self.0.push_str(" = ?");
+                }
+            }
+            PL::accept_part_visitor(&mut BuildConditions(&mut conditions));
+
+            format!("delete from `{}` where {}", E::entity_name(), conditions)
+        },
+        |stmt| {
+            stmt.reset()?;
+            bind_datum_to(stmt, values, 1)?;
+            stmt.next()?;
+            Ok(())
+        },
+    )
+}
+
 pub(crate) fn insert<E: Entity>(conn: &DBConnection, value: E) -> DBResult<E::ID> {
     conn.with_prepared(
         query_hash::<E>(QueryType::Insert),
@@ -374,6 +374,85 @@ pub(crate) trait AssocInterface {
         )
     }
 
+    fn lookup_by<PL: EntityPartList>(
+        &self,
+        values: &PL::DatumList,
+    ) -> DBResult<Vec<IDWrap<Self::RemoteEntity>>>
+    where
+        Self: Sized,
+    {
+        let adata = self.get_data()?;
+
+        // equivalent SQL:
+        // SELECT
+        //     `range_table_name`.*
+        // FROM
+        //     `assoc_table_name`
+        // LEFT JOIN `range_table_name` ON `assoc_table_name`.`range` = `range_table_name`.`rowid`
+        // WHERE
+        //      `domain` = domain_id
+        //      AND `unique_1` = ?
+
+        let an = AssocNames::collect::<Self>(&self)?;
+
+        adata.conn.with_prepared(
+            query_hash::<Self::RemoteEntity>(QueryType::SelectJoinFilter(
+                an.local_name,
+                an.remote_name,
+                an.part_name,
+                std::any::TypeId::of::<<Self::RemoteEntity as Entity>::Uniques>(),
+            )),
+            || {
+                let mut conditions = String::new();
+                struct BuildConditions<'a>(&'a mut String);
+                impl<'a> EntityPartVisitor for BuildConditions<'a> {
+                    fn visit<EP: EntityPart>(&mut self) {
+                        self.0.push_str(" and ");
+                        self.0.push_str("`");
+                        self.0.push_str(EP::part_name());
+                        self.0.push_str("`");
+                        self.0.push_str(" = ?");
+                    }
+                }
+
+                <<Self::RemoteEntity as Entity>::Uniques>::accept_part_visitor(
+                    &mut BuildConditions(&mut conditions),
+                );
+
+                format!(
+                    "select `{remote_name}`.* from `{assoc_name}` \
+                        left join `{remote_name}` on \
+                            `{assoc_name}`.`{remote_field}` = `{remote_name}`.`id` \
+                        where `{assoc_name}`.`{local_field}` = ? {conditions}",
+                    assoc_name = an.assoc_name(),
+                    remote_name = an.remote_name,
+                    local_field = an.local_field,
+                    remote_field = an.remote_field,
+                )
+            },
+            |stmt| {
+                stmt.bind((1, adata.local_id))?;
+                bind_datum_to(stmt, values, 2);
+
+                // now we grab the statement outputs
+                let mut rows = vec![];
+                while stmt.next()? == sqlite::State::Row {
+                    let id = stmt.read::<i64, _>(0)?;
+                    let datum_list = <<Self::RemoteEntity as Entity>::Parts>::build_datum_list(
+                        &adata.conn,
+                        stmt,
+                    )?;
+                    rows.push(IDWrap::new(
+                        <Self::RemoteEntity as Entity>::ID::from_raw(id),
+                        Self::RemoteEntity::build(datum_list),
+                    ));
+                }
+
+                Ok(rows)
+            },
+        )
+    }
+
     fn associate_with(&self, remote_id: <Self::RemoteEntity as Entity>::ID) -> DBResult<()>
     where
         Self: Sized,
@@ -408,6 +487,40 @@ pub(crate) trait AssocInterface {
         )
     }
 
+    fn dissociate_with(&self, remote_id: <Self::RemoteEntity as Entity>::ID) -> DBResult<()>
+    where
+        Self: Sized,
+    {
+        let adata = self.get_data()?;
+        let an = AssocNames::collect::<Self>(&self)?;
+
+        // second, add to the assoc table
+        adata.conn.with_prepared(
+            query_hash::<Self::RemoteEntity>(QueryType::DeleteAssoc(
+                an.local_name,
+                an.remote_name,
+                an.part_name,
+            )),
+            || {
+                format!(
+                    "delete from `{assoc_name}` where `{local_field}` = ? and `{remote_field}` = ?",
+                    assoc_name = an.assoc_name(),
+                    local_field = an.local_field,
+                    remote_field = an.remote_field
+                )
+            },
+            |stmt| {
+                stmt.reset()?;
+                stmt.bind((1, adata.local_id))?;
+                stmt.bind((2, remote_id.into_raw()))?;
+
+                stmt.next()?;
+
+                Ok(())
+            },
+        )
+    }
+
     fn insert(&self, value: Self::RemoteEntity) -> DBResult<<Self::RemoteEntity as Entity>::ID>
     where
         Self: Sized,

+ 60 - 0
microrm/src/schema.rs

@@ -45,6 +45,10 @@ impl<T: Entity> IDWrap<T> {
     pub fn as_ref(&self) -> &T {
         &self.wrap
     }
+
+    pub fn wrapped(self) -> T {
+        self.wrap
+    }
 }
 
 impl<T: Entity> AsRef<T> for IDWrap<T> {
@@ -133,6 +137,12 @@ impl<T: Entity> std::fmt::Debug for AssocMap<T> {
     }
 }
 
+impl<T: Entity> Default for AssocMap<T> {
+    fn default() -> Self {
+        Self::empty()
+    }
+}
+
 impl<T: Entity> AssocInterface for AssocMap<T> {
     type RemoteEntity = T;
     const SIDE: LocalSide = LocalSide::Domain;
@@ -165,10 +175,21 @@ impl<T: Entity> AssocMap<T> {
         <Self as AssocInterface>::get_all(self)
     }
 
+    pub fn lookup_unique(
+        &self,
+        values: &<T::Uniques as EntityPartList>::DatumList,
+    ) -> DBResult<Option<IDWrap<T>>> {
+        <Self as AssocInterface>::lookup_by::<T::Uniques>(self, values).map(|mut v| v.pop())
+    }
+
     pub fn associate_with(&self, remote_id: T::ID) -> DBResult<()> {
         <Self as AssocInterface>::associate_with(self, remote_id)
     }
 
+    pub fn dissociate_with(&self, remote_id: T::ID) -> DBResult<()> {
+        <Self as AssocInterface>::dissociate_with(self, remote_id)
+    }
+
     pub fn insert(&self, value: T) -> DBResult<T::ID> {
         <Self as AssocInterface>::insert(self, value)
     }
@@ -232,6 +253,15 @@ impl<R: Relation> Default for AssocDomain<R> {
     }
 }
 
+impl<R: Relation> std::fmt::Debug for AssocDomain<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_fmt(format_args!(
+            "AssocDomain {{ id: {:?} }}",
+            self.data.as_ref().map(|d| d.local_id)
+        ))
+    }
+}
+
 impl<R: Relation> AssocDomain<R> {
     pub fn empty() -> Self {
         Self {
@@ -248,6 +278,10 @@ impl<R: Relation> AssocDomain<R> {
         <Self as AssocInterface>::associate_with(self, remote_id)
     }
 
+    pub fn dissociate_with(&self, remote_id: <R::Range as Entity>::ID) -> DBResult<()> {
+        <Self as AssocInterface>::dissociate_with(self, remote_id)
+    }
+
     pub fn insert(&self, value: R::Range) -> DBResult<<R::Range as Entity>::ID> {
         <Self as AssocInterface>::insert(self, value)
     }
@@ -328,6 +362,15 @@ impl<R: Relation> Default for AssocRange<R> {
     }
 }
 
+impl<R: Relation> std::fmt::Debug for AssocRange<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_fmt(format_args!(
+            "AssocRange {{ id: {:?} }}",
+            self.data.as_ref().map(|d| d.local_id)
+        ))
+    }
+}
+
 impl<R: Relation> AssocRange<R> {
     pub fn empty() -> Self {
         Self {
@@ -344,6 +387,10 @@ impl<R: Relation> AssocRange<R> {
         <Self as AssocInterface>::associate_with(self, remote_id)
     }
 
+    pub fn dissociate_with(&self, remote_id: <R::Domain as Entity>::ID) -> DBResult<()> {
+        <Self as AssocInterface>::dissociate_with(self, remote_id)
+    }
+
     pub fn insert(&self, value: R::Domain) -> DBResult<<R::Domain as Entity>::ID> {
         <Self as AssocInterface>::insert(self, value)
     }
@@ -504,6 +551,12 @@ impl<T: Entity> IDMap<T> {
         }
     }
 
+    /// Retrieve all entities in this map.
+
+    pub fn get_all<E: Entity>(&self) -> DBResult<Vec<IDWrap<E>>> {
+        query::get_all(&self.conn)
+    }
+
     /// Look up an Entity in this map by ID.
     pub fn by_id(&self, id: T::ID) -> DBResult<Option<IDWrap<T>>> {
         query::by_id(&self.conn, id)
@@ -525,6 +578,13 @@ impl<T: Entity> IDMap<T> {
         })
     }
 
+    pub fn delete_unique(
+        &self,
+        uniques: &<<T as Entity>::Uniques as EntityPartList>::DatumList,
+    ) -> DBResult<()> {
+        query::delete_by::<T, T::Uniques>(&self.conn, uniques)
+    }
+
     /// Insert a new Entity into this map, and return its new ID.
     pub fn insert(&self, value: T) -> DBResult<T::ID> {
         query::insert(self.conn(), value)

+ 16 - 14
microrm/src/schema/build.rs

@@ -36,7 +36,7 @@ impl TableInfo {
                 ", `{}` {}{}",
                 col.name,
                 col.ty,
-                if col.unique { "unique" } else { "" }
+                if col.unique { " unique" } else { "" }
             )
         });
         let fkeys = self.columns.iter().filter_map(|col| {
@@ -48,7 +48,7 @@ impl TableInfo {
         });
 
         format!(
-            "create table {} (`id` integer primary key{}{});",
+            "create table `{}` (`id` integer primary key{}{});",
             self.table_name,
             columns.collect::<String>(),
             fkeys.collect::<String>()
@@ -130,14 +130,12 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
         let mut table = TableInfo::new(table_name.clone());
         for part in state.parts.iter() {
             match &part.ty {
-                PartType::Datum(dtype) => {
-                    table.columns.push(ColumnInfo {
-                        name: part.name,
-                        ty: dtype.to_string(),
-                        fkey: None,
-                        unique: false, // XXX
-                    })
-                }
+                PartType::Datum(dtype) => table.columns.push(ColumnInfo {
+                    name: part.name,
+                    ty: dtype.to_string(),
+                    fkey: None,
+                    unique: part.unique,
+                }),
                 PartType::AssocDomain {
                     table_name: assoc_table_name,
                     range_name,
@@ -149,14 +147,14 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
                     assoc_table.columns.push(ColumnInfo {
                         name: "domain".into(),
                         ty: "int".into(),
-                        fkey: Some(format!("{}(`id`)", table_name)),
+                        fkey: Some(format!("`{}`(`id`)", table_name)),
                         unique: false,
                     });
 
                     assoc_table.columns.push(ColumnInfo {
                         name: "range".into(),
                         ty: "int".into(),
-                        fkey: Some(format!("{}(`id`)", range_name)),
+                        fkey: Some(format!("`{}`(`id`)", range_name)),
                         unique: false,
                     });
 
@@ -173,14 +171,14 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
                     assoc_table.columns.push(ColumnInfo {
                         name: "domain".into(),
                         ty: "int".into(),
-                        fkey: Some(format!("{}(`id`)", domain_name)),
+                        fkey: Some(format!("`{}`(`id`)", domain_name)),
                         unique: false,
                     });
 
                     assoc_table.columns.push(ColumnInfo {
                         name: "range".into(),
                         ty: "int".into(),
-                        fkey: Some(format!("{}(`id`)", table_name)),
+                        fkey: Some(format!("`{}`(`id`)", table_name)),
                         unique: false,
                     });
 
@@ -228,6 +226,10 @@ pub(crate) fn collect_from_database<DB: Database>() -> DatabaseSchema {
         }
     }
 
+    if tsort.len() > 0 {
+        panic!("Cycle detected in dependency keys!")
+    }
+
     // TODO: generate index schemas here
 
     DatabaseSchema {

+ 4 - 1
microrm/src/schema/collect.rs

@@ -21,6 +21,7 @@ pub enum PartType {
 pub struct PartState {
     pub name: &'static str,
     pub ty: PartType,
+    pub unique: bool,
 }
 
 impl PartState {
@@ -41,7 +42,8 @@ impl PartState {
         }
         impl<EP: EntityPart> DatumDiscriminator for Discriminator<EP> {
             fn visit_entity_id<E: Entity>(&mut self) {
-                unreachable!()
+                // TODO: add foreign key constraint
+                self.ty = Some(PartType::Datum(<E::ID as Datum>::sql_type()));
             }
 
             fn visit_bare_field<T: Datum>(&mut self) {
@@ -100,6 +102,7 @@ impl PartState {
             PartState {
                 name: EP::part_name(),
                 ty,
+                unique: EP::unique(),
             }
         } else {
             unreachable!("no PartType extracted from EntityPart")

+ 29 - 3
microrm/src/schema/datum/datum_common.rs

@@ -1,8 +1,34 @@
 use crate::{
     schema::{AssocData, Datum},
-    DBResult,
+    DBError, DBResult,
 };
 
+impl Datum for time::OffsetDateTime {
+    fn sql_type() -> &'static str {
+        "text"
+    }
+
+    fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
+        let ts = self.unix_timestamp();
+        ts.bind_to(stmt, index)
+    }
+
+    fn build_from<'a>(
+        adata: AssocData,
+        stmt: &mut sqlite::Statement<'a>,
+        index: usize,
+    ) -> DBResult<(Self, usize)>
+    where
+        Self: Sized,
+    {
+        let (unix, index) = i64::build_from(adata, stmt, index)?;
+        Ok((
+            Self::from_unix_timestamp(unix).map_err(|e| DBError::UnknownValue(e.to_string()))?,
+            index,
+        ))
+    }
+}
+
 impl Datum for String {
     fn sql_type() -> &'static str {
         "text"
@@ -94,8 +120,8 @@ impl Datum for i64 {
         "int"
     }
 
-    fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, _index: usize) {
-        todo!()
+    fn bind_to<'a>(&self, stmt: &mut sqlite::Statement<'a>, index: usize) {
+        stmt.bind((index, *self)).expect("couldn't bind i64")
     }
 
     fn build_from<'a>(

+ 1 - 1
microrm/src/schema/entity.rs

@@ -9,7 +9,7 @@ use crate::{
 pub(crate) mod helpers;
 
 /// Integral identifier for an entity.
-pub trait EntityID: 'static + PartialEq + Hash + PartialOrd + Debug + Copy {
+pub trait EntityID: 'static + PartialEq + Hash + PartialOrd + Debug + Copy + Datum {
     type Entity: Entity<ID = Self>;
 
     /// Construct from a raw integer.

+ 55 - 0
microrm/src/schema/tests.rs

@@ -10,6 +10,7 @@ mod manual_test_db {
     // simple hand-built database example
 
     use crate::db::DBConnection;
+    use crate::schema::datum::Datum;
     use crate::schema::entity::{
         Entity, EntityID, EntityPart, EntityPartList, EntityPartVisitor, EntityVisitor,
     };
@@ -22,6 +23,33 @@ mod manual_test_db {
     #[derive(Clone, Copy, PartialEq, PartialOrd, Debug, Hash)]
     struct SimpleEntityID(i64);
 
+    impl Datum for SimpleEntityID {
+        fn sql_type() -> &'static str {
+            "int"
+        }
+        fn accept_entity_visitor(_: &mut impl EntityVisitor) {}
+        fn accept_discriminator(d: &mut impl crate::schema::DatumDiscriminator)
+        where
+            Self: Sized,
+        {
+            d.visit_entity_id::<<Self as EntityID>::Entity>();
+        }
+
+        fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize) {
+            todo!()
+        }
+        fn build_from<'a>(
+            adata: crate::schema::AssocData,
+            stmt: &mut sqlite::Statement<'a>,
+            index: usize,
+        ) -> crate::DBResult<(Self, usize)>
+        where
+            Self: Sized,
+        {
+            todo!()
+        }
+    }
+
     impl EntityID for SimpleEntityID {
         type Entity = SimpleEntity;
 
@@ -343,3 +371,30 @@ mod mutual_relationship {
         );
     }
 }
+
+mod reserved_words {
+    use crate::db::DBConnection;
+    use crate::prelude::*;
+    use crate::schema::entity::Entity;
+    use crate::schema::{AssocDomain, AssocRange, Database, IDMap};
+
+    #[derive(Entity)]
+    struct Select {
+        delete: String,
+    }
+
+    #[derive(Entity)]
+    struct Group {
+        by: AssocMap<Select>,
+    }
+
+    #[derive(Database)]
+    struct ReservedWordDB {
+        group: IDMap<Group>,
+    }
+
+    #[test]
+    fn open_test() {
+        ReservedWordDB::open_path(":memory:");
+    }
+}