absurder_sql/
types.rs

1use serde::{Deserialize, Serialize};
2use tsify::Tsify;
3use wasm_bindgen::prelude::*;
4
5// Database configuration - automatically generates TypeScript interface
6#[derive(Tsify, Serialize, Deserialize, Debug, Clone)]
7#[tsify(into_wasm_abi, from_wasm_abi)]
8pub struct DatabaseConfig {
9    pub name: String,
10    pub version: Option<u32>,
11    pub cache_size: Option<usize>,
12    pub page_size: Option<usize>,
13    pub auto_vacuum: Option<bool>,
14    /// Journal mode for SQLite transactions
15    ///
16    /// Options:
17    /// - "MEMORY" (default): Fast in-memory journaling, optimal browser performance
18    /// - "WAL": Write-Ahead Logging with full shared memory support
19    ///          Set `journal_mode: Some("WAL".to_string())` to enable
20    ///          Note: WAL has overhead in concurrent operations but provides
21    ///          better crash recovery and allows concurrent reads
22    /// - "DELETE": Traditional rollback journal
23    ///
24    /// Example enabling WAL:
25    /// ```
26    /// use absurder_sql::DatabaseConfig;
27    /// let config = DatabaseConfig {
28    ///     name: "mydb".to_string(),
29    ///     journal_mode: Some("WAL".to_string()),
30    ///     ..Default::default()
31    /// };
32    /// ```
33    pub journal_mode: Option<String>,
34    /// Maximum database size for export operations (in bytes).
35    /// Default: 2GB (2_147_483_648 bytes)
36    /// Rationale: Balances IndexedDB capacity (10GB+) with browser memory limits (~2-4GB/tab)
37    /// Set to None for no limit (not recommended - may cause OOM errors)
38    pub max_export_size_bytes: Option<u64>,
39}
40
41impl Default for DatabaseConfig {
42    fn default() -> Self {
43        Self {
44            name: "default.db".to_string(),
45            version: Some(1),
46            cache_size: Some(10_000),
47            page_size: Some(4096),
48            auto_vacuum: Some(true),
49            // MEMORY mode: optimal browser performance (absurd-sql approach)
50            // WAL mode is fully supported - explicitly set journal_mode to enable
51            journal_mode: Some("MEMORY".to_string()),
52            max_export_size_bytes: Some(2 * 1024 * 1024 * 1024), // 2GB default
53        }
54    }
55}
56
57impl DatabaseConfig {
58    /// Create mobile-optimized database configuration
59    /// 
60    /// Optimizations:
61    /// - WAL mode: Better concurrency, crash recovery, and write performance
62    /// - Larger cache: 20K pages (~80MB with 4KB pages) for better read performance
63    /// - 4KB pages: Optimal for mobile storage
64    /// - Auto vacuum: Keeps database size manageable
65    /// 
66    /// Use this for React Native, Flutter, or other mobile applications.
67    /// 
68    /// # Examples
69    /// ```
70    /// use absurder_sql::types::DatabaseConfig;
71    /// 
72    /// let config = DatabaseConfig::mobile_optimized("myapp.db");
73    /// assert_eq!(config.journal_mode, Some("WAL".to_string()));
74    /// ```
75    pub fn mobile_optimized(name: impl Into<String>) -> Self {
76        Self {
77            name: name.into(),
78            version: Some(1),
79            cache_size: Some(20_000), // ~80MB cache with 4KB pages
80            page_size: Some(4096),
81            auto_vacuum: Some(true),
82            journal_mode: Some("WAL".to_string()), // WAL for mobile performance
83            max_export_size_bytes: Some(2 * 1024 * 1024 * 1024),
84        }
85    }
86}
87// Query result types with proper TypeScript mapping
88#[derive(Tsify, Serialize, Deserialize, Debug)]
89#[tsify(into_wasm_abi, from_wasm_abi)]
90#[serde(rename_all = "camelCase")]
91pub struct QueryResult {
92    pub columns: Vec<String>,
93    pub rows: Vec<Row>,
94    pub affected_rows: u32,
95    pub last_insert_id: Option<i64>,
96    pub execution_time_ms: f64,
97}
98
99#[derive(Tsify, Serialize, Deserialize, Debug, Clone)]
100#[tsify(into_wasm_abi, from_wasm_abi)]
101pub struct Row {
102    pub values: Vec<ColumnValue>,
103}
104
105#[derive(Tsify, Serialize, Deserialize, Debug, Clone, PartialEq)]
106#[tsify(into_wasm_abi, from_wasm_abi)]
107#[serde(tag = "type", content = "value")]
108pub enum ColumnValue {
109    Null,
110    Integer(i64),
111    Real(f64),
112    Text(String),
113    Blob(Vec<u8>),
114    Date(i64), // Store as UTC timestamp (milliseconds since epoch)
115    BigInt(String), // Store as string to handle large integers beyond i64
116}
117
118impl ColumnValue {
119    #[cfg(not(target_arch = "wasm32"))]
120    pub fn from_rusqlite_value(value: &rusqlite::types::Value) -> Self {
121        match value {
122            rusqlite::types::Value::Null => ColumnValue::Null,
123            rusqlite::types::Value::Integer(i) => ColumnValue::Integer(*i),
124            rusqlite::types::Value::Real(f) => ColumnValue::Real(*f),
125            rusqlite::types::Value::Text(s) => {
126                // Check if the text might be a date in ISO format
127                if s.len() >= 20 && s.starts_with("20") && s.contains('T') && s.contains('Z') {
128                    if let Ok(dt) = time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339) {
129                        return ColumnValue::Date((dt.unix_timestamp_nanos() / 1_000_000) as i64);
130                    }
131                }
132                // Check if it might be a BigInt (large number as string)
133                if s.len() > 18 && s.chars().all(|c| c.is_digit(10) || c == '-' || c == '+') {
134                    return ColumnValue::BigInt(s.clone());
135                }
136                ColumnValue::Text(s.clone())
137            },
138            rusqlite::types::Value::Blob(b) => ColumnValue::Blob(b.clone()),
139        }
140    }
141
142    #[cfg(not(target_arch = "wasm32"))]
143    pub fn to_rusqlite_value(&self) -> rusqlite::types::Value {
144        match self {
145            ColumnValue::Null => rusqlite::types::Value::Null,
146            ColumnValue::Integer(i) => rusqlite::types::Value::Integer(*i),
147            ColumnValue::Real(f) => rusqlite::types::Value::Real(*f),
148            ColumnValue::Text(s) => rusqlite::types::Value::Text(s.clone()),
149            ColumnValue::Blob(b) => rusqlite::types::Value::Blob(b.clone()),
150            ColumnValue::Date(ts) => {
151                // Convert timestamp to ISO string
152                let dt = time::OffsetDateTime::from_unix_timestamp_nanos((*ts as i128) * 1_000_000)
153                    .unwrap_or_else(|_| time::OffsetDateTime::UNIX_EPOCH);
154                let formatted = dt.format(&time::format_description::well_known::Rfc3339)
155                    .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string());
156                rusqlite::types::Value::Text(formatted)
157            },
158            ColumnValue::BigInt(s) => rusqlite::types::Value::Text(s.clone()),
159        }
160    }
161}
162
163// Transaction options
164#[derive(Tsify, Serialize, Deserialize, Debug)]
165#[tsify(into_wasm_abi, from_wasm_abi)]
166pub struct TransactionOptions {
167    pub isolation_level: IsolationLevel,
168    pub timeout_ms: Option<u32>,
169}
170
171#[derive(Tsify, Serialize, Deserialize, Debug)]
172#[tsify(into_wasm_abi, from_wasm_abi)]
173pub enum IsolationLevel {
174    ReadUncommitted,
175    ReadCommitted,
176    RepeatableRead,
177    Serializable,
178}
179
180// Error types
181#[derive(Tsify, Serialize, Deserialize, Debug, Clone, thiserror::Error)]
182#[tsify(into_wasm_abi, from_wasm_abi)]
183#[error("Database error: {message}")]
184pub struct DatabaseError {
185    pub code: String,
186    pub message: String,
187    pub sql: Option<String>,
188}
189
190impl DatabaseError {
191    pub fn new(code: &str, message: &str) -> Self {
192        Self {
193            code: code.to_string(),
194            message: message.to_string(),
195            sql: None,
196        }
197    }
198
199    pub fn with_sql(mut self, sql: &str) -> Self {
200        self.sql = Some(sql.to_string());
201        self
202    }
203}
204
205#[cfg(not(target_arch = "wasm32"))]
206impl From<rusqlite::Error> for DatabaseError {
207    fn from(err: rusqlite::Error) -> Self {
208        DatabaseError::new("SQLITE_ERROR", &err.to_string())
209    }
210}
211
212impl From<JsValue> for DatabaseError {
213    fn from(err: JsValue) -> Self {
214        let message = err.as_string().unwrap_or_else(|| "Unknown JavaScript error".to_string());
215        DatabaseError::new("JS_ERROR", &message)
216    }
217}