diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..4d8d8c0
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,122 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
+dependencies = [
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
+
+[[package]]
+name = "variadics_please"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "wyrd"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "variadics_please",
+ "wyrd_sqlite",
+]
+
+[[package]]
+name = "wyrd_sqlite"
+version = "0.1.0"
+dependencies = [
+ "bitflags",
+ "libsqlite3-sys",
+ "thiserror",
+ "variadics_please",
+]
diff --git a/Cargo.toml b/Cargo.toml
index e01005b..636ecc3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,9 +1,21 @@
-[package]
-name = "wyrd"
-version = "0.1.0"
+[workspace]
+resolver = "3"
+members = ["wyrd_sqlite"]
+
+[workspace.package]
edition = "2024"
authors = ["wires <wires@wires.systems>"]
license = "GPL-3.0-only"
repository = "https://git.wires.systems/wyrd"
+[package]
+name = "wyrd"
+version = "0.1.0"
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+
[dependencies]
+anyhow = "1.0.100"
+wyrd_sqlite = { path = "wyrd_sqlite" }
diff --git a/src/main.rs b/src/main.rs
index 5b2e5a6..279e701 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,31 @@
-fn main() {
- println!("hello, world!");
+use wyrd_sqlite::Connection;
+
+fn main() -> anyhow::Result<()> {
+ let conn = Connection::open("")?;
+
+ conn.execute(
+ "CREATE TABLE pairs (
+ a INTEGER PRIMARY KEY,
+ b INTEGER
+ )",
+ (),
+ )?;
+
+ let (mut insert, _) = conn.prepare("INSERT INTO pairs (a, b) VALUES (?, ?)")?;
+
+ insert.execute((23, ()))?;
+ insert.execute((3, 33))?;
+ insert.execute(((), 5))?;
+
+ let (mut stmt, _) = conn.prepare("SELECT * FROM pairs")?;
+ let mut query = stmt.query(())?;
+
+ while let Some(mut row) = query.try_next_row()? {
+ let a: i32 = { row.get(0)? };
+ let b: Option<i32> = { row.get(1)? };
+
+ println!("{a}, {b:?}");
+ }
+
+ Ok(())
}
diff --git a/wyrd_sqlite/Cargo.toml b/wyrd_sqlite/Cargo.toml
new file mode 100644
index 0000000..4b249f5
--- /dev/null
+++ b/wyrd_sqlite/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "wyrd_sqlite"
+version = "0.1.0"
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+bitflags = "2.10.0"
+libsqlite3-sys = { version = "0.35.0", features = [
+ "bundled_bindings"
+]}
+thiserror = "2.0.17"
+variadics_please = "1.1.0"
diff --git a/wyrd_sqlite/src/error.rs b/wyrd_sqlite/src/error.rs
new file mode 100644
index 0000000..281862f
--- /dev/null
+++ b/wyrd_sqlite/src/error.rs
@@ -0,0 +1,84 @@
+use std::{
+ ffi::{CStr, NulError, c_int},
+ fmt::{self, Display, Formatter},
+ str::Utf8Error,
+};
+
+use crate::{FromSql, ffi};
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error(transparent)]
+ Sqlite(#[from] SqliteError),
+ #[error("input contained no SQL")]
+ EmptyStatement,
+ #[error(transparent)]
+ Nul(#[from] NulError),
+ #[error(transparent)]
+ Utf8(#[from] Utf8Error),
+ #[error("invalid column index {0}")]
+ InvalidColumn(c_int),
+ #[error("execute returned results")]
+ ExecReturnedRows,
+ #[error("multiple statements provided")]
+ MultipleStatements,
+}
+
+#[derive(Debug)]
+pub struct SqliteError {
+ code: c_int,
+ msg: Option<String>,
+}
+
+impl Error {
+ pub(crate) fn from_code(code: c_int) -> Self {
+ SqliteError { code, msg: None }.into()
+ }
+
+ pub(crate) fn from_db(db: *mut ffi::sqlite3) -> Self {
+ // SAFETY: sqlite has checks to handle if db is null or dangling, so these shouldn't cause
+ // ub for any input
+ let (code, c_msg) = unsafe { (ffi::sqlite3_errcode(db), ffi::sqlite3_errmsg(db)) };
+
+ let msg = if c_msg.is_null() {
+ None
+ } else {
+ Some(
+ // SAFETY: as long as c_msg is non-null, sqlite shouldn't be giving us bad strings
+ unsafe { CStr::from_ptr(c_msg) }
+ .to_string_lossy()
+ .to_string(),
+ )
+ };
+
+ SqliteError { code, msg }.into()
+ }
+}
+
+fn errstr(code: c_int) -> &'static str {
+ // SAFETY: `sqlite3_errstr` always returns a valid null-terminated static string
+ unsafe { CStr::from_ptr(ffi::sqlite3_errstr(code)) }
+ .to_str()
+ .expect("sqlite errors should be valid utf8")
+}
+
+impl Display for SqliteError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ let msg = self.msg.as_deref().unwrap_or(errstr(self.code));
+ write!(f, "{msg} ({})", self.code)
+ }
+}
+
+impl std::error::Error for SqliteError {}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Debug, thiserror::Error)]
+pub enum GetError<T: FromSql> {
+ #[error(transparent)]
+ Sqlite(#[from] Error),
+ #[error(transparent)]
+ FromSql(T::Error),
+}
+
+pub type GetResult<T> = std::result::Result<T, GetError<T>>;
diff --git a/wyrd_sqlite/src/from_sql.rs b/wyrd_sqlite/src/from_sql.rs
new file mode 100644
index 0000000..3b149e8
--- /dev/null
+++ b/wyrd_sqlite/src/from_sql.rs
@@ -0,0 +1,85 @@
+use std::num::{NonZero, TryFromIntError};
+
+pub enum Value<'a> {
+ Int(i64),
+ Float(f64),
+ Text(&'a str),
+ Blob(&'a [u8]),
+ Null,
+}
+
+// this is an owned trait to get around some problems with blanket implementations
+pub trait FromSql: Sized {
+ type Error: std::error::Error;
+
+ fn try_from_sql(value: Value<'_>) -> Result<Self, Self::Error>;
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("invalid type")]
+pub struct InvalidTypeError;
+
+#[derive(Debug, thiserror::Error)]
+pub enum FromSqlIntError {
+ #[error(transparent)]
+ InvalidType(#[from] InvalidTypeError),
+ #[error(transparent)]
+ TryFromInt(#[from] TryFromIntError),
+}
+
+impl FromSql for i64 {
+ type Error = FromSqlIntError;
+
+ fn try_from_sql(value: Value<'_>) -> Result<Self, Self::Error> {
+ if let Value::Int(i) = value {
+ Ok(i)
+ } else {
+ Err(InvalidTypeError.into())
+ }
+ }
+}
+
+impl FromSql for NonZero<i64> {
+ type Error = FromSqlIntError;
+
+ fn try_from_sql(value: Value) -> Result<Self, Self::Error> {
+ let i: i64 = FromSql::try_from_sql(value)?;
+ Ok(i.try_into()?)
+ }
+}
+
+macro_rules! int_impl {
+ ($($source:ty),+) => {$(
+ impl FromSql for $source {
+ type Error = FromSqlIntError;
+
+ fn try_from_sql(value: Value<'_>) -> Result<Self, Self::Error> {
+ let i: i64 = FromSql::try_from_sql(value)?;
+ Ok(i.try_into()?)
+ }
+ }
+
+ impl FromSql for NonZero<$source> {
+ type Error = FromSqlIntError;
+
+ fn try_from_sql(value: Value<'_>) -> Result<Self, Self::Error> {
+ let i: NonZero<i64> = FromSql::try_from_sql(value)?;
+ Ok(i.try_into()?)
+ }
+ }
+ )*}
+}
+
+int_impl!(i8, i16, i32, u8, u16, u32, u64);
+
+impl<'a, T: FromSql> FromSql for Option<T> {
+ type Error = T::Error;
+
+ fn try_from_sql(value: Value<'_>) -> Result<Self, Self::Error> {
+ if let Value::Null = value {
+ Ok(None)
+ } else {
+ FromSql::try_from_sql(value).map(Some)
+ }
+ }
+}
diff --git a/wyrd_sqlite/src/lib.rs b/wyrd_sqlite/src/lib.rs
new file mode 100644
index 0000000..2ef498a
--- /dev/null
+++ b/wyrd_sqlite/src/lib.rs
@@ -0,0 +1,436 @@
+//! thin wrapper around the bits of sqlite we need. why not just use
+//! [rusqlite](https://docs.rs/rusqlite) or similar? i ran into a minor annoyance with their API
+//! and decided it would be fun to reinvent the wheel a bit and see how i could do. that said, this
+//! implementation owes a great deal to theirs.
+#![deny(clippy::undocumented_unsafe_blocks)]
+#![deny(clippy::missing_safety_doc)]
+#![deny(unsafe_op_in_unsafe_fn)]
+
+use std::{
+ cell::RefCell,
+ ffi::{CString, c_char, c_int, c_uint},
+ marker::PhantomData,
+ mem::ManuallyDrop,
+ ops::{Deref, DerefMut},
+ ptr, slice,
+ str::FromStr,
+};
+
+use libsqlite3_sys as ffi;
+
+mod error;
+mod from_sql;
+mod params;
+
+pub use error::{Error, GetError, GetResult, Result};
+pub use from_sql::{FromSql, Value};
+pub use params::{Param, Params};
+
+fn version_number() -> i32 {
+ // SAFETY: trivial wrapper
+ unsafe { ffi::sqlite3_libversion_number() }
+}
+
+bitflags::bitflags! {
+ #[derive(Clone, Copy, Debug)]
+ #[repr(C)]
+ pub struct OpenFlags: c_int {
+ const READONLY = ffi::SQLITE_OPEN_READONLY;
+ const READWRITE = ffi::SQLITE_OPEN_READWRITE;
+ const CREATE = ffi::SQLITE_OPEN_CREATE;
+ const URI = ffi::SQLITE_OPEN_URI;
+ const MEMORY = ffi::SQLITE_OPEN_MEMORY;
+ const NOMUTEX = ffi::SQLITE_OPEN_NOMUTEX;
+ const NOFOLLOW = ffi::SQLITE_OPEN_NOFOLLOW;
+ const EXRESCODE = ffi::SQLITE_OPEN_EXRESCODE;
+ }
+}
+
+bitflags::bitflags! {
+ #[derive(Clone, Copy, Debug, Default)]
+ #[repr(C)]
+ pub struct PrepFlags: c_uint {
+ const PERSISTENT = ffi::SQLITE_PREPARE_PERSISTENT;
+ const NO_VTAB = ffi::SQLITE_PREPARE_NO_VTAB;
+ const DONT_LOG = ffi::SQLITE_PREPARE_DONT_LOG;
+ }
+}
+
+impl Default for OpenFlags {
+ fn default() -> Self {
+ if version_number() > 3_037_000 {
+ Self::READWRITE | Self::CREATE | Self::NOMUTEX | Self::URI | Self::EXRESCODE
+ } else {
+ Self::READWRITE | Self::CREATE | Self::NOMUTEX | Self::URI
+ }
+ }
+}
+
+pub struct Connection(RefCell<ConnectionInner>);
+
+impl Connection {
+ pub fn open(filename: &str) -> Result<Self> {
+ Self::open_with_flags(filename, OpenFlags::default())
+ }
+
+ pub fn open_with_flags(filename: &str, flags: OpenFlags) -> Result<Self> {
+ ConnectionInner::open(filename, flags)
+ .map(RefCell::new)
+ .map(Self)
+ }
+
+ pub fn prepare(&self, src: &str) -> Result<(Statement<'_>, usize)> {
+ self.prepare_with_flags(src, PrepFlags::default())
+ }
+
+ pub fn prepare_with_flags(
+ &self,
+ src: &str,
+ flags: PrepFlags,
+ ) -> Result<(Statement<'_>, usize)> {
+ self.prepare_raw(src, flags)
+ .map(|(raw, read)| (Statement { raw, conn: self }, read))
+ }
+
+ pub fn prepare_raw(&self, src: &str, flags: PrepFlags) -> Result<(RawStatement, usize)> {
+ self.0.borrow_mut().prepare_raw(src, flags)
+ }
+
+ fn get_error(&self) -> Error {
+ self.0.borrow_mut().get_error()
+ }
+
+ fn decode_response(&self, code: c_int) -> Result<()> {
+ self.0.borrow_mut().decode_response(code)
+ }
+
+ fn changes(&self) -> usize {
+ self.0.borrow_mut().changes()
+ }
+
+ pub fn execute<P: Params>(&self, src: &str, params: P) -> Result<usize> {
+ let (mut stmt, read) = self.prepare(src)?;
+ if read != src.len() {
+ Err(Error::MultipleStatements)
+ } else {
+ stmt.execute(params)
+ }
+ }
+}
+
+struct ConnectionInner {
+ db: *mut ffi::sqlite3,
+}
+
+// SAFETY: ConnectionInner owns the underlying pointer, and all its methods are safe to call from
+// different threads, just not more than one at a time.
+unsafe impl Send for ConnectionInner {}
+
+impl ConnectionInner {
+ // we take &str and not &Path because 1. sqlite specifies that the argument should be
+ // valid utf-8 and 2. there are valid inputs that aren't actually paths
+ fn open(filename: &str, flags: OpenFlags) -> Result<Self> {
+ let c_filename = CString::from_str(filename)?;
+
+ let mut db: *mut ffi::sqlite3 = std::ptr::null_mut();
+ let r =
+ // SAFETY: we're mutating the pointer db, not dereferencing it, so we're good
+ unsafe { ffi::sqlite3_open_v2(c_filename.as_ptr(), &mut db, flags.bits(), std::ptr::null()) };
+
+ if r == ffi::SQLITE_OK {
+ Ok(Self { db })
+ } else if db.is_null() {
+ Err(Error::from_code(r))
+ } else {
+ let e = Error::from_db(db);
+ // SAFETY: db came from sqlite3_open_v2 and it's not null so we're good to close it
+ let r = unsafe { ffi::sqlite3_close(db) };
+ debug_assert_eq!(r, ffi::SQLITE_OK);
+ Err(e)
+ }
+ }
+
+ fn get_error(&mut self) -> Error {
+ Error::from_db(self.db)
+ }
+
+ fn decode_response(&mut self, code: c_int) -> Result<()> {
+ if code == ffi::SQLITE_OK {
+ Ok(())
+ } else {
+ Err(self.get_error())
+ }
+ }
+
+ fn changes(&mut self) -> usize {
+ // SAFETY: this is only ever called immediately after executing a statement, and we're only
+ // using the database connection from a single thread at a time, so the value should always
+ // be good.
+ (unsafe { ffi::sqlite3_changes(self.db) }) as usize
+ }
+
+ fn prepare_raw(&mut self, src: &str, flags: PrepFlags) -> Result<(RawStatement, usize)> {
+ let mut stmt: *mut ffi::sqlite3_stmt = ptr::null_mut();
+ let mut tail: *const c_char = ptr::null();
+
+ // SAFETY: we know self.db hasn't been closed because we only close when we drop, and we
+ // know &mut c_stmt isn't null because it's pointing to a thing we own. this upholds
+ // sqlite's requirements
+ self.decode_response(unsafe {
+ ffi::sqlite3_prepare_v3(
+ self.db,
+ src.as_ptr().cast(),
+ src.len() as c_int,
+ flags.bits(),
+ &mut stmt,
+ &mut tail,
+ )
+ })?;
+
+ if stmt.is_null() {
+ Err(Error::EmptyStatement)
+ } else {
+ // SAFETY: sqlite guarantees that tail will point into src, so we know its address is >=
+ let read = unsafe { tail.offset_from_unsigned(src.as_ptr().cast()) };
+ Ok((RawStatement::new(stmt), read))
+ }
+ }
+}
+
+impl Drop for ConnectionInner {
+ fn drop(&mut self) {
+ // SAFETY: Connection always owns the underlying db, so can close it
+ let r = unsafe { ffi::sqlite3_close(self.db) };
+ debug_assert_eq!(r, ffi::SQLITE_OK);
+ }
+}
+
+pub struct RawStatement {
+ ptr: *mut ffi::sqlite3_stmt,
+}
+
+impl Drop for RawStatement {
+ fn drop(&mut self) {
+ // SAFETY: RawStatement owns its pointer so this should never double free
+ unsafe { ffi::sqlite3_finalize(self.ptr) };
+ }
+}
+
+impl RawStatement {
+ fn new(ptr: *mut ffi::sqlite3_stmt) -> Self {
+ Self { ptr }
+ }
+
+ /// # Safety
+ ///
+ /// the caller must ensure that `conn` is the database connection that was used to prepare the
+ /// statement. this isn't unsafe in a strict sense, since the outcome of failing to uphold this
+ /// invariant is just `MISUSE` errors for sqlite but still, don't do it!
+ pub unsafe fn with_conn<'a>(&'a mut self, conn: &'a Connection) -> BorrowedStatement<'a> {
+ // copying a type w/ a destructor is bad news, but we're immediately wrapping it to make
+ // sure the destructor won't be called twice.
+ let inner = ManuallyDrop::new(Statement {
+ raw: Self { ptr: self.ptr },
+ conn,
+ });
+ BorrowedStatement {
+ marker: PhantomData,
+ inner,
+ }
+ }
+}
+
+pub struct Statement<'a> {
+ raw: RawStatement,
+ conn: &'a Connection,
+}
+
+// lot of little wrapper functions that we know are safe because holding a borrow guarantees
+// we're on the same thread as our database connection, and they don't have any safety requirements
+// beyond that.
+#[allow(clippy::undocumented_unsafe_blocks)]
+impl<'a> Statement<'a> {
+ fn ptr(&self) -> *mut ffi::sqlite3_stmt {
+ self.raw.ptr
+ }
+
+ fn step(&mut self) -> c_int {
+ unsafe { ffi::sqlite3_step(self.ptr()) }
+ }
+
+ fn column_type(&mut self, i: c_int) -> Result<c_int> {
+ let count = unsafe { ffi::sqlite3_column_count(self.ptr()) };
+ if i >= count || i < 0 {
+ Err(Error::InvalidColumn(i))
+ } else {
+ Ok(unsafe { ffi::sqlite3_column_type(self.ptr(), i) })
+ }
+ }
+
+ fn reset_inner(&mut self) -> c_int {
+ unsafe { ffi::sqlite3_reset(self.ptr()) }
+ }
+
+ fn reset(&mut self) -> Result<()> {
+ self.conn.decode_response(self.reset_inner())
+ }
+
+ fn bind_i32(&mut self, i: c_int, n: c_int) -> Result<()> {
+ let r = unsafe { ffi::sqlite3_bind_int(self.ptr(), i, n) };
+ self.conn.decode_response(r)
+ }
+
+ fn bind_i64(&mut self, i: c_int, n: i64) -> Result<()> {
+ let r = unsafe { ffi::sqlite3_bind_int64(self.ptr(), i, n) };
+ self.conn.decode_response(r)
+ }
+
+ fn bind_double(&mut self, i: c_int, f: f64) -> Result<()> {
+ let r = unsafe { ffi::sqlite3_bind_double(self.ptr(), i, f) };
+ self.conn.decode_response(r)
+ }
+
+ fn bind_null(&mut self, i: c_int) -> Result<()> {
+ let r = unsafe { ffi::sqlite3_bind_null(self.ptr(), i) };
+ self.conn.decode_response(r)
+ }
+
+ pub fn query<'q, P: Params>(&'q mut self, params: P) -> Result<Rows<'a, 'q>> {
+ params.__bind_in(self)?;
+ Ok(Rows::new(self))
+ }
+
+ pub fn execute<P: Params>(&mut self, params: P) -> Result<usize> {
+ params.__bind_in(self)?;
+ match self.step() {
+ ffi::SQLITE_DONE => {
+ self.reset()?;
+ Ok(self.conn.changes())
+ }
+ ffi::SQLITE_ROW => {
+ self.reset_inner();
+ Err(Error::ExecReturnedRows)
+ }
+ _ => Err(self.conn.get_error()),
+ }
+ }
+}
+
+pub struct BorrowedStatement<'conn> {
+ marker: PhantomData<&'conn mut RawStatement>,
+ // using the raw pointer contained in here as a secret &'a mut RawStatement so we can make Deref
+ // happy but that means we REALLY DONT WANNA DROP THIS bc we don't *actually* own the
+ // underlying pointer
+ inner: ManuallyDrop<Statement<'conn>>,
+}
+
+impl<'conn> Deref for BorrowedStatement<'conn> {
+ type Target = Statement<'conn>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+impl DerefMut for BorrowedStatement<'_> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+}
+
+pub struct Rows<'conn, 'stmt> {
+ stmt: &'stmt mut Statement<'conn>,
+ // hate that this is necessary. should be able to get away with making stmt optional but get
+ // stuck in borrow checker hell
+ finished: bool,
+}
+
+impl<'a, 'q> Rows<'a, 'q> {
+ fn new(stmt: &'q mut Statement<'a>) -> Self {
+ Self {
+ stmt,
+ finished: false,
+ }
+ }
+
+ pub fn try_next_row<'row>(&'row mut self) -> Result<Option<Row<'a, 'row>>> {
+ if self.finished {
+ return Ok(None);
+ }
+
+ match self.stmt.step() {
+ ffi::SQLITE_ROW => Ok(Some(Row { stmt: self.stmt })),
+ ffi::SQLITE_DONE => {
+ self.finished = true;
+ self.stmt.reset()?;
+ Ok(None)
+ }
+ _ => {
+ self.stmt.reset_inner();
+ Err(self.stmt.conn.get_error())
+ }
+ }
+ }
+}
+
+pub struct Row<'conn, 'row> {
+ stmt: &'row mut Statement<'conn>,
+}
+
+impl<'row> Row<'_, 'row> {
+ // should potentially add a "raw" variant of this that lets the caller just accept the sqlite
+ // dynamic conversions
+ fn get_value(&mut self, i: c_int) -> Result<Value<'row>> {
+ let ptr = self.stmt.ptr();
+
+ Ok(match self.stmt.column_type(i)? {
+ ffi::SQLITE_INTEGER => Value::Int(
+ // SAFETY: we've verified the type, ptr is valid, etc.
+ unsafe { ffi::sqlite3_column_int64(ptr, i) },
+ ),
+ ffi::SQLITE_FLOAT => Value::Float(
+ // SAFETY: same as above
+ unsafe { ffi::sqlite3_column_double(ptr, i) },
+ ),
+ ffi::SQLITE_TEXT => {
+ // SAFETY: same as above
+ let data = unsafe { ffi::sqlite3_column_text(ptr, i) };
+ if data.is_null() {
+ Value::Null
+ } else {
+ // SAFETY: going through the criteria for from_raw_parts we have
+ // 1. we know data is non-null, and sqlite has assured us it's valid for len
+ // bytes
+ // 2. utf-8 strings have no alignment requirements so that's fine
+ // 3. our lifetime restrictions should prevent the statement from being stepped
+ // again before this reference goes out of scope, so it shouldn't be mutated
+ Value::Text(str::from_utf8(unsafe {
+ let len = ffi::sqlite3_column_bytes(ptr, i) as usize;
+ slice::from_raw_parts(data, len)
+ })?)
+ }
+ }
+ ffi::SQLITE_BLOB => {
+ // SAFETY: same as above
+ let data: *const u8 = unsafe { ffi::sqlite3_column_blob(ptr, i) }.cast();
+ if data.is_null() {
+ Value::Null
+ } else {
+ // SAFETY: same as above
+ Value::Blob(unsafe {
+ let len = ffi::sqlite3_column_bytes(ptr, i) as usize;
+ slice::from_raw_parts(data, len)
+ })
+ }
+ }
+ ffi::SQLITE_NULL => Value::Null,
+ _ => unreachable!(),
+ })
+ }
+
+ pub fn get<T: FromSql>(&mut self, col: c_int) -> GetResult<T> {
+ let val = self.get_value(col)?;
+
+ T::try_from_sql(val).map_err(GetError::FromSql)
+ }
+}
diff --git a/wyrd_sqlite/src/params.rs b/wyrd_sqlite/src/params.rs
new file mode 100644
index 0000000..4561c40
--- /dev/null
+++ b/wyrd_sqlite/src/params.rs
@@ -0,0 +1,92 @@
+use std::ffi::{c_double, c_int};
+
+use variadics_please::all_tuples_enumerated;
+
+use crate::{Result, Statement, ffi};
+
+mod sealed {
+ pub trait Sealed {}
+}
+
+use sealed::Sealed;
+
+pub trait Param: Sealed {
+ #[doc(hidden)]
+ fn __bind_in(&self, stmt: &mut Statement<'_>, i: c_int) -> Result<()>;
+}
+
+pub trait Params: Sealed {
+ #[doc(hidden)]
+ fn __bind_in(&self, stmt: &mut Statement<'_>) -> Result<()>;
+}
+
+impl Sealed for c_int {}
+impl Param for c_int {
+ #[inline]
+ fn __bind_in(&self, stmt: &mut Statement<'_>, i: c_int) -> Result<()> {
+ stmt.bind_i32(i, *self)
+ }
+}
+
+impl Sealed for ffi::sqlite3_int64 {}
+impl Param for ffi::sqlite3_int64 {
+ #[inline]
+ fn __bind_in(&self, stmt: &mut Statement<'_>, i: c_int) -> Result<()> {
+ stmt.bind_i64(i, *self)
+ }
+}
+
+impl Sealed for c_double {}
+impl Param for c_double {
+ #[inline]
+ fn __bind_in(&self, stmt: &mut Statement<'_>, i: c_int) -> Result<()> {
+ stmt.bind_double(i, *self)
+ }
+}
+
+impl<T: Param> Sealed for Option<T> {}
+impl<T: Param> Param for Option<T> {
+ fn __bind_in(&self, stmt: &mut Statement<'_>, i: c_int) -> Result<()> {
+ match self.as_ref() {
+ Some(v) => v.__bind_in(stmt, i),
+ // explicitly binding NULL is required in case a previous call bound a value here
+ None => stmt.bind_null(i),
+ }
+ }
+}
+
+impl Sealed for () {}
+impl Param for () {
+ #[inline]
+ fn __bind_in(&self, stmt: &mut Statement<'_>, i: c_int) -> Result<()> {
+ stmt.bind_null(i)
+ }
+}
+
+impl Params for () {
+ fn __bind_in(&self, _stmt: &mut Statement<'_>) -> Result<()> {
+ Ok(())
+ }
+}
+
+macro_rules! impl_params {
+ ($(($i:tt, $T:ident)),*) => {
+ impl<$($T:Param),*> Sealed for ($($T,)*) {}
+ impl<$($T:Param),*> Params for ($($T,)*) {
+ fn __bind_in(&self, stmt: &mut Statement<'_>) -> Result<()> {
+ $(self.$i.__bind_in(stmt, $i + 1)?;)*
+ Ok(())
+ }
+ }
+
+ impl<$($T:Param),*> Sealed for ($(($T, c_int),)*) {}
+ impl<$($T:Param),*> Params for ($(($T, c_int),)*) {
+ fn __bind_in(&self, stmt: &mut Statement<'_>) -> Result<()> {
+ $(self.$i.0.__bind_in(stmt, self.$i.1)?;)*
+ Ok(())
+ }
+ }
+ }
+}
+
+all_tuples_enumerated!(impl_params, 1, 12, T);
|