use serde::{Deserialize, Serialize};
use tsify::Tsify;
use wasm_bindgen::prelude::*;
#[derive(Tsify, Serialize, Deserialize, Debug, Clone)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct DatabaseConfig {
pub name: String,
pub version: Option<u32>,
pub cache_size: Option<usize>,
pub page_size: Option<usize>,
pub auto_vacuum: Option<bool>,
pub journal_mode: Option<String>,
pub max_export_size_bytes: Option<u64>,
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
name: "default.db".to_string(),
version: Some(1),
cache_size: Some(10_000),
page_size: Some(4096),
auto_vacuum: Some(true),
journal_mode: Some("MEMORY".to_string()),
max_export_size_bytes: Some(2 * 1024 * 1024 * 1024), }
}
}
impl DatabaseConfig {
pub fn mobile_optimized(name: impl Into<String>) -> Self {
Self {
name: name.into(),
version: Some(1),
cache_size: Some(20_000), page_size: Some(4096),
auto_vacuum: Some(true),
journal_mode: Some("WAL".to_string()), max_export_size_bytes: Some(2 * 1024 * 1024 * 1024),
}
}
}
#[derive(Tsify, Serialize, Deserialize, Debug)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<Row>,
pub affected_rows: u32,
pub last_insert_id: Option<i64>,
pub execution_time_ms: f64,
}
#[derive(Tsify, Serialize, Deserialize, Debug, Clone)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Row {
pub values: Vec<ColumnValue>,
}
#[derive(Tsify, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(tag = "type", content = "value")]
pub enum ColumnValue {
Null,
Integer(i64),
Real(f64),
Text(String),
Blob(Vec<u8>),
Date(i64), BigInt(String), }
impl ColumnValue {
#[cfg(not(target_arch = "wasm32"))]
pub fn from_rusqlite_value(value: &rusqlite::types::Value) -> Self {
match value {
rusqlite::types::Value::Null => ColumnValue::Null,
rusqlite::types::Value::Integer(i) => ColumnValue::Integer(*i),
rusqlite::types::Value::Real(f) => ColumnValue::Real(*f),
rusqlite::types::Value::Text(s) => {
if s.len() >= 20 && s.starts_with("20") && s.contains('T') && s.contains('Z') {
if let Ok(dt) = time::OffsetDateTime::parse(
s,
&time::format_description::well_known::Rfc3339,
) {
return ColumnValue::Date((dt.unix_timestamp_nanos() / 1_000_000) as i64);
}
}
if s.len() > 18
&& s.chars()
.all(|c| c.is_ascii_digit() || c == '-' || c == '+')
{
return ColumnValue::BigInt(s.clone());
}
ColumnValue::Text(s.clone())
}
rusqlite::types::Value::Blob(b) => ColumnValue::Blob(b.clone()),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn to_rusqlite_value(&self) -> rusqlite::types::Value {
match self {
ColumnValue::Null => rusqlite::types::Value::Null,
ColumnValue::Integer(i) => rusqlite::types::Value::Integer(*i),
ColumnValue::Real(f) => rusqlite::types::Value::Real(*f),
ColumnValue::Text(s) => rusqlite::types::Value::Text(s.clone()),
ColumnValue::Blob(b) => rusqlite::types::Value::Blob(b.clone()),
ColumnValue::Date(ts) => {
let dt = time::OffsetDateTime::from_unix_timestamp_nanos((*ts as i128) * 1_000_000)
.unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
let formatted = dt
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string());
rusqlite::types::Value::Text(formatted)
}
ColumnValue::BigInt(s) => rusqlite::types::Value::Text(s.clone()),
}
}
}
#[derive(Tsify, Serialize, Deserialize, Debug)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct TransactionOptions {
pub isolation_level: IsolationLevel,
pub timeout_ms: Option<u32>,
}
#[derive(Tsify, Serialize, Deserialize, Debug)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub enum IsolationLevel {
ReadUncommitted,
ReadCommitted,
RepeatableRead,
Serializable,
}
#[derive(Tsify, Serialize, Deserialize, Debug, Clone, thiserror::Error)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[error("Database error: {message}")]
pub struct DatabaseError {
pub code: String,
pub message: String,
pub sql: Option<String>,
}
impl DatabaseError {
pub fn new(code: &str, message: &str) -> Self {
Self {
code: code.to_string(),
message: message.to_string(),
sql: None,
}
}
pub fn with_sql(mut self, sql: &str) -> Self {
self.sql = Some(sql.to_string());
self
}
}
#[cfg(not(target_arch = "wasm32"))]
impl From<rusqlite::Error> for DatabaseError {
fn from(err: rusqlite::Error) -> Self {
DatabaseError::new("SQLITE_ERROR", &err.to_string())
}
}
impl From<JsValue> for DatabaseError {
fn from(err: JsValue) -> Self {
let message = err
.as_string()
.unwrap_or_else(|| "Unknown JavaScript error".to_string());
DatabaseError::new("JS_ERROR", &message)
}
}