use std::fmt;
#[derive(Clone, Debug, PartialEq)]
pub enum SqlValue {
Null,
Integer(i64),
Real(f64),
Text(String),
Blob(Vec<u8>),
}
impl From<i64> for SqlValue {
fn from(v: i64) -> Self {
SqlValue::Integer(v)
}
}
impl From<i32> for SqlValue {
fn from(v: i32) -> Self {
SqlValue::Integer(v.into())
}
}
impl From<bool> for SqlValue {
fn from(v: bool) -> Self {
SqlValue::Integer(v.into())
}
}
impl From<&str> for SqlValue {
fn from(v: &str) -> Self {
SqlValue::Text(v.to_string())
}
}
impl From<String> for SqlValue {
fn from(v: String) -> Self {
SqlValue::Text(v)
}
}
impl SqlValue {
pub fn as_i64(&self) -> Option<i64> {
match self {
SqlValue::Integer(i) => Some(*i),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dialect {
Sqlite,
DuckDb,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SqlError {
pub message: String,
pub sqlite_code: Option<i32>,
}
impl fmt::Display for SqlError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for SqlError {}
impl SqlError {
pub fn new(message: impl Into<String>) -> Self {
SqlError {
message: message.into(),
sqlite_code: None,
}
}
}
pub trait SqlRow {
fn get_value(&self, idx: usize) -> Result<SqlValue, SqlError>;
fn column_count(&self) -> usize;
fn get_i64(&self, idx: usize) -> Result<i64, SqlError> {
match self.get_value(idx)? {
SqlValue::Integer(i) => Ok(i),
other => Err(SqlError::new(format!(
"column {idx}: expected an integer, got {other:?}"
))),
}
}
fn get_bool(&self, idx: usize) -> Result<bool, SqlError> {
Ok(self.get_i64(idx)? != 0)
}
fn get_text(&self, idx: usize) -> Result<String, SqlError> {
match self.get_value(idx)? {
SqlValue::Text(s) => Ok(s),
other => Err(SqlError::new(format!(
"column {idx}: expected text, got {other:?}"
))),
}
}
fn get_opt_i64(&self, idx: usize) -> Result<Option<i64>, SqlError> {
match self.get_value(idx)? {
SqlValue::Null => Ok(None),
SqlValue::Integer(i) => Ok(Some(i)),
other => Err(SqlError::new(format!(
"column {idx}: expected an integer or NULL, got {other:?}"
))),
}
}
}
impl SqlRow for Vec<SqlValue> {
fn get_value(&self, idx: usize) -> Result<SqlValue, SqlError> {
self.get(idx)
.cloned()
.ok_or_else(|| SqlError::new(format!("column {idx} out of range")))
}
fn column_count(&self) -> usize {
self.len()
}
}
pub trait SqlConn {
fn dialect(&self) -> Dialect;
fn execute_batch(&self, sql: &str) -> Result<(), SqlError>;
fn execute(&self, sql: &str, params: &[SqlValue]) -> Result<usize, SqlError>;
fn query(
&self,
sql: &str,
params: &[SqlValue],
f: &mut dyn FnMut(&dyn SqlRow) -> Result<(), SqlError>,
) -> Result<(), SqlError>;
fn query_named(
&self,
sql: &str,
params: &[(&str, SqlValue)],
f: &mut dyn FnMut(&dyn SqlRow) -> Result<(), SqlError>,
) -> Result<(), SqlError>;
fn max_variables(&self) -> usize;
fn column_count(&self, sql: &str) -> Result<usize, SqlError>;
fn use_schema(&self, schema: &str) -> Result<(), SqlError> {
self.execute_batch(&format!("SET search_path = '{schema}'"))
}
fn query_cached(
&self,
sql: &str,
params: &[SqlValue],
f: &mut dyn FnMut(&dyn SqlRow) -> Result<(), SqlError>,
) -> Result<(), SqlError> {
self.query(sql, params, f)
}
fn execute_cached(&self, sql: &str, params: &[SqlValue]) -> Result<usize, SqlError> {
self.execute(sql, params)
}
}
pub trait SqlConnExt: SqlConn {
fn query_rows<T>(
&self,
sql: &str,
params: &[SqlValue],
mut f: impl FnMut(&dyn SqlRow) -> Result<T, SqlError>,
) -> Result<Vec<T>, SqlError> {
let mut out = Vec::new();
self.query(sql, params, &mut |row| {
out.push(f(row)?);
Ok(())
})?;
Ok(out)
}
fn query_opt<T>(
&self,
sql: &str,
params: &[SqlValue],
mut f: impl FnMut(&dyn SqlRow) -> Result<T, SqlError>,
) -> Result<Option<T>, SqlError> {
let mut out = None;
self.query(sql, params, &mut |row| {
if out.is_none() {
out = Some(f(row)?);
}
Ok(())
})?;
Ok(out)
}
}
impl<C: SqlConn + ?Sized> SqlConnExt for C {}