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}