Browse Source

Swap from sqlite crate to custom sqlite wrapper.

Kestrel 1 year ago
parent
commit
0da682090e

+ 1 - 1
microrm-macros/src/database.rs

@@ -75,7 +75,7 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
 
     quote! {
         impl ::microrm::schema::Database for #db_ident {
-            fn build(conn: ::microrm::db::DBConnection) -> Self where Self: Sized {
+            fn build(conn: ::microrm::db::Connection) -> Self where Self: Sized {
                 Self { #(#build_method),* }
             }
 

+ 5 - 6
microrm-macros/src/entity.rs

@@ -169,20 +169,19 @@ pub fn derive(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
                 <i64 as ::microrm::schema::datum::Datum>::sql_type()
             }
 
-            fn bind_to<'a>(&self, stmt: &mut ::microrm::sqlite::Statement<'a>, index: usize) {
+            fn bind_to<'a>(&self, stmt: &mut ::microrm::db::StatementContext, index: i32) {
                 <i64 as ::microrm::schema::datum::Datum>::bind_to(&self.0, stmt, index)
             }
 
             fn build_from<'a>(
                 adata: ::microrm::schema::AssocData,
-                stmt: &mut ::microrm::sqlite::Statement<'a>,
-                index: usize,
-            ) -> ::microrm::DBResult<(Self, usize)>
+                stmt: &mut ::microrm::db::StatementRow,
+                index: &mut i32,
+            ) -> ::microrm::DBResult<Self>
             where
                 Self: Sized,
             {
-                let raw = <i64 as ::microrm::schema::datum::Datum>::build_from(adata, stmt, index)?;
-                Ok((Self(raw.0), raw.1))
+                Ok(Self(<i64 as ::microrm::schema::datum::Datum>::build_from(adata, stmt, index)?))
             }
 
             fn accept_discriminator(d: &mut impl ::microrm::schema::DatumDiscriminator) where Self: Sized {

+ 4 - 4
microrm/Cargo.toml

@@ -12,7 +12,7 @@ description = "Lightweight ORM using sqlite as a backend."
 [dependencies]
 base64 = "0.13"
 sha2 = "0.10"
-sqlite = "0.33"
+libsqlite3-sys = "0.28"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = { version = "1.0" }
 lazy_static = { version = "1.4.0" }
@@ -24,9 +24,9 @@ log = "0.4.17"
 topological-sort = { version = "0.2" }
 
 [dev-dependencies]
-criterion = "0.5"
-rand = "0.8.5"
-stats_alloc = "0.1.10"
+# criterion = "0.5"
+# rand = "0.8.5"
+# stats_alloc = "0.1.10"
 
 # [[bench]]
 # name = "simple_in_memory"

+ 396 - 10
microrm/src/db.rs

@@ -1,15 +1,28 @@
-use crate::{DBError, DBResult};
-
+use crate::{DBResult, Error};
+use libsqlite3_sys as sq;
 use std::{
     collections::HashMap,
+    ffi::{CStr, CString},
     sync::{Arc, Mutex},
 };
 
-pub type DBConnection = std::sync::Arc<Connection>;
+fn check_rcode(sql: Option<&str>, rcode: i32) -> Result<(), Error> {
+    if rcode == sq::SQLITE_OK {
+        Ok(())
+    } else {
+        Err(Error::Sqlite {
+            code: rcode,
+            msg: unsafe { CStr::from_ptr(sq::sqlite3_errstr(rcode)) }
+                .to_str()?
+                .to_string(),
+            sql: sql.map(|s| s.to_string()),
+        })
+    }
+}
 
-pub(crate) struct CachedStatement {
-    stmt: sqlite::Statement<'static>,
-    sql: String,
+struct ConnectionData {
+    sqlite: *mut sq::sqlite3,
+    stmts: HashMap<u64, Statement>,
 }
 
 pub(crate) trait PreparedKey {
@@ -32,6 +45,377 @@ impl PreparedKey for std::any::TypeId {
     }
 }
 
+/// Represents a single sqlite connection, in SQLITE_MULTITHREADED mode.
+///
+/// This translates to a struct that is Send, but not Sync.
+#[derive(Clone)]
+pub struct Connection(Arc<Mutex<ConnectionData>>);
+
+impl Connection {
+    pub fn new(url: &str) -> Result<Self, Error> {
+        let db_ptr = unsafe {
+            let url = CString::new(url)?;
+            let mut db_ptr = std::ptr::null_mut();
+            check_rcode(
+                None,
+                sq::sqlite3_open_v2(
+                    url.as_ptr(),
+                    &mut db_ptr,
+                    sq::SQLITE_OPEN_READWRITE | sq::SQLITE_OPEN_NOMUTEX | sq::SQLITE_OPEN_CREATE,
+                    std::ptr::null_mut(),
+                ),
+            )?;
+            db_ptr
+        };
+
+        if db_ptr.is_null() {
+            return Err(Error::InternalError(
+                "sqlite3_open_v2 returned a NULL connection",
+            ));
+        }
+
+        Ok(Self(Arc::new(Mutex::new(ConnectionData {
+            sqlite: db_ptr,
+            stmts: Default::default(),
+        }))))
+    }
+
+    pub fn execute_raw_sql(&self, sql: impl AsRef<str>) -> DBResult<()> {
+        let data = self.0.lock()?;
+
+        unsafe {
+            let c_sql = CString::new(sql.as_ref())?;
+            let mut err = std::ptr::null_mut();
+            let rcode = sq::sqlite3_exec(
+                data.sqlite,
+                c_sql.as_ptr(),
+                None,
+                std::ptr::null_mut(),
+                &mut err,
+            );
+
+            // special error handling because of the err string
+            if rcode != sq::SQLITE_OK {
+                let e = Error::Sqlite {
+                    code: rcode,
+                    msg: if err == std::ptr::null_mut() {
+                        CStr::from_ptr(sq::sqlite3_errstr(rcode))
+                    } else {
+                        CStr::from_ptr(err)
+                    }
+                    .to_str()?
+                    .to_string(),
+                    sql: Some(sql.as_ref().into()),
+                };
+                if err != std::ptr::null_mut() {
+                    sq::sqlite3_free(err.cast());
+                }
+                return Err(e);
+            }
+        }
+
+        Ok(())
+    }
+
+    pub(crate) fn with_prepared<R>(
+        &self,
+        hash_key: impl PreparedKey,
+        build_query: impl Fn() -> String,
+        run_query: impl Fn(StatementContext) -> DBResult<R>,
+    ) -> DBResult<R> {
+        let mut data = self.0.lock()?;
+        let conn = data.sqlite;
+
+        use std::collections::hash_map::Entry;
+        match data.stmts.entry(hash_key.into_u64()) {
+            Entry::Vacant(e) => {
+                let sql = build_query();
+
+                // prepare the statement
+                let mut stmt = std::ptr::null_mut();
+                unsafe {
+                    check_rcode(
+                        Some(sql.as_str()),
+                        sq::sqlite3_prepare_v2(
+                            conn,
+                            sql.as_ptr().cast(),
+                            sql.len() as i32,
+                            &mut stmt,
+                            std::ptr::null_mut(),
+                        ),
+                    )?;
+                };
+
+                if stmt == std::ptr::null_mut() {
+                    return Err(Error::InternalError(
+                        "sqlite3_prepare_v2 returned a NULL stmt",
+                    ));
+                }
+
+                let stmt = e.insert(Statement { sqlite: conn, stmt });
+
+                run_query(stmt.make_context()?)
+            }
+            Entry::Occupied(mut e) => run_query(e.get_mut().make_context()?),
+        }
+    }
+}
+
+unsafe impl Send for Connection {}
+
+struct Statement {
+    sqlite: *mut sq::sqlite3,
+    stmt: *mut sq::sqlite3_stmt,
+}
+
+impl Statement {
+    fn make_context(&mut self) -> DBResult<StatementContext> {
+        // begin by resetting the statement
+        unsafe {
+            check_rcode(None, sq::sqlite3_reset(self.stmt))?;
+        }
+        Ok(StatementContext { stmt: self })
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::Connection;
+
+    #[test]
+    fn simple_sql() {
+        let c = Connection::new(":memory:").expect("couldn't open test db");
+        c.execute_raw_sql("CREATE TABLE test_table (id integer primary key, value string)")
+            .expect("couldn't execute sql");
+    }
+
+    #[test]
+    fn prepare_stmt() {
+        let c = Connection::new(":memory:").expect("couldn't open test db");
+        c.execute_raw_sql("CREATE TABLE test_table (id integer primary key, value string)")
+            .expect("couldn't execute sql");
+
+        c.with_prepared(
+            1,
+            || format!("INSERT INTO test_table VALUES (?, ?)"),
+            |ctx| {
+                ctx.bind(1, 1usize)?;
+                ctx.bind(2, "value")?;
+
+                ctx.iter().last();
+
+                Ok(())
+            },
+        )
+        .expect("couldn't run prepared INSERT statement");
+
+        c.with_prepared(
+            2,
+            || format!("SELECT * FROM test_table"),
+            |ctx| {
+                let count = ctx
+                    .iter()
+                    .map(|row| {
+                        assert_eq!(row.read::<i64>(0).expect("couldn't read row ID"), 1);
+                        assert_eq!(
+                            row.read::<String>(1).expect("couldn't read row value"),
+                            "value"
+                        );
+                    })
+                    .count();
+                assert!(count > 0);
+
+                Ok(())
+            },
+        )
+        .expect("couldn't run prepared SELECT statement");
+    }
+}
+
+pub struct StatementRow<'a> {
+    stmt: &'a Statement,
+}
+
+impl<'a> StatementRow<'a> {
+    pub fn read<T: Readable>(&self, index: i32) -> DBResult<T> {
+        T::read_from(self, index)
+    }
+}
+
+pub struct StatementContext<'a> {
+    stmt: &'a Statement,
+}
+
+impl<'a> StatementContext<'a> {
+    pub fn bind<B: Bindable>(&self, index: i32, bindable: B) -> DBResult<()> {
+        bindable.bind_to(self, index)
+    }
+
+    fn step(&self) -> Option<()> {
+        match unsafe { sq::sqlite3_step(self.stmt.stmt) } {
+            sq::SQLITE_ROW => Some(()),
+            sq::SQLITE_DONE => None,
+            _ => {
+                // check_rcode(None, v)?;
+                // Ok(false)
+                None
+            }
+        }
+    }
+
+    pub fn run(self) -> DBResult<Option<StatementRow<'a>>> {
+        if self.step().is_some() {
+            Ok(Some(StatementRow { stmt: self.stmt }))
+        } else {
+            Ok(None)
+        }
+    }
+
+    pub fn iter(self) -> impl Iterator<Item = StatementRow<'a>> {
+        struct I<'a>(StatementContext<'a>);
+
+        impl<'a> Iterator for I<'a> {
+            type Item = StatementRow<'a>;
+
+            fn next(&mut self) -> Option<Self::Item> {
+                self.0.step().map(|_| StatementRow { stmt: self.0.stmt })
+            }
+        }
+
+        I(self)
+    }
+}
+
+impl<'a> Drop for StatementContext<'a> {
+    fn drop(&mut self) {
+        // attempt to bind NULLs into each parameter
+        unsafe {
+            sq::sqlite3_clear_bindings(self.stmt.stmt);
+        }
+    }
+}
+
+pub trait Bindable {
+    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()>;
+}
+
+impl<'a> Bindable for () {
+    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+        unsafe { check_rcode(None, sq::sqlite3_bind_null(ctx.stmt.stmt, index)) }
+    }
+}
+
+impl<'a> Bindable for i64 {
+    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+        unsafe { check_rcode(None, sq::sqlite3_bind_int64(ctx.stmt.stmt, index, *self)) }
+    }
+}
+
+impl<'a> Bindable for usize {
+    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+        (*self as i64).bind_to(ctx, index)
+    }
+}
+
+impl<'a> Bindable for &'a str {
+    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+        unsafe {
+            check_rcode(
+                None,
+                sq::sqlite3_bind_text(
+                    ctx.stmt.stmt,
+                    index,
+                    self.as_ptr().cast(),
+                    self.len() as i32,
+                    sq::SQLITE_STATIC(),
+                ),
+            )
+        }
+    }
+}
+
+impl Bindable for str {
+    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+        <&'_ str>::bind_to(&self, ctx, index)
+    }
+}
+
+impl Bindable for String {
+    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+        self.as_str().bind_to(ctx, index)
+    }
+}
+
+impl<'a> Bindable for &'a [u8] {
+    fn bind_to(&self, ctx: &StatementContext, index: i32) -> DBResult<()> {
+        unsafe {
+            check_rcode(
+                None,
+                sq::sqlite3_bind_blob64(
+                    ctx.stmt.stmt,
+                    index,
+                    self.as_ptr().cast(),
+                    self.len() as u64,
+                    sq::SQLITE_STATIC(),
+                ),
+            )
+        }
+    }
+}
+
+pub trait Readable: Sized {
+    fn read_from(sr: &StatementRow<'_>, index: i32) -> DBResult<Self>;
+}
+
+impl Readable for i64 {
+    fn read_from(sr: &StatementRow<'_>, index: i32) -> DBResult<Self> {
+        unsafe { Ok(sq::sqlite3_column_int64(sr.stmt.stmt, index)) }
+    }
+}
+
+impl Readable for String {
+    fn read_from(sr: &StatementRow<'_>, index: i32) -> DBResult<Self> {
+        unsafe {
+            let text = sq::sqlite3_column_text(sr.stmt.stmt, index);
+            if text.is_null() {
+                Err(Error::InternalError(
+                    "NULL pointer result from sqlite3_column_text",
+                ))
+            } else {
+                Ok(CStr::from_ptr(text.cast()).to_str()?.to_string())
+            }
+        }
+    }
+}
+
+impl Readable for Vec<u8> {
+    fn read_from(sr: &StatementRow<'_>, index: i32) -> DBResult<Self> {
+        unsafe {
+            let ptr = sq::sqlite3_column_blob(sr.stmt.stmt, index);
+            let len = sq::sqlite3_column_bytes(sr.stmt.stmt, index);
+
+            if len == 0 {
+                Ok(vec![])
+            } else if len > 0 {
+                Ok(std::slice::from_raw_parts(ptr.cast(), len as usize).to_vec())
+            } else {
+                Err(Error::InternalError(
+                    "negative length returned from sqlite3_column_bytes",
+                ))
+            }
+        }
+    }
+}
+
+/*
+
+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
@@ -54,7 +438,7 @@ mod sendsync_check {
 */
 
 impl Connection {
-    pub fn open<U: AsRef<str>>(uri: U) -> Result<DBConnection, DBError> {
+    pub fn open<U: AsRef<str>>(uri: U) -> Result<DBConnection, Error> {
         match sqlite::Connection::open_thread_safe_with_flags(
             uri.as_ref(),
             sqlite::OpenFlags::new()
@@ -69,11 +453,11 @@ impl Connection {
                     statement_cache: Default::default(),
                 }))
             }
-            Err(e) => Err(DBError::Sqlite(e)),
+            Err(e) => Err(Error::Sqlite(e)),
         }
     }
 
