use std::path::Path;
use rusqlite::types::ValueRef;
use rusqlite::{Connection, OpenFlags};
use serde_json::{Map, Value};
use crate::Result;
fn other(message: String) -> crate::Error {
crate::Error::Other(message)
}
fn open_read_only(path: &Path) -> Result<Connection> {
Connection::open_with_flags(
path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|e| other(format!("{}: {e}", path.display())))
}
fn cell(value: ValueRef<'_>) -> Value {
match value {
ValueRef::Null => Value::Null,
ValueRef::Integer(i) => Value::from(i),
ValueRef::Real(f) => serde_json::Number::from_f64(f)
.map(Value::Number)
.unwrap_or(Value::Null),
ValueRef::Text(t) => Value::String(String::from_utf8_lossy(t).into_owned()),
ValueRef::Blob(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
}
}
pub fn table_exists(path: &Path, table: &str) -> bool {
if !path.is_file() {
return false;
}
let Ok(conn) = open_read_only(path) else {
return false;
};
conn.query_row(
"select name from sqlite_master where type='table' and name=?1",
[table],
|_| Ok(()),
)
.is_ok()
}
pub fn read_rows(
path: &Path,
sql: &str,
params: &[&dyn rusqlite::ToSql],
) -> Result<Option<Vec<Map<String, Value>>>> {
if !path.is_file() {
return Ok(None);
}
let conn = open_read_only(path)?;
let mut statement = conn
.prepare(sql)
.map_err(|e| other(format!("{}: {e}", path.display())))?;
let names: Vec<String> = statement
.column_names()
.iter()
.map(|s| s.to_string())
.collect();
let rows = statement
.query_map(params, |row| {
let mut object = Map::new();
for (index, name) in names.iter().enumerate() {
object.insert(name.clone(), cell(row.get_ref(index)?));
}
Ok(object)
})
.map_err(|e| other(format!("{}: {e}", path.display())))?
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(|e| other(format!("{}: {e}", path.display())))?;
Ok(Some(rows))
}
#[derive(Debug, Clone)]
pub enum Param {
Null,
Int(i64),
Real(f64),
Text(String),
}
impl From<&Value> for Param {
fn from(value: &Value) -> Self {
match value {
Value::Null => Self::Null,
Value::Bool(b) => Self::Int(i64::from(*b)),
Value::Number(n) => n
.as_i64()
.map(Self::Int)
.or_else(|| n.as_f64().map(Self::Real))
.unwrap_or(Self::Null),
Value::String(s) => Self::Text(s.clone()),
other => Self::Text(other.to_string()),
}
}
}
impl rusqlite::ToSql for Param {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
use rusqlite::types::{ToSqlOutput, Value as SqlValue};
Ok(ToSqlOutput::Owned(match self {
Self::Null => SqlValue::Null,
Self::Int(i) => SqlValue::Integer(*i),
Self::Real(f) => SqlValue::Real(*f),
Self::Text(t) => SqlValue::Text(t.clone()),
}))
}
}
pub fn write_table(path: &Path, ddl: &str, insert: &str, rows: &[Vec<Param>]) -> Result<()> {
let conn = Connection::open(path).map_err(|e| other(format!("{}: {e}", path.display())))?;
if !ddl.trim().is_empty() {
conn.execute_batch(ddl)
.map_err(|e| other(format!("{}: {e}", path.display())))?;
}
conn.execute_batch("begin")
.map_err(|e| other(e.to_string()))?;
{
let mut statement = conn
.prepare(insert)
.map_err(|e| other(format!("{}: {e}", path.display())))?;
for row in rows {
let params: Vec<&dyn rusqlite::ToSql> =
row.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
statement
.execute(params.as_slice())
.map_err(|e| other(format!("{}: {e}", path.display())))?;
}
}
conn.execute_batch("commit")
.map_err(|e| other(e.to_string()))?;
Ok(())
}