use std::path::Path;
use super::error::{DbResult, Error};
use super::ffi::{self, RawDb};
use super::statement::{Row, Statement, StepResult};
use super::transaction::Transaction;
use super::value::Value;
pub struct Connection {
db: RawDb,
}
impl Connection {
pub fn open(path: &Path, read_only: bool) -> DbResult<Self> {
let path_str = path.to_string_lossy();
let flags = if read_only {
ffi::SQLITE_OPEN_READONLY | ffi::SQLITE_OPEN_FULLMUTEX
} else {
ffi::SQLITE_OPEN_READWRITE
| ffi::SQLITE_OPEN_CREATE
| ffi::SQLITE_OPEN_FULLMUTEX
};
let db = RawDb::open(&path_str, flags)?;
Ok(Self { db })
}
pub fn execute_batch(&self, sql: &str) -> DbResult<()> {
self.db.exec(sql)
}
pub fn execute_batch_zeroized(&self, sql: &str) -> DbResult<()> {
self.db.exec_zeroized(sql)
}
pub fn prepare(&self, sql: &str) -> DbResult<Statement<'_>> {
let raw_stmt = self.db.prepare(sql)?;
Ok(Statement::new(raw_stmt))
}
pub fn execute(&self, sql: &str, params: &[Value]) -> DbResult<usize> {
let mut stmt = self.prepare(sql)?;
stmt.bind_values(params)?;
stmt.step()?;
Ok(usize::try_from(self.db.changes()).unwrap_or(0))
}
pub fn query_row<T>(
&self,
sql: &str,
params: &[Value],
mapper: impl FnOnce(&Row<'_, '_>) -> DbResult<T>,
) -> DbResult<T> {
let mut stmt = self.prepare(sql)?;
stmt.bind_values(params)?;
match stmt.step()? {
StepResult::Row(row) => mapper(&row),
StepResult::Done => {
Err(Error::new(ffi::SQLITE_DONE, "query returned no rows"))
}
}
}
pub fn query_row_optional<T>(
&self,
sql: &str,
params: &[Value],
mapper: impl FnOnce(&Row<'_, '_>) -> DbResult<T>,
) -> DbResult<Option<T>> {
let mut stmt = self.prepare(sql)?;
stmt.bind_values(params)?;
match stmt.step()? {
StepResult::Row(row) => mapper(&row).map(Some),
StepResult::Done => Ok(None),
}
}
pub fn transaction(&self) -> DbResult<Transaction<'_>> {
Transaction::begin(self, false)
}
pub fn transaction_immediate(&self) -> DbResult<Transaction<'_>> {
Transaction::begin(self, true)
}
#[allow(dead_code)]
#[must_use]
pub fn last_insert_rowid(&self) -> i64 {
self.db.last_insert_rowid()
}
#[allow(dead_code)]
#[must_use]
pub fn changes(&self) -> usize {
usize::try_from(self.db.changes()).unwrap_or(0)
}
}
impl std::fmt::Debug for Connection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Connection").finish_non_exhaustive()
}
}
#[cfg(test)]
impl Connection {
pub fn open_in_memory() -> DbResult<Self> {
Self::open(Path::new(":memory:"), false)
}
}
#[cfg(test)]
mod tests {
use super::Connection;
use crate::params;
use crate::sqlite::Value;
use crate::test_utils::init_sqlite;
#[test]
fn test_open_in_memory() {
init_sqlite();
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);")
.expect("create table");
conn.execute(
"INSERT INTO t (id, val) VALUES (?1, ?2)",
params![1_i64, "hello"],
)
.expect("insert");
let result = conn
.query_row("SELECT val FROM t WHERE id = ?1", params![1_i64], |stmt| {
Ok(stmt.column_text(0))
})
.expect("query");
assert_eq!(result, "hello");
}
#[test]
fn test_query_row_optional_none() {
init_sqlite();
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY);")
.expect("create table");
let result = conn
.query_row_optional("SELECT id FROM t WHERE id = 999", &[], |stmt| {
Ok(stmt.column_i64(0))
})
.expect("query");
assert!(result.is_none());
}
#[test]
fn test_blob_round_trip() {
init_sqlite();
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, data BLOB);")
.expect("create table");
let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
conn.execute(
"INSERT INTO t (id, data) VALUES (?1, ?2)",
params![1_i64, data.as_slice()],
)
.expect("insert");
let result = conn
.query_row("SELECT data FROM t WHERE id = 1", &[], |stmt| {
Ok(stmt.column_blob(0))
})
.expect("query");
assert_eq!(result, data);
}
#[test]
fn test_null_handling() {
init_sqlite();
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);")
.expect("create table");
conn.execute(
"INSERT INTO t (id, val) VALUES (?1, ?2)",
params![1_i64, Value::Null],
)
.expect("insert");
let result = conn
.query_row("SELECT val FROM t WHERE id = 1", &[], |stmt| {
Ok(stmt.is_column_null(0))
})
.expect("query");
assert!(result);
}
}