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(
129                        s,
130                        &time::format_description::well_known::Rfc3339,
131                    ) {
132                        return ColumnValue::Date((dt.unix_timestamp_nanos() / 1_000_000) as i64);
133                    }
134                }
135                // Check if it might be a BigInt (large number as string)
136                if s.len() > 18
137                    && s.chars()
138                        .all(|c| c.is_ascii_digit() || c == '-' || c == '+')
139                {
140                    return ColumnValue::BigInt(s.clone());
141                }
142                ColumnValue::Text(s.clone())
143            }
144            rusqlite::types::Value::Blob(b) => ColumnValue::Blob(b.clone()),
145        }
146    }
147
148    #[cfg(not(target_arch = "wasm32"))]
149    pub fn to_rusqlite_value(&self) -> rusqlite::types::Value {
150        match self {
151            ColumnValue::Null => rusqlite::types::Value::Null,
152            ColumnValue::Integer(i) => rusqlite::types::Value::Integer(*i),
153            ColumnValue::Real(f) => rusqlite::types::Value::Real(*f),
154            ColumnValue::Text(s) => rusqlite::types::Value::Text(s.clone()),
155            ColumnValue::Blob(b) => rusqlite::types::Value::Blob(b.clone()),
156            ColumnValue::Date(ts) => {
157                // Convert timestamp to ISO string
158                let dt = time::OffsetDateTime::from_unix_timestamp_nanos((*ts as i128) * 1_000_000)
159                    .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
160                let formatted = dt
161                    .format(&time::format_description::well_known::Rfc3339)
162                    .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string());
163                rusqlite::types::Value::Text(formatted)
164            }
165            ColumnValue::BigInt(s) => rusqlite::types::Value::Text(s.clone()),
166        }
167    }
168}
169
170// Transaction options
171#[derive(Tsify, Serialize, Deserialize, Debug)]
172#[tsify(into_wasm_abi, from_wasm_abi)]
173pub struct TransactionOptions {
174    pub isolation_level: IsolationLevel,
175    pub timeout_ms: Option<u32>,
176}
177
178#[derive(Tsify, Serialize, Deserialize, Debug)]
179#[tsify(into_wasm_abi, from_wasm_abi)]
180pub enum IsolationLevel {
181    ReadUncommitted,
182    ReadCommitted,
183    RepeatableRead,
184    Serializable,
185}
186
187// Error types
188#[derive(Tsify, Serialize, Deserialize, Debug, Clone, thiserror::Error)]
189#[tsify(into_wasm_abi, from_wasm_abi)]
190#[error("Database error: {message}")]
191pub struct DatabaseError {
192    pub code: String,
193    pub message: String,
194    pub sql: Option<String>,
195}
196
197impl DatabaseError {
198    pub fn new(code: &str, message: &str) -> Self {
199        Self {
200            code: code.to_string(),
201            message: message.to_string(),
202            sql: None,
203        }
204    }
205
206    pub fn with_sql(mut self, sql: &str) -> Self {
207        self.sql = Some(sql.to_string());
208        self
209    }
210}
211
212#[cfg(not(target_arch = "wasm32"))]
213impl From<rusqlite::Error> for DatabaseError {
214    fn from(err: rusqlite::Error) -> Self {
215        DatabaseError::new("SQLITE_ERROR", &err.to_string())
216    }
217}
218
219impl From<JsValue> for DatabaseError {
220    fn from(err: JsValue) -> Self {
221        let message = err
222            .as_string()
223            .unwrap_or_else(|| "Unknown JavaScript error".to_string());
224        DatabaseError::new("JS_ERROR", &message)
225    }
226}