use crate::db::DbError;
use crate::sqlite_vfs::ffi;
use std::ffi::c_int;
use std::marker::PhantomData;
use std::slice;
pub struct Row<'row> {
raw: *mut ffi::sqlite3_stmt,
_row: PhantomData<&'row ()>,
}
pub trait FromColumn: Sized {
const EXPECTED: &'static str;
fn read(row: &Row<'_>, index: usize) -> Result<Self, DbError>;
}
impl Row<'_> {
pub(crate) fn new(raw: *mut ffi::sqlite3_stmt) -> Self {
Self {
raw,
_row: PhantomData,
}
}
pub fn get<T: FromColumn>(&self, index: usize) -> Result<T, DbError> {
self.check_index(index)?;
T::read(self, index)
}
fn check_index(&self, index: usize) -> Result<(), DbError> {
let count = unsafe { ffi::sqlite3_column_count(self.raw) };
let count = usize::try_from(count).unwrap_or(0);
if index < count {
Ok(())
} else {
Err(DbError::ColumnOutOfRange { index, count })
}
}
fn column_type(&self, index: usize) -> Result<c_int, DbError> {
let index = c_int::try_from(index).map_err(|_| DbError::ColumnOutOfRange {
index,
count: usize::try_from(unsafe { ffi::sqlite3_column_count(self.raw) }).unwrap_or(0),
})?;
Ok(unsafe { ffi::sqlite3_column_type(self.raw, index) })
}
fn require_type(
&self,
index: usize,
expected: &'static str,
actual: c_int,
) -> Result<(), DbError> {
let expected_code = match expected {
"TEXT" => ffi::SQLITE_TEXT,
"INTEGER" => ffi::SQLITE_INTEGER,
"REAL" => ffi::SQLITE_FLOAT,
"BLOB" => ffi::SQLITE_BLOB,
"NULL" => ffi::SQLITE_NULL,
_ => actual,
};
if actual == expected_code {
Ok(())
} else {
Err(DbError::TypeMismatch {
index,
expected,
actual: type_name(actual),
})
}
}
}
impl FromColumn for String {
const EXPECTED: &'static str = "TEXT";
fn read(row: &Row<'_>, index: usize) -> Result<Self, DbError> {
let actual = row.column_type(index)?;
row.require_type(index, Self::EXPECTED, actual)?;
let index = c_int::try_from(index).map_err(|_| DbError::TooManyParameters)?;
let text = unsafe { ffi::sqlite3_column_text(row.raw, index) };
let len = unsafe { ffi::sqlite3_column_bytes(row.raw, index) };
let len = usize::try_from(len).map_err(|_| DbError::TextTooLarge)?;
if len == 0 {
return Ok(String::new());
}
if text.is_null() {
return Ok(String::new());
}
let bytes = unsafe { slice::from_raw_parts(text.cast::<u8>(), len) };
Ok(String::from_utf8_lossy(bytes).into_owned())
}
}
impl FromColumn for i64 {
const EXPECTED: &'static str = "INTEGER";
fn read(row: &Row<'_>, index: usize) -> Result<Self, DbError> {
let actual = row.column_type(index)?;
row.require_type(index, Self::EXPECTED, actual)?;
let index = c_int::try_from(index).map_err(|_| DbError::TooManyParameters)?;
Ok(unsafe { ffi::sqlite3_column_int64(row.raw, index) })
}
}
impl FromColumn for f64 {
const EXPECTED: &'static str = "REAL";
fn read(row: &Row<'_>, index: usize) -> Result<Self, DbError> {
let actual = row.column_type(index)?;
row.require_type(index, Self::EXPECTED, actual)?;
let index = c_int::try_from(index).map_err(|_| DbError::TooManyParameters)?;
Ok(unsafe { ffi::sqlite3_column_double(row.raw, index) })
}
}
impl FromColumn for Vec<u8> {
const EXPECTED: &'static str = "BLOB";
fn read(row: &Row<'_>, index: usize) -> Result<Self, DbError> {
let actual = row.column_type(index)?;
row.require_type(index, Self::EXPECTED, actual)?;
let index = c_int::try_from(index).map_err(|_| DbError::TooManyParameters)?;
let ptr = unsafe { ffi::sqlite3_column_blob(row.raw, index) };
let len = unsafe { ffi::sqlite3_column_bytes(row.raw, index) };
let len = usize::try_from(len).map_err(|_| DbError::BlobTooLarge)?;
if len == 0 {
return Ok(Vec::new());
}
if ptr.is_null() {
return Ok(Vec::new());
}
let bytes = unsafe { slice::from_raw_parts(ptr.cast::<u8>(), len) };
Ok(bytes.to_vec())
}
}
impl<T: FromColumn> FromColumn for Option<T> {
const EXPECTED: &'static str = T::EXPECTED;
fn read(row: &Row<'_>, index: usize) -> Result<Self, DbError> {
if row.column_type(index)? == ffi::SQLITE_NULL {
Ok(None)
} else {
T::read(row, index).map(Some)
}
}
}
fn type_name(code: c_int) -> &'static str {
match code {
ffi::SQLITE_INTEGER => "INTEGER",
ffi::SQLITE_FLOAT => "REAL",
ffi::SQLITE_TEXT => "TEXT",
ffi::SQLITE_BLOB => "BLOB",
ffi::SQLITE_NULL => "NULL",
_ => "UNKNOWN",
}
}