Skip to main content

cdk_sqlite/
common.rs

1use std::fmt;
2use std::path::PathBuf;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5use std::time::Duration;
6
7use cdk_sql_common::pool::{self, DatabasePool};
8use cdk_sql_common::value::Value;
9use rusqlite::{ffi, Connection};
10
11use crate::async_sqlite;
12
13/// The config need to create a new SQLite connection
14pub struct Config {
15    path: Option<String>,
16    password: Option<String>,
17    /// Set once the in-memory database has been opened. It lives only inside its
18    /// single connection, so a second open means that connection was discarded.
19    in_memory_opened: AtomicBool,
20}
21
22impl Clone for Config {
23    fn clone(&self) -> Self {
24        Self {
25            path: self.path.clone(),
26            password: self.password.clone(),
27            // A cloned config describes a new pool with its own database.
28            in_memory_opened: AtomicBool::new(false),
29        }
30    }
31}
32
33impl fmt::Debug for Config {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.debug_struct("Config")
36            .field("path", &self.path)
37            .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
38            .finish()
39    }
40}
41
42impl pool::DatabaseConfig for Config {
43    fn default_timeout(&self) -> Duration {
44        Duration::from_secs(5)
45    }
46
47    fn max_size(&self) -> usize {
48        if self.path.is_none() {
49            1
50        } else {
51            20
52        }
53    }
54}
55
56/// Sqlite connection manager
57#[derive(Debug)]
58pub struct SqliteConnectionManager;
59
60impl DatabasePool for SqliteConnectionManager {
61    type Config = Config;
62
63    type Connection = async_sqlite::AsyncSqlite;
64
65    type Error = rusqlite::Error;
66
67    fn new_resource(
68        config: &Self::Config,
69        _stale: Arc<AtomicBool>,
70        _timeout: Duration,
71    ) -> Result<Self::Connection, pool::Error<Self::Error>> {
72        let in_memory = config.path.is_none();
73        if in_memory && config.in_memory_opened.load(Ordering::SeqCst) {
74            // Handing out a fresh, empty database would silently drop every
75            // previous write, so make all later operations fail instead.
76            tracing::error!("In-memory SQLite connection was discarded; its database is lost");
77            return Err(pool::Error::Resource(rusqlite::Error::SqliteFailure(
78                ffi::Error::new(ffi::SQLITE_CANTOPEN),
79                Some("in-memory database was lost after its connection was discarded".to_owned()),
80            )));
81        }
82
83        let conn = if let Some(path) = config.path.as_ref() {
84            // Check if parent directory exists before attempting to open database
85            let path_buf = PathBuf::from(path);
86            if let Some(parent) = path_buf.parent() {
87                if !parent.to_str().unwrap_or_default().is_empty() && !parent.exists() {
88                    return Err(pool::Error::Resource(rusqlite::Error::InvalidPath(
89                        path_buf.clone(),
90                    )));
91                }
92            }
93            Connection::open(path)?
94        } else {
95            Connection::open_in_memory()?
96        };
97
98        if let Some(password) = config.password.as_ref() {
99            conn.pragma_update(None, "key", password)?;
100        }
101
102        conn.execute_batch(
103            r#"
104            pragma busy_timeout = 10000;
105            pragma journal_mode = WAL;
106            pragma synchronous = FULL;
107            pragma temp_store = memory;
108            pragma mmap_size = 5242880;
109            pragma cache = shared;
110            "#,
111        )?;
112
113        conn.busy_timeout(Duration::from_secs(10))?;
114
115        if in_memory {
116            config.in_memory_opened.store(true, Ordering::SeqCst);
117        }
118
119        Ok(async_sqlite::AsyncSqlite::new(conn))
120    }
121}
122
123impl From<PathBuf> for Config {
124    fn from(path: PathBuf) -> Self {
125        path.to_str().unwrap_or_default().into()
126    }
127}
128
129impl From<(PathBuf, String)> for Config {
130    fn from((path, password): (PathBuf, String)) -> Self {
131        (path.to_str().unwrap_or_default(), password.as_str()).into()
132    }
133}
134
135impl From<&PathBuf> for Config {
136    fn from(path: &PathBuf) -> Self {
137        path.to_str().unwrap_or_default().into()
138    }
139}
140
141impl From<&str> for Config {
142    fn from(path: &str) -> Self {
143        if path.contains(":memory:") {
144            Config {
145                path: None,
146                password: None,
147                in_memory_opened: Default::default(),
148            }
149        } else {
150            Config {
151                path: Some(path.to_owned()),
152                password: None,
153                in_memory_opened: Default::default(),
154            }
155        }
156    }
157}
158
159impl From<(&str, &str)> for Config {
160    fn from((path, pass): (&str, &str)) -> Self {
161        if path.contains(":memory:") {
162            Config {
163                path: None,
164                password: Some(pass.to_owned()),
165                in_memory_opened: Default::default(),
166            }
167        } else {
168            Config {
169                path: Some(path.to_owned()),
170                password: Some(pass.to_owned()),
171                in_memory_opened: Default::default(),
172            }
173        }
174    }
175}
176
177/// Convert cdk_sql_common::value::Value to rusqlite Value
178#[inline(always)]
179pub fn to_sqlite(v: Value) -> rusqlite::types::Value {
180    match v {
181        Value::Blob(blob) => rusqlite::types::Value::Blob(blob),
182        Value::Integer(i) => rusqlite::types::Value::Integer(i),
183        Value::Null => rusqlite::types::Value::Null,
184        Value::Text(t) => rusqlite::types::Value::Text(t),
185        Value::Real(r) => rusqlite::types::Value::Real(r),
186    }
187}
188
189/// Convert from rusqlite Valute to cdk_sql_common::value::Value
190#[inline(always)]
191pub fn from_sqlite(v: rusqlite::types::Value) -> Value {
192    match v {
193        rusqlite::types::Value::Blob(blob) => Value::Blob(blob),
194        rusqlite::types::Value::Integer(i) => Value::Integer(i),
195        rusqlite::types::Value::Null => Value::Null,
196        rusqlite::types::Value::Text(t) => Value::Text(t),
197        rusqlite::types::Value::Real(r) => Value::Real(r),
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::Config;
204
205    #[test]
206    fn config_debug_redacts_sqlcipher_password() {
207        let secret = "sqlcipher-password-secret";
208        let config = Config::from(("wallet.sqlite", secret));
209
210        let debug = format!("{config:?}");
211
212        assert!(debug.contains("wallet.sqlite"));
213        assert!(debug.contains("[REDACTED]"));
214        assert!(!debug.contains(secret));
215    }
216}