Skip to main content

sqlite_objs/
lib.rs

1//! # sqlite-objs - SQLite VFS backed by Azure Blob Storage
2//!
3//! This crate provides safe Rust bindings to sqlite-objs, a SQLite VFS (Virtual File System)
4//! that stores database files in Azure Blob Storage.
5//!
6//! ## Features
7//!
8//! - Store SQLite databases in Azure Blob Storage (page blobs for DB, block blobs for journal)
9//! - Blob lease-based locking for safe concurrent access
10//! - Full-blob caching for performance
11//! - SAS token and Shared Key authentication
12//! - URI-based per-database configuration
13//!
14//! ## Usage
15//!
16//! ### Basic Registration (Environment Variables)
17//!
18//! ```no_run
19//! use sqlite_objs::SqliteObjsVfs;
20//! use rusqlite::Connection;
21//!
22//! // Register VFS from environment variables
23//! SqliteObjsVfs::register(false)?;
24//!
25//! // Open a database using the sqlite-objs VFS
26//! let conn = Connection::open_with_flags_and_vfs(
27//!     "mydb.db",
28//!     rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
29//!     "sqlite-objs"
30//! )?;
31//! # Ok::<(), Box<dyn std::error::Error>>(())
32//! ```
33//!
34//! ### URI Mode (Per-Database Credentials)
35//!
36//! ```no_run
37//! use sqlite_objs::SqliteObjsVfs;
38//! use rusqlite::Connection;
39//!
40//! // Register VFS in URI mode (no global config)
41//! SqliteObjsVfs::register_uri(false)?;
42//!
43//! // Open database with Azure credentials in URI
44//! let conn = Connection::open_with_flags_and_vfs(
45//!     "file:mydb.db?azure_account=myaccount&azure_container=databases&azure_sas=sv=2024...",
46//!     rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE | rusqlite::OpenFlags::SQLITE_OPEN_URI,
47//!     "sqlite-objs"
48//! )?;
49//! # Ok::<(), Box<dyn std::error::Error>>(())
50//! ```
51//!
52//! ### Explicit Configuration
53//!
54//! ```no_run
55//! use sqlite_objs::{SqliteObjsVfs, SqliteObjsConfig};
56//! use rusqlite::Connection;
57//!
58//! let config = SqliteObjsConfig {
59//!     account: "myaccount".to_string(),
60//!     container: "databases".to_string(),
61//!     sas_token: Some("sv=2024-08-04&...".to_string()),
62//!     account_key: None,
63//!     endpoint: None,
64//! };
65//!
66//! SqliteObjsVfs::register_with_config(&config, false)?;
67//!
68//! let conn = Connection::open_with_flags_and_vfs(
69//!     "mydb.db",
70//!     rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
71//!     "sqlite-objs"
72//! )?;
73//! # Ok::<(), Box<dyn std::error::Error>>(())
74//! ```
75
76use std::ffi::CString;
77use std::ptr;
78use thiserror::Error;
79
80/// Error type for sqlite-objs operations.
81#[derive(Error, Debug)]
82pub enum SqliteObjsError {
83    /// SQLite returned an error code
84    #[error("SQLite error: {0}")]
85    Sqlite(i32),
86
87    /// Invalid configuration (e.g., null bytes in strings)
88    #[error("Invalid configuration: {0}")]
89    InvalidConfig(String),
90
91    /// VFS registration failed
92    #[error("VFS registration failed: {0}")]
93    RegistrationFailed(String),
94}
95
96/// Result type for sqlite-objs operations.
97pub type Result<T> = std::result::Result<T, SqliteObjsError>;
98
99/// Configuration for the sqlite-objs VFS.
100///
101/// Maps to the C `sqlite_objs_config_t` struct. All fields are owned strings
102/// for safety and convenience.
103#[derive(Debug, Clone)]
104pub struct SqliteObjsConfig {
105    /// Azure Storage account name
106    pub account: String,
107    /// Blob container name
108    pub container: String,
109    /// SAS token (preferred)
110    pub sas_token: Option<String>,
111    /// Shared Key (fallback)
112    pub account_key: Option<String>,
113    /// Custom endpoint (e.g., for Azurite)
114    pub endpoint: Option<String>,
115}
116
117/// Handle to the sqlite-objs VFS.
118///
119/// The VFS is registered globally and persists for the lifetime of the process.
120/// This is a zero-sized type that provides static methods for VFS registration.
121pub struct SqliteObjsVfs;
122
123impl SqliteObjsVfs {
124    /// Register the sqlite-objs VFS using environment variables.
125    ///
126    /// Reads configuration from:
127    /// - `AZURE_STORAGE_ACCOUNT`
128    /// - `AZURE_STORAGE_CONTAINER`
129    /// - `AZURE_STORAGE_SAS` (checked first)
130    /// - `AZURE_STORAGE_KEY` (fallback)
131    ///
132    /// # Arguments
133    ///
134    /// * `make_default` - If true, sqlite-objs becomes the default VFS for all connections
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if registration fails (e.g., missing environment variables).
139    pub fn register(make_default: bool) -> Result<()> {
140        let rc = unsafe { sqlite_objs_sys::sqlite_objs_vfs_register(make_default as i32) };
141        if rc == sqlite_objs_sys::SQLITE_OK {
142            Ok(())
143        } else {
144            Err(SqliteObjsError::RegistrationFailed(format!(
145                "sqlite_objs_vfs_register returned {}",
146                rc
147            )))
148        }
149    }
150
151    /// Register the sqlite-objs VFS with explicit configuration.
152    ///
153    /// # Arguments
154    ///
155    /// * `config` - Azure Storage configuration
156    /// * `make_default` - If true, sqlite-objs becomes the default VFS for all connections
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if the configuration contains invalid data (null bytes)
161    /// or if registration fails.
162    pub fn register_with_config(config: &SqliteObjsConfig, make_default: bool) -> Result<()> {
163        // Convert Rust strings to C strings
164        let account = CString::new(config.account.as_str())
165            .map_err(|_| SqliteObjsError::InvalidConfig("account contains null byte".into()))?;
166        let container = CString::new(config.container.as_str())
167            .map_err(|_| SqliteObjsError::InvalidConfig("container contains null byte".into()))?;
168
169        let sas_token = config
170            .sas_token
171            .as_ref()
172            .map(|s| CString::new(s.as_str()))
173            .transpose()
174            .map_err(|_| SqliteObjsError::InvalidConfig("sas_token contains null byte".into()))?;
175
176        let account_key = config
177            .account_key
178            .as_ref()
179            .map(|s| CString::new(s.as_str()))
180            .transpose()
181            .map_err(|_| SqliteObjsError::InvalidConfig("account_key contains null byte".into()))?;
182
183        let endpoint = config
184            .endpoint
185            .as_ref()
186            .map(|s| CString::new(s.as_str()))
187            .transpose()
188            .map_err(|_| SqliteObjsError::InvalidConfig("endpoint contains null byte".into()))?;
189
190        let c_config = sqlite_objs_sys::sqlite_objs_config_t {
191            account: account.as_ptr(),
192            container: container.as_ptr(),
193            sas_token: sas_token
194                .as_ref()
195                .map(|s| s.as_ptr())
196                .unwrap_or(ptr::null()),
197            account_key: account_key
198                .as_ref()
199                .map(|s| s.as_ptr())
200                .unwrap_or(ptr::null()),
201            endpoint: endpoint
202                .as_ref()
203                .map(|s| s.as_ptr())
204                .unwrap_or(ptr::null()),
205            ops: ptr::null(),
206            ops_ctx: ptr::null_mut(),
207        };
208
209        let rc = unsafe {
210            sqlite_objs_sys::sqlite_objs_vfs_register_with_config(&c_config, make_default as i32)
211        };
212
213        if rc == sqlite_objs_sys::SQLITE_OK {
214            Ok(())
215        } else {
216            Err(SqliteObjsError::RegistrationFailed(format!(
217                "sqlite_objs_vfs_register_with_config returned {}",
218                rc
219            )))
220        }
221    }
222
223    /// Register the sqlite-objs VFS in URI mode.
224    ///
225    /// In this mode, Azure credentials must be provided via URI parameters for each database:
226    ///
227    /// ```text
228    /// file:mydb.db?azure_account=acct&azure_container=cont&azure_sas=token
229    /// ```
230    ///
231    /// Supported URI parameters:
232    /// - `azure_account` (required)
233    /// - `azure_container`
234    /// - `azure_sas`
235    /// - `azure_key`
236    /// - `azure_endpoint`
237    ///
238    /// # Arguments
239    ///
240    /// * `make_default` - If true, sqlite-objs becomes the default VFS for all connections
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if registration fails.
245    pub fn register_uri(make_default: bool) -> Result<()> {
246        let rc = unsafe { sqlite_objs_sys::sqlite_objs_vfs_register_uri(make_default as i32) };
247        if rc == sqlite_objs_sys::SQLITE_OK {
248            Ok(())
249        } else {
250            Err(SqliteObjsError::RegistrationFailed(format!(
251                "sqlite_objs_vfs_register_uri returned {}",
252                rc
253            )))
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_register_uri() {
264        // URI mode should succeed without config
265        SqliteObjsVfs::register_uri(false).expect("URI registration should succeed");
266    }
267
268    #[test]
269    fn test_config_with_sas() {
270        let config = SqliteObjsConfig {
271            account: "testaccount".to_string(),
272            container: "testcontainer".to_string(),
273            sas_token: Some("sv=2024-08-04&sig=test".to_string()),
274            account_key: None,
275            endpoint: None,
276        };
277
278        // This will fail since we don't have real Azure creds,
279        // but it tests the FFI layer
280        let _ = SqliteObjsVfs::register_with_config(&config, false);
281    }
282
283    #[test]
284    fn test_invalid_config() {
285        let config = SqliteObjsConfig {
286            account: "test\0account".to_string(),
287            container: "container".to_string(),
288            sas_token: None,
289            account_key: None,
290            endpoint: None,
291        };
292
293        let result = SqliteObjsVfs::register_with_config(&config, false);
294        assert!(matches!(result, Err(SqliteObjsError::InvalidConfig(_))));
295    }
296}