-    pub(crate) fn execute_raw(&self, sql: &str) -> Result<(), DBError> {
+    pub(crate) fn execute_raw(&self, sql: &str) -> Result<(), Error> {
         Ok(self.conn.execute(sql)?)
     }
 
@@ -90,7 +474,7 @@ impl Connection {
                 let q: sqlite::Statement<'static> = self
                     .conn
                     .prepare(sql.as_str())
-                    .map_err(|e| DBError::from(e).sqlite_to_query(sql.as_str()))?;
+                    .map_err(|e| Error::from(e).sqlite_to_query(sql.as_str()))?;
 
                 log::trace!("prepared new SQL query: {sql}");
 
@@ -125,3 +509,5 @@ impl std::fmt::Debug for Connection {
         f.write_str("microrm::Connection")
     }
 }
+
+*/

+ 18 - 20
microrm/src/lib.rs

@@ -5,9 +5,6 @@ pub mod db;
 mod query;
 pub mod schema;
 
-#[doc(hidden)]
-pub use sqlite;
-
 pub mod prelude {
     pub use crate::query::{AssocInterface, Queryable};
     // pub use crate::schema::entity::Entity;
@@ -20,37 +17,38 @@ pub mod prelude {
 // ----------------------------------------------------------------------
 
 #[derive(Debug)]
-pub enum DBError {
+pub enum Error {
     EmptyResult,
     UnknownValue(String),
     IncompatibleSchema,
+    InternalError(&'static str),
+    EncodingError(std::str::Utf8Error),
     LogicError(&'static str),
-    Sqlite(sqlite::Error),
-    Query(String, sqlite::Error),
+    Sqlite {
+        code: i32,
+        msg: String,
+        sql: Option<String>,
+    },
     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<T> From<std::sync::PoisonError<T>> for Error {
+    fn from(value: std::sync::PoisonError<T>) -> Self {
+        Self::LockError(value.to_string())
     }
 }
 
-impl From<sqlite::Error> for DBError {
-    fn from(value: sqlite::Error) -> Self {
-        Self::Sqlite(value)
+impl From<std::ffi::NulError> for Error {
+    fn from(_value: std::ffi::NulError) -> Self {
+        Self::InternalError("NULL pointer encountered in unexpected location")
     }
 }
 
-impl<T> From<std::sync::PoisonError<T>> for DBError {
-    fn from(value: std::sync::PoisonError<T>) -> Self {
-        Self::LockError(value.to_string())
+impl From<std::str::Utf8Error> for Error {
+    fn from(value: std::str::Utf8Error) -> Self {
+        Self::EncodingError(value)
     }
 }
 
-pub type DBResult<T> = Result<T, DBError>;
+pub type DBResult<T> = Result<T, Error>;

+ 47 - 65
microrm/src/query.rs

@@ -1,17 +1,17 @@
-use crate::db::DBConnection;
+use crate::db::{Connection, StatementContext, StatementRow};
 use crate::schema::entity::helpers::check_assoc;
 use crate::schema::{AssocData, IDWrap, LocalSide};
-use crate::DBResult;
 use crate::{
     schema::datum::{Datum, DatumList},
     schema::entity::{Entity, EntityID, EntityPart, EntityPartList, EntityPartVisitor},
 };
+use crate::{DBResult, Error};
 use std::collections::HashMap;
 use std::hash::{Hash, Hasher};
 
 pub(crate) mod components;
 
-pub(crate) fn insert<E: Entity>(conn: &DBConnection, value: E) -> DBResult<E::ID> {
+pub(crate) fn insert<E: Entity>(conn: &Connection, value: E) -> DBResult<E::ID> {
     struct InsertQuery<E: Entity>(std::marker::PhantomData<E>);
 
     conn.with_prepared(
@@ -48,10 +48,8 @@ pub(crate) fn insert<E: Entity>(conn: &DBConnection, value: E) -> DBResult<E::ID
                 placeholders
             )
         },
-        |prepared| {
-            prepared.reset()?;
-
-            struct PartBinder<'a, 'b>(&'a mut sqlite::Statement<'b>, usize);
+        |mut ctx| {
+            struct PartBinder<'a, 'b>(&'a mut StatementContext<'b>, i32);
             impl<'a, 'b> EntityPartVisitor for PartBinder<'a, 'b> {
                 fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) {
                     // skip associations, as with the query preparation above
@@ -64,11 +62,11 @@ pub(crate) fn insert<E: Entity>(conn: &DBConnection, value: E) -> DBResult<E::ID
                 }
             }
 
-            value.accept_part_visitor_ref(&mut PartBinder(prepared, 1));
-
-            prepared.next()?;
+            value.accept_part_visitor_ref(&mut PartBinder(&mut ctx, 1));
 
-            Ok(<E::ID>::from_raw(prepared.read::<i64, _>(0)?))
+            ctx.run()?
+                .ok_or(Error::InternalError("No result row from INSERT query"))
+                .map(|r| <E::ID>::from_raw(r.read(0).expect("couldn't read resulting ID")))
         },
     )
 }
@@ -263,14 +261,11 @@ pub trait AssocInterface: 'static {
                     remote_field = an.remote_field
                 )
             },
-            |stmt| {
-                stmt.reset()?;
-                stmt.bind((1, adata.local_id))?;
-                stmt.bind((2, remote_id.into_raw()))?;
+            |mut ctx| {
+                ctx.bind(1, adata.local_id)?;
+                ctx.bind(2, remote_id.into_raw())?;
 
-                stmt.next()?;
-
-                Ok(())
+                ctx.run().map(|_| ())
             },
         )
     }
@@ -293,14 +288,11 @@ pub trait AssocInterface: 'static {
                     remote_field = an.remote_field
                 )
             },
-            |stmt| {
-                stmt.reset()?;
-                stmt.bind((1, adata.local_id))?;
-                stmt.bind((2, remote_id.into_raw()))?;
+            |mut ctx| {
+                ctx.bind(1, adata.local_id)?;
+                ctx.bind(2, remote_id.into_raw())?;
 
-                stmt.next()?;
-
-                Ok(())
+                ctx.run().map(|_| ())
             },
         )
     }
@@ -329,39 +321,35 @@ pub trait AssocInterface: 'static {
 // ----------------------------------------------------------------------
 
 pub trait OutputContainer: 'static {
-    fn assemble_from(conn: &DBConnection, stmt: &mut sqlite::Statement<'static>) -> DBResult<Self>
+    fn assemble_from(conn: &Connection, stmt: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized;
 }
 
+fn assemble_single<T: Entity>(conn: &Connection, row: &mut StatementRow) -> IDWrap<T> {
+    let id = row.read::<i64>(0).expect("couldn't read ID");
+    let datum_list = <T::Parts>::build_datum_list(conn, row).expect("couldn't build datum list");
+    IDWrap::new(T::ID::from_raw(id), T::build(datum_list))
+}
+
 impl<T: Entity> OutputContainer for Option<IDWrap<T>> {
-    fn assemble_from(conn: &DBConnection, stmt: &mut sqlite::Statement<'static>) -> DBResult<Self>
+    fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized,
     {
-        if stmt.next()? == sqlite::State::Row {
-            let id = stmt.read::<i64, _>(0)?;
-            let datum_list = <T::Parts>::build_datum_list(conn, stmt)?;
-            Ok(Some(IDWrap::new(T::ID::from_raw(id), T::build(datum_list))))
-        } else {
-            Ok(None)
-        }
+        Ok(ctx.run()?.map(|mut r| assemble_single(conn, &mut r)))
     }
 }
 
 impl<T: Entity> OutputContainer for Vec<IDWrap<T>> {
-    fn assemble_from(conn: &DBConnection, stmt: &mut sqlite::Statement<'static>) -> DBResult<Self>
+    fn assemble_from(conn: &Connection, ctx: StatementContext<'_>) -> DBResult<Self>
     where
         Self: Sized,
     {
-        let mut rows = vec![];
-        while stmt.next()? == sqlite::State::Row {
-            let id = stmt.read::<i64, _>(0)?;
-            let datum_list = <T::Parts>::build_datum_list(conn, stmt)?;
-            rows.push(IDWrap::new(T::ID::from_raw(id), T::build(datum_list)));
-        }
-
-        Ok(rows)
+        Ok(ctx
+            .iter()
+            .map(|mut r| assemble_single(conn, &mut r))
+            .collect())
     }
 }
 
@@ -371,8 +359,8 @@ pub trait Queryable {
     type StaticVersion: Queryable + 'static;
 
     fn build(&self) -> Query;
-    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize);
-    fn conn(&self) -> &DBConnection;
+    fn bind(&self, stmt: &mut StatementContext, index: &mut i32);
+    fn conn(&self) -> &Connection;
 
     // ----------------------------------------------------------------------
     // Verbs
@@ -398,16 +386,15 @@ pub trait Queryable {
                     )
                     .assemble()
             },
-            |stmt| {
-                stmt.reset()?;
-
+            |mut ctx| {
                 // starting index is 1
                 let mut index = 1;
-                self.bind(stmt, &mut index);
+                self.bind(&mut ctx, &mut index);
 
-                stmt.next()?;
-
-                Ok(stmt.read::<i64, _>(0)? as usize)
+                Ok(ctx
+                    .run()?
+                    .ok_or(Error::InternalError("no resulting rows from COUNT query"))?
+                    .read::<i64>(0)? as usize)
             },
         )
     }
@@ -420,14 +407,12 @@ pub trait Queryable {
         self.conn().with_prepared(
             std::any::TypeId::of::<(Self::StaticVersion, GetTag)>(),
             || self.build().assemble(),
-            |stmt| {
-                stmt.reset()?;
-
+            |mut ctx| {
                 // starting index is 1
                 let mut index = 1;
-                self.bind(stmt, &mut index);
+                self.bind(&mut ctx, &mut index);
 
-                <Self::OutputContainer>::assemble_from(self.conn(), stmt)
+                <Self::OutputContainer>::assemble_from(self.conn(), ctx)
             },
         )
     }
@@ -451,15 +436,12 @@ pub trait Queryable {
                         .assemble()
                 )
             },
-            |stmt| {
-                stmt.reset()?;
-
+            |mut ctx| {
                 // starting index is 1
                 let mut index = 1;
-                self.bind(stmt, &mut index);
-
-                stmt.next()?;
+                self.bind(&mut ctx, &mut index);
 
+                ctx.run()?;
                 Ok(())
             },
         )
@@ -531,11 +513,11 @@ impl<'a, AI: AssocInterface> Queryable for &'a AI {
         unreachable!()
     }
 
-    fn bind(&self, _stmt: &mut sqlite::Statement, _index: &mut usize) {
+    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {
         unreachable!()
     }
 
-    fn conn(&self) -> &DBConnection {
+    fn conn(&self) -> &Connection {
         &self.get_data().unwrap().conn
     }
 

+ 15 - 14
microrm/src/query/components.rs

@@ -1,6 +1,7 @@
 //! Component types for query construction.
 
 use crate::{
+    db::{Connection, StatementContext},
     prelude::Queryable,
     query::{AssocInterface, QueryPart},
     schema::{
@@ -34,9 +35,9 @@ impl<'a, E: Entity> Queryable for MapQueryable<'a, E> {
             .attach(QueryPart::Columns, "*".into())
             .attach(QueryPart::From, format!("`{}`", E::entity_name()))
     }
-    fn bind(&self, _stmt: &mut sqlite::Statement, _index: &mut usize) {}
+    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {}
 
-    fn conn(&self) -> &crate::db::DBConnection {
+    fn conn(&self) -> &Connection {
         self.map.conn()
     }
 }
@@ -76,18 +77,18 @@ impl<'a, AI: AssocInterface> Queryable for AssocQueryable<'a, AI> {
                 format!("`{}`.`{}` = ?", assoc_name, anames.local_field),
             )
     }
-    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+    fn bind(&self, ctx: &mut StatementContext, index: &mut i32) {
         let adata = self
             .assoc
             .get_data()
             .expect("binding query for assoc with no data");
 
-        stmt.bind((*index, adata.local_id))
+        ctx.bind(*index, adata.local_id)
             .expect("couldn't bind assoc id");
         *index += 1;
     }
 
-    fn conn(&self) -> &crate::db::DBConnection {
+    fn conn(&self) -> &Connection {
         &self.assoc.get_data().unwrap().conn
     }
 }
@@ -124,13 +125,13 @@ impl<'a, WEP: EntityPart, Parent: Queryable> Queryable for WithComponent<'a, WEP
             ),
         )
     }
-    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+    fn bind(&self, stmt: &mut StatementContext, index: &mut i32) {
         self.parent.bind(stmt, index);
         self.datum.bind_to(stmt, *index);
         *index += 1;
     }
 
-    fn conn(&self) -> &crate::db::DBConnection {
+    fn conn(&self) -> &Connection {
         self.parent.conn()
     }
 }
@@ -177,10 +178,10 @@ impl<'a, E: Entity, Parent: Queryable> Queryable for UniqueComponent<'a, E, Pare
         query
     }
 
-    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+    fn bind(&self, stmt: &mut StatementContext, index: &mut i32) {
         self.parent.bind(stmt, index);
 
-        struct Visitor<'a, 'b>(&'a mut sqlite::Statement<'b>, &'a mut usize);
+        struct Visitor<'a, 'b>(&'a mut StatementContext<'b>, &'a mut i32);
         impl<'a, 'b> DatumVisitor for Visitor<'a, 'b> {
             fn visit<ED: Datum>(&mut self, datum: &ED) {
                 datum.bind_to(self.0, *self.1);
@@ -191,7 +192,7 @@ impl<'a, E: Entity, Parent: Queryable> Queryable for UniqueComponent<'a, E, Pare
         self.datum.accept(&mut Visitor(stmt, index));
     }
 
-    fn conn(&self) -> &crate::db::DBConnection {
+    fn conn(&self) -> &Connection {
         self.parent.conn()
     }
 }
@@ -217,11 +218,11 @@ impl<Parent: Queryable> Queryable for SingleComponent<Parent> {
             .attach(QueryPart::Trailing, "LIMIT 1".into())
     }
 
-    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+    fn bind(&self, stmt: &mut StatementContext, index: &mut i32) {
         self.parent.bind(stmt, index)
     }
 
-    fn conn(&self) -> &crate::db::DBConnection {
+    fn conn(&self) -> &Connection {
         self.parent.conn()
     }
 }
@@ -299,11 +300,11 @@ impl<R: Entity, L: Entity, EP: EntityPart<Entity = L>, Parent: Queryable> Querya
             )
             .replace(QueryPart::Columns, format!("`{remote_name}`.*"))
     }
-    fn bind(&self, stmt: &mut sqlite::Statement, index: &mut usize) {
+    fn bind(&self, stmt: &mut StatementContext, index: &mut i32) {
         self.parent.bind(stmt, index);
     }
 
-    fn conn(&self) -> &crate::db::DBConnection {
+    fn conn(&self) -> &Connection {
         self.parent.conn()
     }
 }

+ 41 - 66
microrm/src/schema.rs

@@ -9,12 +9,12 @@
 use query::Queryable;
 
 use crate::{
-    db::{Connection, DBConnection},
+    db::{Connection, StatementContext, StatementRow},
     query::{self, AssocInterface},
     schema::datum::Datum,
     schema::entity::{Entity, EntityPartList, EntityVisitor},
 };
-use crate::{DBError, DBResult};
+use crate::{DBResult, Error};
 
 pub mod datum;
 pub mod entity;
@@ -112,7 +112,7 @@ pub enum LocalSide {
 
 /// Opaque data structure used for constructing `Assoc{Map,Domain,Range}` instances.
 pub struct AssocData {
-    pub(crate) conn: DBConnection,
+    pub(crate) conn: Connection,
     pub(crate) local_name: &'static str,
     pub(crate) part_name: &'static str,
     pub(crate) local_id: i64,
@@ -148,7 +148,7 @@ impl<T: Entity> AssocInterface for AssocMap<T> {
     fn get_distinguishing_name(&self) -> DBResult<&'static str> {
         self.data
             .as_ref()
-            .ok_or(DBError::LogicError(
+            .ok_or(Error::LogicError(
                 "no distinguishing name for empty AssocMap",
             ))
             .map(|d| d.part_name)
@@ -157,7 +157,7 @@ impl<T: Entity> AssocInterface for AssocMap<T> {
     fn get_data(&self) -> DBResult<&AssocData> {
         self.data
             .as_ref()
-            .ok_or(DBError::LogicError("Reading from unassigned AssocMap"))
+            .ok_or(Error::LogicError("Reading from unassigned AssocMap"))
     }
 }
 
@@ -186,25 +186,18 @@ impl<T: Entity> Datum for AssocMap<T> {
         d.visit_assoc_map::<T>();
     }
 
-    fn bind_to(&self, _stmt: &mut sqlite::Statement, _index: usize) {
+    fn bind_to(&self, _stmt: &mut StatementContext, _index: i32) {
         unreachable!()
     }
 
-    fn build_from(
-        adata: AssocData,
-        _stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(adata: AssocData, _stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((
-            Self {
-                data: Some(adata),
-                _ghost: Default::default(),
-            },
-            index,
-        ))
+        Ok(Self {
+            data: Some(adata),
+            _ghost: Default::default(),
+        })
     }
 }
 
@@ -249,7 +242,7 @@ impl<R: Relation> AssocInterface for AssocDomain<R> {
     fn get_data(&self) -> DBResult<&AssocData> {
         self.data
             .as_ref()
-            .ok_or(DBError::LogicError("Reading from unassigned AssocDomain"))
+            .ok_or(Error::LogicError("Reading from unassigned AssocDomain"))
     }
 }
 
@@ -269,25 +262,18 @@ impl<R: Relation> Datum for AssocDomain<R> {
         d.visit_assoc_domain::<R>();
     }
 
-    fn bind_to(&self, _stmt: &mut sqlite::Statement, _index: usize) {
+    fn bind_to(&self, _stmt: &mut StatementContext, _index: i32) {
         unreachable!()
     }
 
-    fn build_from(
-        adata: AssocData,
-        _stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(adata: AssocData, _stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((
-            Self {
-                data: Some(adata),
-                _ghost: Default::default(),
-            },
-            index,
-        ))
+        Ok(Self {
+            data: Some(adata),
+            _ghost: Default::default(),
+        })
     }
 }
 
@@ -332,7 +318,7 @@ impl<R: Relation> AssocInterface for AssocRange<R> {
     fn get_data(&self) -> DBResult<&AssocData> {
         self.data
             .as_ref()
-            .ok_or(DBError::LogicError("Reading from unassigned AssocRange"))
+            .ok_or(Error::LogicError("Reading from unassigned AssocRange"))
     }
 }
 
@@ -352,25 +338,18 @@ impl<R: Relation> Datum for AssocRange<R> {
         d.visit_assoc_range::<R>();
     }
 
-    fn bind_to(&self, _stmt: &mut sqlite::Statement, _index: usize) {
+    fn bind_to(&self, _stmt: &mut StatementContext, _index: i32) {
         unreachable!()
     }
 
-    fn build_from(
-        adata: AssocData,
-        _stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(adata: AssocData, _stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((
-            Self {
-                data: Some(adata),
-                _ghost: Default::default(),
-            },
-            index,
-        ))
+        Ok(Self {
+            data: Some(adata),
+            _ghost: Default::default(),
+        })
     }
 }
 
@@ -412,7 +391,7 @@ impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned> Datum for Seri
         "text"
     }
 
-    fn bind_to(&self, stmt: &mut sqlite::Statement, index: usize) {
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
         <String as Datum>::bind_to(
             &serde_json::to_string(&self.wrapped).expect("couldn't serialize object into JSON"),
             stmt,
@@ -420,19 +399,15 @@ impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned> Datum for Seri
         )
     }
 
-    fn build_from(
-        adata: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        let (s, idx) = <String as Datum>::build_from(adata, stmt, index)?;
+        let s = <String as Datum>::build_from(adata, stmt, index)?;
 
-        let d = serde_json::from_str::<T>(s.as_str()).map_err(DBError::JSON)?;
+        let d = serde_json::from_str::<T>(s.as_str()).map_err(Error::JSON)?;
 
-        Ok((Self { wrapped: d }, idx))
+        Ok(Self { wrapped: d })
     }
 }
 
@@ -442,19 +417,19 @@ impl<T: 'static + serde::Serialize + serde::de::DeserializeOwned> Datum for Seri
 
 /// Table with EntityID-based lookup.
 pub struct IDMap<T: Entity> {
-    conn: DBConnection,
+    conn: Connection,
     _ghost: std::marker::PhantomData<T>,
 }
 
 impl<T: Entity> IDMap<T> {
-    pub fn build(db: DBConnection) -> Self {
+    pub fn build(db: Connection) -> Self {
         Self {
             conn: db,
             _ghost: std::marker::PhantomData,
         }
     }
 
-    pub(crate) fn conn(&self) -> &DBConnection {
+    pub(crate) fn conn(&self) -> &Connection {
         &self.conn
     }
 
@@ -477,11 +452,11 @@ impl<'a, T: Entity> Queryable for &'a IDMap<T> {
     fn build(&self) -> query::Query {
         unreachable!()
     }
-    fn bind(&self, _stmt: &mut sqlite::Statement, _index: &mut usize) {
+    fn bind(&self, _stmt: &mut StatementContext, _index: &mut i32) {
         unreachable!()
     }
 
-    fn conn(&self) -> &DBConnection {
+    fn conn(&self) -> &Connection {
         &self.conn
     }
 
@@ -533,7 +508,7 @@ impl<'a, T: Entity> Queryable for &'a IDMap<T> {
 }
 
 pub struct Index<T: Entity, Key: Datum> {
-    _conn: DBConnection,
+    _conn: Connection,
     _ghost: std::marker::PhantomData<(T, Key)>,
 }
 
@@ -573,30 +548,30 @@ impl<T: Entity> DatabaseSpec for IDMap<T> {
 
 /// A root structure for the database specification graph.
 pub trait Database {
-    fn open_path<U: AsRef<str>>(uri: U) -> Result<Self, DBError>
+    fn open_path<U: AsRef<str>>(uri: U) -> DBResult<Self>
     where
         Self: Sized,
     {
-        let conn = Connection::open(uri)?;
+        let conn = Connection::new(uri.as_ref())?;
         let schema = build::collect_from_database::<Self>();
         match schema.check(conn.clone()) {
             // schema checks out
             Some(true) => {}
             // schema doesn't match
-            Some(false) => Err(DBError::IncompatibleSchema)?,
+            Some(false) => Err(Error::IncompatibleSchema)?,
             // no schema found
             None => {
                 schema.create(conn.clone())?;
             }
         }
 
-        conn.execute_raw("PRAGMA foreign_keys = ON")?;
+        conn.execute_raw_sql("PRAGMA foreign_keys = ON")?;
 
         Ok(Self::build(conn))
     }
 
     #[doc(hidden)]
-    fn build(conn: DBConnection) -> Self
+    fn build(conn: Connection) -> Self
     where
         Self: Sized;
 

+ 5 - 5
microrm/src/schema/build.rs

@@ -2,7 +2,7 @@ use crate::{
     query::Queryable,
     schema::{
         collect::{EntityStateContainer, PartType},
-        meta, DBConnection, Database, DatabaseItem, DatabaseItemVisitor,
+        meta, Connection, Database, DatabaseItem, DatabaseItemVisitor,
     },
     DBResult,
 };
@@ -69,7 +69,7 @@ impl DatabaseSchema {
     /// - yes, this is a schema match (true)
     /// - no, this is not a schema match (false)
     /// - there is no schema that we know of (None)
-    pub fn check(&self, db: DBConnection) -> Option<bool> {
+    pub fn check(&self, db: Connection) -> Option<bool> {
         // attempt to use connection as a MetadataDB database
         let metadb = meta::MetadataDB::build(db);
 
@@ -83,16 +83,16 @@ impl DatabaseSchema {
             .map(|kv| kv.value.parse::<u64>().unwrap_or(0) == self.signature)
     }
 
-    pub fn create(&self, db: DBConnection) -> DBResult<()> {
+    pub fn create(&self, db: Connection) -> DBResult<()> {
         for query in self.queries.iter() {
-            db.execute_raw(query)?;
+            db.execute_raw_sql(query)?;
         }
 
         // attempt to use connection as a MetadataDB database
         let metadb = meta::MetadataDB::build(db.clone());
 
         for query in collect_from_database::<meta::MetadataDB>().queries.iter() {
-            db.execute_raw(query)?;
+            db.execute_raw_sql(query)?;
         }
 
         // store signature

+ 3 - 6
microrm/src/schema/datum.rs

@@ -1,4 +1,5 @@
 use crate::{
+    db::{StatementContext, StatementRow},
     schema::{AssocData, DatumDiscriminator, EntityVisitor},
     DBResult,
 };
@@ -16,12 +17,8 @@ mod datum_list;
 pub trait Datum {
     fn sql_type() -> &'static str;
 
-    fn bind_to(&self, _stmt: &mut sqlite::Statement, index: usize);
-    fn build_from(
-        adata: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn bind_to(&self, _stmt: &mut StatementContext, index: i32);
+    fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized;
 

+ 42 - 78
microrm/src/schema/datum/datum_common.rs

@@ -1,6 +1,7 @@
 use crate::{
+    db::{StatementContext, StatementRow},
     schema::{AssocData, Datum},
-    DBError, DBResult,
+    DBResult, Error,
 };
 
 impl Datum for time::OffsetDateTime {
@@ -8,24 +9,17 @@ impl Datum for time::OffsetDateTime {
         "text"
     }
 
-    fn bind_to(&self, stmt: &mut sqlite::Statement, index: usize) {
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
         let ts = self.unix_timestamp();
         ts.bind_to(stmt, index)
     }
 
-    fn build_from(
-        adata: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     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,
-        ))
+        let unix = i64::build_from(adata, stmt, index)?;
+        Ok(Self::from_unix_timestamp(unix).map_err(|e| Error::UnknownValue(e.to_string()))?)
     }
 }
 
@@ -34,20 +28,18 @@ impl Datum for String {
         "text"
     }
 
-    fn bind_to(&self, stmt: &mut sqlite::Statement, index: usize) {
-        stmt.bind((index, self.as_str()))
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+        stmt.bind(index, self.as_str())
             .expect("couldn't bind string");
     }
 
-    fn build_from(
-        _: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(_: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((stmt.read(index)?, index + 1))
+        let val = stmt.read(*index)?;
+        *index += 1;
+        Ok(val)
     }
 }
 
@@ -56,20 +48,16 @@ impl Datum for usize {
         "int"
     }
 
-    fn bind_to(&self, stmt: &mut sqlite::Statement, index: usize) {
-        stmt.bind((index, *self as i64))
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+        stmt.bind(index, *self as i64)
             .expect("couldn't bind usize as i64");
     }
 
-    fn build_from(
-        _: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((stmt.read::<i64, _>(index)? as usize, index + 1))
+        Ok(i64::build_from(adata, stmt, index)? as usize)
     }
 }
 
@@ -78,19 +66,15 @@ impl Datum for isize {
         "int"
     }
 
-    fn bind_to(&self, _stmt: &mut sqlite::Statement, _index: usize) {
+    fn bind_to(&self, _stmt: &mut StatementContext, _index: i32) {
         todo!()
     }
 
-    fn build_from(
-        _: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((stmt.read::<i64, _>(index)? as isize, index + 1))
+        Ok(i64::build_from(adata, stmt, index)? as isize)
     }
 }
 
@@ -99,19 +83,15 @@ impl Datum for u64 {
         "int"
     }
 
-    fn bind_to(&self, _stmt: &mut sqlite::Statement, _index: usize) {
+    fn bind_to(&self, _stmt: &mut StatementContext, _index: i32) {
         todo!()
     }
 
-    fn build_from(
-        _: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((stmt.read::<i64, _>(index)? as u64, index + 1))
+        Ok(i64::build_from(adata, stmt, index)? as u64)
     }
 }
 
@@ -120,19 +100,17 @@ impl Datum for i64 {
         "int"
     }
 
-    fn bind_to(&self, stmt: &mut sqlite::Statement, index: usize) {
-        stmt.bind((index, *self)).expect("couldn't bind i64")
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+        stmt.bind(index, *self).expect("couldn't bind i64")
     }
 
-    fn build_from(
-        _: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(_: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((stmt.read(index)?, index + 1))
+        let val = stmt.read(*index)?;
+        *index += 1;
+        Ok(val)
     }
 }
 
@@ -141,15 +119,11 @@ impl<T: Datum> Datum for Option<T> {
         T::sql_type()
     }
 
-    fn bind_to(&self, _stmt: &mut sqlite::Statement, _index: usize) {
+    fn bind_to(&self, _stmt: &mut StatementContext, _index: i32) {
         todo!()
     }
 
-    fn build_from(
-        _: AssocData,
-        _stmt: &mut sqlite::Statement,
-        _index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(_: AssocData, _stmt: &mut StatementRow, _index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
@@ -163,20 +137,16 @@ impl Datum for bool {
         "int"
     }
 
-    fn bind_to(&self, stmt: &mut sqlite::Statement, index: usize) {
-        stmt.bind((index, if *self { 1 } else { 0 }))
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+        stmt.bind(index, if *self { 1i64 } else { 0i64 })
             .expect("couldn't bind bool");
     }
 
-    fn build_from(
-        _: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(adata: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((stmt.read::<i64, _>(index)? == 1, index + 1))
+        Ok(i64::build_from(adata, stmt, index)? != 0)
     }
 }
 
@@ -185,20 +155,18 @@ impl Datum for Vec<u8> {
         "blob"
     }
 
-    fn bind_to(&self, stmt: &mut sqlite::Statement, index: usize) {
-        stmt.bind((index, self.as_slice()))
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
+        stmt.bind(index, self.as_slice())
             .expect("couldn't bind Vec<u8>");
     }
 
-    fn build_from(
-        _: AssocData,
-        stmt: &mut sqlite::Statement,
-        index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(_: AssocData, stmt: &mut StatementRow, index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {
-        Ok((stmt.read::<Vec<u8>, _>(index)?, index + 1))
+        let val = stmt.read(*index)?;
+        *index += 1;
+        Ok(val)
     }
 }
 
@@ -207,15 +175,11 @@ impl<'l, T: Datum> Datum for &'l T {
         T::sql_type()
     }
 
-    fn bind_to(&self, stmt: &mut sqlite::Statement, index: usize) {
+    fn bind_to(&self, stmt: &mut StatementContext, index: i32) {
         T::bind_to(self, stmt, index)
     }
 
-    fn build_from(
-        _adata: AssocData,
-        _stmt: &mut sqlite::Statement,
-        _index: usize,
-    ) -> DBResult<(Self, usize)>
+    fn build_from(_adata: AssocData, _stmt: &mut StatementRow, _index: &mut i32) -> DBResult<Self>
     where
         Self: Sized,
     {

+ 0 - 69
microrm/src/schema/datum/datum_list.rs

@@ -62,72 +62,3 @@ datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5);
 datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6);
 datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7);
 datum_list!(T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8);
-
-/*
-impl<T0: Datum, T1: Datum> DatumList for (T0, T1) {
-    type Ref<'a> = (&'a T0, &'a T1) where Self: 'a;
-    fn accept(&self, visitor: &mut impl DatumVisitor) {
-        visitor.visit(&self.0);
-        visitor.visit(&self.1);
-    }
-}
-
-impl<'a, T0: Datum, T1: Datum> DatumListRef for (&'a T0, &'a T1) {
-    fn accept(&self, visitor: &mut impl DatumVisitor) {
-        visitor.visit(&self.0);
-        visitor.visit(&self.1);
-    }
-}
-
-impl<T0: Datum, T1: Datum, T2: Datum> DatumList for (T0, T1, T2) {
-    type Ref<'a> = (&'a T0, &'a T1, &'a T2) where Self: 'a;
-    fn accept(&self, visitor: &mut impl DatumVisitor) {
-        visitor.visit(&self.0);
-        visitor.visit(&self.1);
-        visitor.visit(&self.2);
-    }
-}
-
-impl<'a, T0: Datum, T1: Datum, T2: Datum> DatumListRef for (&'a T0, &'a T1, &'a T2) {
-    fn accept(&self, visitor: &mut impl DatumVisitor) {
-        visitor.visit(&self.0);
-        visitor.visit(&self.1);
-        visitor.visit(&self.2);
-    }
-}
-
-impl<T0: Datum, T1: Datum, T2: Datum, T3: Datum> DatumList for (T0, T1, T2, T3) {
-    type Ref<'a> = (&'a T0, &'a T1, &'a T2, &'a T3) where Self: 'a;
-    fn accept(&self, visitor: &mut impl DatumVisitor) {
-        visitor.visit(&self.0);
-        visitor.visit(&self.1);
-        visitor.visit(&self.2);
-        visitor.visit(&self.3);
-    }
-}
-
-impl<T0: Datum, T1: Datum, T2: Datum, T3: Datum, T4: Datum> DatumList for (T0, T1, T2, T3, T4) {
-    type Ref<'a> = (&'a T0, &'a T1, &'a T2, &'a T3, &'a T4);
-    fn accept(&self, visitor: &mut impl DatumVisitor) {
-        visitor.visit(&self.0);
-        visitor.visit(&self.1);
-        visitor.visit(&self.2);
-        visitor.visit(&self.3);
-        visitor.visit(&self.4);
-    }
-}
-
-impl<T0: Datum, T1: Datum, T2: Datum, T3: Datum, T4: Datum, T5: Datum> DatumList
-    for (T0, T1, T2, T3, T4, T5)
-{
-    type Ref<'a> = (&'a T0, &'a T1, &'a T2, &'a T3, &'a T4, &'a T5);
-    fn accept(&self, visitor: &mut impl DatumVisitor) {
-        visitor.visit(&self.0);
-        visitor.visit(&self.1);
-        visitor.visit(&self.2);
-        visitor.visit(&self.3);
-        visitor.visit(&self.4);
-        visitor.visit(&self.5);
-    }
-}
-*/

+ 2 - 5
microrm/src/schema/entity.rs

@@ -1,7 +1,7 @@
 use std::{fmt::Debug, hash::Hash};
 
 use crate::{
-    db::DBConnection,
+    db::{Connection, StatementRow},
     schema::datum::{Datum, DatumList},
     DBResult,
 };
@@ -43,10 +43,7 @@ pub trait EntityPartVisitor {
 pub trait EntityPartList: 'static {
     type DatumList: DatumList;
 
-    fn build_datum_list(
-        conn: &DBConnection,
-        stmt: &mut sqlite::Statement<'static>,
-    ) -> DBResult<Self::DatumList>;
+    fn build_datum_list(conn: &Connection, stmt: &mut StatementRow) -> DBResult<Self::DatumList>;
 
     fn accept_part_visitor(_: &mut impl EntityPartVisitor);
     fn accept_part_visitor_ref(datum_list: &Self::DatumList, _: &mut impl EntityPartVisitor);

+ 21 - 47
microrm/src/schema/entity/part_list.rs

@@ -1,10 +1,14 @@
-use crate::{db::DBConnection, schema::AssocData, DBResult};
+use crate::{
+    db::{Connection, StatementRow},
+    schema::AssocData,
+    DBResult,
+};
 
 use super::{Datum, Entity, EntityPart, EntityPartList, EntityPartVisitor};
 
 macro_rules! build_datum {
     ($conn:ident,$local_id:ident,$stmt:ident,$idx:ident,$d:ident,$t:ident) => {
-        let ($d, $idx) = <$t::Datum as Datum>::build_from(
+        let $d = <$t::Datum as Datum>::build_from(
             AssocData {
                 conn: $conn.clone(),
                 local_name: <$t::Entity as Entity>::entity_name(),
@@ -12,7 +16,7 @@ macro_rules! build_datum {
                 local_id: $local_id,
             },
             $stmt,
-            $idx,
+            &mut $idx,
         )?;
     };
 }
@@ -20,10 +24,7 @@ macro_rules! build_datum {
 impl EntityPartList for () {
     type DatumList = ();
 
-    fn build_datum_list(
-        _conn: &DBConnection,
-        _stmt: &mut sqlite::Statement<'static>,
-    ) -> DBResult<Self::DatumList> {
+    fn build_datum_list(_conn: &Connection, _stmt: &mut StatementRow) -> DBResult<Self::DatumList> {
         Ok(())
     }
 
@@ -34,15 +35,11 @@ impl EntityPartList for () {
 impl<P0: EntityPart> EntityPartList for P0 {
     type DatumList = P0::Datum;
 
-    fn build_datum_list(
-        conn: &DBConnection,
-        stmt: &mut sqlite::Statement<'static>,
-    ) -> DBResult<Self::DatumList> {
+    fn build_datum_list(conn: &Connection, stmt: &mut StatementRow) -> DBResult<Self::DatumList> {
         let local_id: i64 = stmt.read(0)?;
-        let idx = 1; // starting index is 1 since index 0 is the ID
+        let mut idx = 1; // starting index is 1 since index 0 is the ID
         build_datum!(conn, local_id, stmt, idx, d0, P0);
 
-        let _ = idx;
         Ok(d0)
     }
 
@@ -57,10 +54,7 @@ impl<P0: EntityPart> EntityPartList for P0 {
 impl<P0: EntityPart> EntityPartList for (P0,) {
     type DatumList = P0::Datum;
 
-    fn build_datum_list(
-        conn: &DBConnection,
-        stmt: &mut sqlite::Statement<'static>,
-    ) -> DBResult<Self::DatumList> {
+    fn build_datum_list(conn: &Connection, stmt: &mut StatementRow) -> DBResult<Self::DatumList> {
         <P0 as EntityPartList>::build_datum_list(conn, stmt)
     }
 
@@ -75,16 +69,12 @@ impl<P0: EntityPart> EntityPartList for (P0,) {
 impl<P0: EntityPart, P1: EntityPart> EntityPartList for (P0, P1) {
     type DatumList = (P0::Datum, P1::Datum);
 
-    fn build_datum_list(
-        conn: &DBConnection,
-        stmt: &mut sqlite::Statement<'static>,
-    ) -> DBResult<Self::DatumList> {
+    fn build_datum_list(conn: &Connection, stmt: &mut StatementRow) -> DBResult<Self::DatumList> {
         let local_id: i64 = stmt.read(0)?;
-        let idx = 1; // starting index is 1 since index 0 is the ID
+        let mut idx = 1; // starting index is 1 since index 0 is the ID
         build_datum!(conn, local_id, stmt, idx, d0, P0);
         build_datum!(conn, local_id, stmt, idx, d1, P1);
 
-        let _ = idx;
         Ok((d0, d1))
     }
 
@@ -101,17 +91,13 @@ impl<P0: EntityPart, P1: EntityPart> EntityPartList for (P0, P1) {
 impl<P0: EntityPart, P1: EntityPart, P2: EntityPart> EntityPartList for (P0, P1, P2) {
     type DatumList = (P0::Datum, P1::Datum, P2::Datum);
 
-    fn build_datum_list(
-        conn: &DBConnection,
-        stmt: &mut sqlite::Statement<'static>,
-    ) -> DBResult<Self::DatumList> {
+    fn build_datum_list(conn: &Connection, stmt: &mut StatementRow) -> DBResult<Self::DatumList> {
         let local_id: i64 = stmt.read(0)?;
-        let idx = 1; // starting index is 1 since index 0 is the ID
+        let mut idx = 1; // starting index is 1 since index 0 is the ID
         build_datum!(conn, local_id, stmt, idx, d0, P0);
         build_datum!(conn, local_id, stmt, idx, d1, P1);
         build_datum!(conn, local_id, stmt, idx, d2, P2);
 
-        let _ = idx;
         Ok((d0, d1, d2))
     }
 
@@ -132,18 +118,14 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart> EntityPartL
 {
     type DatumList = (P0::Datum, P1::Datum, P2::Datum, P3::Datum);
 
-    fn build_datum_list(
-        conn: &DBConnection,
-        stmt: &mut sqlite::Statement<'static>,
-    ) -> DBResult<Self::DatumList> {
+    fn build_datum_list(conn: &Connection, stmt: &mut StatementRow) -> DBResult<Self::DatumList> {
         let local_id: i64 = stmt.read(0)?;
-        let idx = 1; // starting index is 1 since index 0 is the ID
+        let mut idx = 1; // starting index is 1 since index 0 is the ID
         build_datum!(conn, local_id, stmt, idx, d0, P0);
         build_datum!(conn, local_id, stmt, idx, d1, P1);
         build_datum!(conn, local_id, stmt, idx, d2, P2);
         build_datum!(conn, local_id, stmt, idx, d3, P3);
 
-        let _ = idx;
         Ok((d0, d1, d2, d3))
     }
 
@@ -166,19 +148,15 @@ impl<P0: EntityPart, P1: EntityPart, P2: EntityPart, P3: EntityPart, P4: EntityP
 {
     type DatumList = (P0::Datum, P1::Datum, P2::Datum, P3::Datum, P4::Datum);
 
-    fn build_datum_list(
-        conn: &DBConnection,
-        stmt: &mut sqlite::Statement<'static>,
-    ) -> DBResult<Self::DatumList> {
+    fn build_datum_list(conn: &Connection, stmt: &mut StatementRow) -> DBResult<Self::DatumList> {
         let local_id: i64 = stmt.read(0)?;
-        let idx = 1; // starting index is 1 since index 0 is the ID
+        let mut idx = 1; // starting index is 1 since index 0 is the ID
         build_datum!(conn, local_id, stmt, idx, d0, P0);
         build_datum!(conn, local_id, stmt, idx, d1, P1);
         build_datum!(conn, local_id, stmt, idx, d2, P2);
         build_datum!(conn, local_id, stmt, idx, d3, P3);
         build_datum!(conn, local_id, stmt, idx, d4, P4);
 
-        let _ = idx;
         Ok((d0, d1, d2, d3, d4))
     }
 
@@ -216,12 +194,9 @@ impl<
         P5::Datum,
     );
 
-    fn build_datum_list(
-        conn: &DBConnection,
-        stmt: &mut sqlite::Statement<'static>,
-    ) -> DBResult<Self::DatumList> {
+    fn build_datum_list(conn: &Connection, stmt: &mut StatementRow) -> DBResult<Self::DatumList> {
         let local_id: i64 = stmt.read(0)?;
-        let idx = 1; // starting index is 1 since index 0 is the ID
+        let mut idx = 1; // starting index is 1 since index 0 is the ID
         build_datum!(conn, local_id, stmt, idx, d0, P0);
         build_datum!(conn, local_id, stmt, idx, d1, P1);
         build_datum!(conn, local_id, stmt, idx, d2, P2);
@@ -229,7 +204,6 @@ impl<
         build_datum!(conn, local_id, stmt, idx, d4, P4);
         build_datum!(conn, local_id, stmt, idx, d5, P5);
 
-        let _ = idx;
         Ok((d0, d1, d2, d3, d4, d5))
     }
 

+ 7 - 7
microrm/src/schema/tests.rs

@@ -9,7 +9,7 @@ fn open_test_db<DB: super::Database>(identifier: &'static str) -> DB {
 mod manual_test_db {
     // simple hand-built database example
 
-    use crate::db::DBConnection;
+    use crate::db::{Connection, StatementContext, StatementRow};
     use crate::schema::datum::Datum;
     use crate::schema::entity::{
         Entity, EntityID, EntityPart, EntityPartList, EntityPartVisitor, EntityVisitor,
@@ -35,14 +35,14 @@ mod manual_test_db {
             d.visit_entity_id::<<Self as EntityID>::Entity>();
         }
 
-        fn bind_to<'a>(&self, _stmt: &mut sqlite::Statement<'a>, index: usize) {
+        fn bind_to<'a>(&self, _stmt: &mut StatementContext<'a>, index: i32) {
             todo!()
         }
         fn build_from<'a>(
             adata: crate::schema::AssocData,
-            stmt: &mut sqlite::Statement<'a>,
-            index: usize,
-        ) -> crate::DBResult<(Self, usize)>
+            stmt: &mut StatementRow,
+            index: &mut i32,
+        ) -> crate::DBResult<Self>
         where
             Self: Sized,
         {
@@ -116,7 +116,7 @@ mod manual_test_db {
     }
 
     impl Database for SimpleDatabase {
-        fn build(conn: DBConnection) -> Self
+        fn build(conn: Connection) -> Self
         where
             Self: Sized,
         {
@@ -417,7 +417,7 @@ mod mutual_relationship {
 }
 
 mod reserved_words {
-    use crate::db::DBConnection;
+    use crate::db::Connection;
     use crate::prelude::*;
     use crate::schema::entity::Entity;
     use crate::schema::{AssocDomain, AssocRange, Database, IDMap};