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, UriBuilder};
38//! use rusqlite::Connection;
39//!
40//! // Register VFS in URI mode (no global config)
41//! SqliteObjsVfs::register_uri(false)?;
42//!
43//! // Build URI with proper URL encoding
44//! let uri = UriBuilder::new("mydb.db", "myaccount", "databases")
45//! .sas_token("sv=2024-08-04&ss=b&srt=sco&sp=rwdlacyx&se=2026-01-01T00:00:00Z&sig=abc123")
46//! .cache_dir("/var/cache/myapp")
47//! .cache_reuse(true)
48//! .build();
49//!
50//! // Open database with Azure credentials in URI
51//! let conn = Connection::open_with_flags_and_vfs(
52//! &uri,
53//! rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE | rusqlite::OpenFlags::SQLITE_OPEN_URI,
54//! "sqlite-objs"
55//! )?;
56//! # Ok::<(), Box<dyn std::error::Error>>(())
57//! ```
58//!
59//! ### Explicit Configuration
60//!
61//! ```no_run
62//! use sqlite_objs::{SqliteObjsVfs, SqliteObjsConfig};
63//! use rusqlite::Connection;
64//!
65//! let config = SqliteObjsConfig {
66//! account: "myaccount".to_string(),
67//! container: "databases".to_string(),
68//! sas_token: Some("sv=2024-08-04&...".to_string()),
69//! account_key: None,
70//! endpoint: None,
71//! };
72//!
73//! SqliteObjsVfs::register_with_config(&config, false)?;
74//!
75//! let conn = Connection::open_with_flags_and_vfs(
76//! "mydb.db",
77//! rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE | rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
78//! "sqlite-objs"
79//! )?;
80//! # Ok::<(), Box<dyn std::error::Error>>(())
81//! ```
82
83use std::ffi::CString;
84use std::fmt::Write;
85use std::ptr;
86use thiserror::Error;
87
88pub mod metrics;
89
90#[cfg(feature = "rusqlite")]
91pub mod pragmas;
92
93/// Error type for sqlite-objs operations.
94#[derive(Error, Debug)]
95pub enum SqliteObjsError {
96 /// SQLite returned an error code
97 #[error("SQLite error: {0}")]
98 Sqlite(i32),
99
100 /// Invalid configuration (e.g., null bytes in strings)
101 #[error("Invalid configuration: {0}")]
102 InvalidConfig(String),
103
104 /// VFS registration failed
105 #[error("VFS registration failed: {0}")]
106 RegistrationFailed(String),
107
108 /// Failed to parse VFS metrics output
109 #[error("Metrics parse error: {0}")]
110 MetricsParse(String),
111}
112
113/// Controls how the VFS prefetches blob data on open.
114///
115/// Passed to [`UriBuilder::prefetch`] and emitted as the `prefetch` URI
116/// parameter.
117///
118/// # Example
119///
120/// ```
121/// use sqlite_objs::{UriBuilder, PrefetchMode};
122///
123/// let uri = UriBuilder::new("mydb.db", "acct", "cont")
124/// .sas_token("tok")
125/// .prefetch(PrefetchMode::None)
126/// .build();
127/// assert!(uri.contains("prefetch=none"));
128/// ```
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
130pub enum PrefetchMode {
131 /// Download the entire blob into the local cache when the database is
132 /// opened. This is the default behaviour and gives the best read
133 /// performance for workloads that touch most pages.
134 #[default]
135 All,
136 /// Lazy mode — only fetch individual pages from Azure on demand.
137 /// Useful for large databases where you read a small subset of pages.
138 None,
139}
140
141impl PrefetchMode {
142 /// Returns the URI parameter value accepted by the C VFS.
143 fn as_uri_value(self) -> &'static str {
144 match self {
145 PrefetchMode::All => "all",
146 PrefetchMode::None => "none",
147 }
148 }
149}
150
151/// Result type for sqlite-objs operations.
152pub type Result<T> = std::result::Result<T, SqliteObjsError>;
153
154/// Configuration for the sqlite-objs VFS.
155///
156/// Maps to the C `sqlite_objs_config_t` struct. All fields are owned strings
157/// for safety and convenience.
158#[derive(Debug, Clone)]
159pub struct SqliteObjsConfig {
160 /// Azure Storage account name
161 pub account: String,
162 /// Blob container name
163 pub container: String,
164 /// SAS token (preferred)
165 pub sas_token: Option<String>,
166 /// Shared Key (fallback)
167 pub account_key: Option<String>,
168 /// Custom endpoint (e.g., for Azurite)
169 pub endpoint: Option<String>,
170}
171
172/// Handle to the sqlite-objs VFS.
173///
174/// The VFS is registered globally and persists for the lifetime of the process.
175/// This is a zero-sized type that provides static methods for VFS registration.
176pub struct SqliteObjsVfs;
177
178impl SqliteObjsVfs {
179 /// Register the sqlite-objs VFS using environment variables.
180 ///
181 /// Reads configuration from:
182 /// - `AZURE_STORAGE_ACCOUNT`
183 /// - `AZURE_STORAGE_CONTAINER`
184 /// - `AZURE_STORAGE_SAS` (checked first)
185 /// - `AZURE_STORAGE_KEY` (fallback)
186 ///
187 /// # Arguments
188 ///
189 /// * `make_default` - If true, sqlite-objs becomes the default VFS for all connections
190 ///
191 /// # Errors
192 ///
193 /// Returns an error if registration fails (e.g., missing environment variables).
194 pub fn register(make_default: bool) -> Result<()> {
195 let rc = unsafe { sqlite_objs_sys::sqlite_objs_vfs_register(make_default as i32) };
196 if rc == sqlite_objs_sys::SQLITE_OK {
197 Ok(())
198 } else {
199 Err(SqliteObjsError::RegistrationFailed(format!(
200 "sqlite_objs_vfs_register returned {}",
201 rc
202 )))
203 }
204 }
205
206 /// Register the sqlite-objs VFS with explicit configuration.
207 ///
208 /// # Arguments
209 ///
210 /// * `config` - Azure Storage configuration
211 /// * `make_default` - If true, sqlite-objs becomes the default VFS for all connections
212 ///
213 /// # Errors
214 ///
215 /// Returns an error if the configuration contains invalid data (null bytes)
216 /// or if registration fails.
217 pub fn register_with_config(config: &SqliteObjsConfig, make_default: bool) -> Result<()> {
218 // Convert Rust strings to C strings
219 let account = CString::new(config.account.as_str())
220 .map_err(|_| SqliteObjsError::InvalidConfig("account contains null byte".into()))?;
221 let container = CString::new(config.container.as_str())
222 .map_err(|_| SqliteObjsError::InvalidConfig("container contains null byte".into()))?;
223
224 let sas_token = config
225 .sas_token
226 .as_ref()
227 .map(|s| CString::new(s.as_str()))
228 .transpose()
229 .map_err(|_| SqliteObjsError::InvalidConfig("sas_token contains null byte".into()))?;
230
231 let account_key = config
232 .account_key
233 .as_ref()
234 .map(|s| CString::new(s.as_str()))
235 .transpose()
236 .map_err(|_| SqliteObjsError::InvalidConfig("account_key contains null byte".into()))?;
237
238 let endpoint = config
239 .endpoint
240 .as_ref()
241 .map(|s| CString::new(s.as_str()))
242 .transpose()
243 .map_err(|_| SqliteObjsError::InvalidConfig("endpoint contains null byte".into()))?;
244
245 let c_config = sqlite_objs_sys::sqlite_objs_config_t {
246 account: account.as_ptr(),
247 container: container.as_ptr(),
248 sas_token: sas_token
249 .as_ref()
250 .map(|s| s.as_ptr())
251 .unwrap_or(ptr::null()),
252 account_key: account_key
253 .as_ref()
254 .map(|s| s.as_ptr())
255 .unwrap_or(ptr::null()),
256 endpoint: endpoint.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()),
257 ops: ptr::null(),
258 ops_ctx: ptr::null_mut(),
259 };
260
261 let rc = unsafe {
262 sqlite_objs_sys::sqlite_objs_vfs_register_with_config(&c_config, make_default as i32)
263 };
264
265 if rc == sqlite_objs_sys::SQLITE_OK {
266 Ok(())
267 } else {
268 Err(SqliteObjsError::RegistrationFailed(format!(
269 "sqlite_objs_vfs_register_with_config returned {}",
270 rc
271 )))
272 }
273 }
274
275 /// Register the sqlite-objs VFS in URI mode.
276 ///
277 /// In this mode, Azure credentials must be provided via URI parameters for each database:
278 ///
279 /// ```text
280 /// file:mydb.db?azure_account=acct&azure_container=cont&azure_sas=token
281 /// ```
282 ///
283 /// Supported URI parameters:
284 /// - `azure_account` (required)
285 /// - `azure_container`
286 /// - `azure_sas`
287 /// - `azure_key`
288 /// - `azure_endpoint`
289 ///
290 /// # Arguments
291 ///
292 /// * `make_default` - If true, sqlite-objs becomes the default VFS for all connections
293 ///
294 /// # Errors
295 ///
296 /// Returns an error if registration fails.
297 pub fn register_uri(make_default: bool) -> Result<()> {
298 let rc = unsafe { sqlite_objs_sys::sqlite_objs_vfs_register_uri(make_default as i32) };
299 if rc == sqlite_objs_sys::SQLITE_OK {
300 Ok(())
301 } else {
302 Err(SqliteObjsError::RegistrationFailed(format!(
303 "sqlite_objs_vfs_register_uri returned {}",
304 rc
305 )))
306 }
307 }
308}
309
310/// Builder for constructing sqlite-objs URIs with proper URL encoding.
311///
312/// SQLite URIs use query parameters to pass Azure credentials. SAS tokens contain
313/// special characters (`&`, `=`, `%`) that must be percent-encoded to avoid breaking
314/// the URI query string.
315///
316/// # Example
317///
318/// ```
319/// use sqlite_objs::UriBuilder;
320///
321/// let uri = UriBuilder::new("mydb.db", "myaccount", "databases")
322/// .sas_token("sv=2024-08-04&ss=b&srt=sco&sp=rwdlacyx&se=2026-01-01T00:00:00Z&sig=abc123")
323/// .build();
324///
325/// // URI is properly encoded:
326/// // file:mydb.db?azure_account=myaccount&azure_container=databases&azure_sas=sv%3D2024-08-04%26ss%3Db...
327/// ```
328///
329/// # Authentication
330///
331/// Use either `sas_token()` or `account_key()`, not both. If both are set, `sas_token`
332/// takes precedence.
333pub struct UriBuilder {
334 database: String,
335 account: String,
336 container: String,
337 sas_token: Option<String>,
338 account_key: Option<String>,
339 endpoint: Option<String>,
340 cache_dir: Option<String>,
341 cache_reuse: bool,
342 prefetch: Option<PrefetchMode>,
343}
344
345impl UriBuilder {
346 /// Create a new URI builder with required parameters.
347 ///
348 /// # Arguments
349 ///
350 /// * `database` - Database filename (e.g., "mydb.db")
351 /// * `account` - Azure Storage account name
352 /// * `container` - Blob container name
353 pub fn new(database: &str, account: &str, container: &str) -> Self {
354 Self {
355 database: database.to_string(),
356 account: account.to_string(),
357 container: container.to_string(),
358 sas_token: None,
359 account_key: None,
360 endpoint: None,
361 cache_dir: None,
362 cache_reuse: false,
363 prefetch: None,
364 }
365 }
366
367 /// Set the SAS token for authentication (preferred).
368 ///
369 /// The token will be URL-encoded automatically. Do not encode it yourself.
370 pub fn sas_token(mut self, token: &str) -> Self {
371 self.sas_token = Some(token.to_string());
372 self
373 }
374
375 /// Set the account key for Shared Key authentication (fallback).
376 ///
377 /// The key will be URL-encoded automatically.
378 pub fn account_key(mut self, key: &str) -> Self {
379 self.account_key = Some(key.to_string());
380 self
381 }
382
383 /// Set a custom endpoint (e.g., for Azurite: "http://127.0.0.1:10000").
384 pub fn endpoint(mut self, endpoint: &str) -> Self {
385 self.endpoint = Some(endpoint.to_string());
386 self
387 }
388
389 /// Set the local cache directory for downloaded database files.
390 ///
391 /// If not set, defaults to `/tmp`. The directory will be created if it doesn't exist.
392 pub fn cache_dir(mut self, dir: &str) -> Self {
393 self.cache_dir = Some(dir.to_string());
394 self
395 }
396
397 /// Enable persistent cache reuse across database connections.
398 ///
399 /// When enabled, the local cache file is kept after closing the database.
400 /// On reopen, the VFS checks the blob's ETag — if unchanged, the cached
401 /// file is reused instead of re-downloading (saving ~20s for large databases).
402 ///
403 /// Requires `cache_dir` to be set for predictable cache file locations.
404 /// Default: `false` (cache files are deleted on close).
405 pub fn cache_reuse(mut self, enabled: bool) -> Self {
406 self.cache_reuse = enabled;
407 self
408 }
409
410 /// Set the prefetch mode for blob data loading.
411 ///
412 /// - [`PrefetchMode::All`] (default) — download the entire blob into the
413 /// local cache when the database is opened.
414 /// - [`PrefetchMode::None`] — lazy mode; pages are fetched from Azure only
415 /// when SQLite reads them.
416 ///
417 /// Only emitted as a URI parameter when explicitly set to a non-default
418 /// value, keeping URIs short in the common case.
419 ///
420 /// # Example
421 ///
422 /// ```
423 /// use sqlite_objs::{UriBuilder, PrefetchMode};
424 ///
425 /// let uri = UriBuilder::new("big.db", "acct", "cont")
426 /// .sas_token("tok")
427 /// .prefetch(PrefetchMode::None)
428 /// .build();
429 /// assert!(uri.contains("&prefetch=none"));
430 /// ```
431 pub fn prefetch(mut self, mode: PrefetchMode) -> Self {
432 self.prefetch = Some(mode);
433 self
434 }
435
436 /// Build the URI string with proper URL encoding.
437 ///
438 /// Returns a SQLite URI in the format:
439 /// `file:{database}?azure_account={account}&azure_container={container}&...`
440 pub fn build(self) -> String {
441 let mut uri = format!(
442 "file:{}?azure_account={}&azure_container={}",
443 self.database,
444 percent_encode(&self.account),
445 percent_encode(&self.container)
446 );
447
448 // Prefer SAS token over account key
449 if let Some(sas) = &self.sas_token {
450 uri.push_str("&azure_sas=");
451 uri.push_str(&percent_encode(sas));
452 } else if let Some(key) = &self.account_key {
453 uri.push_str("&azure_key=");
454 uri.push_str(&percent_encode(key));
455 }
456
457 if let Some(endpoint) = &self.endpoint {
458 uri.push_str("&azure_endpoint=");
459 uri.push_str(&percent_encode(endpoint));
460 }
461
462 if let Some(cache_dir) = &self.cache_dir {
463 uri.push_str("&cache_dir=");
464 uri.push_str(&percent_encode(cache_dir));
465 }
466
467 if self.cache_reuse {
468 uri.push_str("&cache_reuse=1");
469 }
470
471 if let Some(mode) = self.prefetch {
472 uri.push_str("&prefetch=");
473 uri.push_str(mode.as_uri_value());
474 }
475
476 uri
477 }
478}
479
480/// Percent-encode a string for use in URI query parameters.
481///
482/// Encodes characters that have special meaning in URIs:
483/// - Reserved: `&`, `=`, `%`, `#`, `?`, `+`, `/`, `:`, `@`
484/// - Space
485///
486/// This is a minimal implementation sufficient for SQLite URI parameters.
487/// Uses uppercase hex digits per RFC 3986.
488fn percent_encode(s: &str) -> String {
489 let mut result = String::with_capacity(s.len() * 2);
490
491 for byte in s.bytes() {
492 match byte {
493 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
494 // Unreserved characters (RFC 3986 section 2.3)
495 result.push(byte as char);
496 }
497 _ => {
498 // Encode everything else
499 write!(result, "%{:02X}", byte).unwrap();
500 }
501 }
502 }
503
504 result
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn test_register_uri() {
513 // URI mode should succeed without config
514 SqliteObjsVfs::register_uri(false).expect("URI registration should succeed");
515 }
516
517 #[test]
518 fn test_config_with_sas() {
519 let config = SqliteObjsConfig {
520 account: "testaccount".to_string(),
521 container: "testcontainer".to_string(),
522 sas_token: Some("sv=2024-08-04&sig=test".to_string()),
523 account_key: None,
524 endpoint: None,
525 };
526
527 // This will fail since we don't have real Azure creds,
528 // but it tests the FFI layer
529 let _ = SqliteObjsVfs::register_with_config(&config, false);
530 }
531
532 #[test]
533 fn test_invalid_config() {
534 let config = SqliteObjsConfig {
535 account: "test\0account".to_string(),
536 container: "container".to_string(),
537 sas_token: None,
538 account_key: None,
539 endpoint: None,
540 };
541
542 let result = SqliteObjsVfs::register_with_config(&config, false);
543 assert!(matches!(result, Err(SqliteObjsError::InvalidConfig(_))));
544 }
545
546 #[test]
547 fn test_uri_builder_basic() {
548 let uri = UriBuilder::new("mydb.db", "myaccount", "mycontainer").build();
549 assert_eq!(
550 uri,
551 "file:mydb.db?azure_account=myaccount&azure_container=mycontainer"
552 );
553 }
554
555 #[test]
556 fn test_uri_builder_with_sas() {
557 let uri = UriBuilder::new("mydb.db", "myaccount", "mycontainer")
558 .sas_token("sv=2024-08-04&ss=b&srt=sco&sp=rwdlacyx&se=2026-01-01T00:00:00Z&sig=abc123")
559 .build();
560
561 // Verify SAS token is encoded (&, =, :)
562 assert!(uri.contains("azure_sas=sv%3D2024-08-04%26ss%3Db%26srt%3Dsco%26sp%3Drwdlacyx%26se%3D2026-01-01T00%3A00%3A00Z%26sig%3Dabc123"));
563 assert!(uri.starts_with(
564 "file:mydb.db?azure_account=myaccount&azure_container=mycontainer&azure_sas="
565 ));
566 }
567
568 #[test]
569 fn test_uri_builder_with_account_key() {
570 let uri = UriBuilder::new("test.db", "account", "container")
571 .account_key("my/secret+key==")
572 .build();
573
574 // Verify account key is encoded (/, +, =)
575 assert!(uri.contains("azure_key=my%2Fsecret%2Bkey%3D%3D"));
576 }
577
578 #[test]
579 fn test_uri_builder_with_endpoint() {
580 let uri = UriBuilder::new("test.db", "devstoreaccount1", "testcontainer")
581 .endpoint("http://127.0.0.1:10000/devstoreaccount1")
582 .build();
583
584 // Verify endpoint is encoded (://)
585 assert!(uri.contains("azure_endpoint=http%3A%2F%2F127.0.0.1%3A10000%2Fdevstoreaccount1"));
586 }
587
588 #[test]
589 fn test_uri_builder_sas_precedence() {
590 let uri = UriBuilder::new("test.db", "account", "container")
591 .sas_token("sas_token_value")
592 .account_key("key_value")
593 .build();
594
595 // SAS token should be present, account key should not
596 assert!(uri.contains("azure_sas="));
597 assert!(!uri.contains("azure_key="));
598 }
599
600 #[test]
601 fn test_uri_builder_with_cache_dir() {
602 let uri = UriBuilder::new("mydb.db", "myaccount", "mycontainer")
603 .sas_token("token")
604 .cache_dir("/var/cache/myapp")
605 .build();
606
607 assert!(uri.contains("cache_dir=%2Fvar%2Fcache%2Fmyapp"));
608
609 // Verify it appears after the other parameters
610 let cache_pos = uri.find("cache_dir=").unwrap();
611 let sas_pos = uri.find("azure_sas=").unwrap();
612 assert!(cache_pos > sas_pos);
613 }
614
615 #[test]
616 fn test_uri_builder_cache_dir_without_auth() {
617 let uri = UriBuilder::new("test.db", "account", "container")
618 .cache_dir("/tmp/test")
619 .build();
620
621 assert_eq!(
622 uri,
623 "file:test.db?azure_account=account&azure_container=container&cache_dir=%2Ftmp%2Ftest"
624 );
625 }
626
627 #[test]
628 fn test_uri_builder_cache_reuse_enabled() {
629 let uri = UriBuilder::new("mydb.db", "myaccount", "mycontainer")
630 .sas_token("token")
631 .cache_reuse(true)
632 .build();
633
634 assert!(uri.contains("&cache_reuse=1"));
635 }
636
637 #[test]
638 fn test_uri_builder_cache_reuse_default_omitted() {
639 let uri = UriBuilder::new("mydb.db", "myaccount", "mycontainer")
640 .sas_token("token")
641 .build();
642
643 assert!(!uri.contains("cache_reuse"));
644 }
645
646 #[test]
647 fn test_uri_builder_cache_reuse_with_cache_dir() {
648 let uri = UriBuilder::new("mydb.db", "myaccount", "mycontainer")
649 .sas_token("token")
650 .cache_dir("/var/cache/myapp")
651 .cache_reuse(true)
652 .build();
653
654 assert!(uri.contains("cache_dir=%2Fvar%2Fcache%2Fmyapp"));
655 assert!(uri.contains("&cache_reuse=1"));
656
657 // cache_reuse should appear after cache_dir
658 let dir_pos = uri.find("cache_dir=").unwrap();
659 let reuse_pos = uri.find("cache_reuse=").unwrap();
660 assert!(reuse_pos > dir_pos);
661 }
662
663 #[test]
664 fn test_percent_encode_special_chars() {
665 // Test all special characters that need encoding
666 assert_eq!(percent_encode("hello&world"), "hello%26world");
667 assert_eq!(percent_encode("key=value"), "key%3Dvalue");
668 assert_eq!(percent_encode("100%"), "100%25");
669 assert_eq!(percent_encode("a#b"), "a%23b");
670 assert_eq!(percent_encode("a?b"), "a%3Fb");
671 assert_eq!(percent_encode("a+b"), "a%2Bb");
672 assert_eq!(percent_encode("a/b"), "a%2Fb");
673 assert_eq!(percent_encode("a:b"), "a%3Ab");
674 assert_eq!(percent_encode("a@b"), "a%40b");
675 assert_eq!(percent_encode("hello world"), "hello%20world");
676 }
677
678 #[test]
679 fn test_percent_encode_unreserved() {
680 // Unreserved characters should not be encoded
681 assert_eq!(percent_encode("azAZ09-_.~"), "azAZ09-_.~");
682 }
683
684 #[test]
685 fn test_percent_encode_empty() {
686 assert_eq!(percent_encode(""), "");
687 }
688
689 // -- PrefetchMode + UriBuilder::prefetch tests --
690
691 #[test]
692 fn test_prefetch_mode_default_is_all() {
693 assert_eq!(PrefetchMode::default(), PrefetchMode::All);
694 }
695
696 #[test]
697 fn test_uri_builder_prefetch_none() {
698 let uri = UriBuilder::new("big.db", "acct", "cont")
699 .prefetch(PrefetchMode::None)
700 .build();
701 assert!(uri.contains("&prefetch=none"));
702 }
703
704 #[test]
705 fn test_uri_builder_prefetch_all() {
706 let uri = UriBuilder::new("big.db", "acct", "cont")
707 .prefetch(PrefetchMode::All)
708 .build();
709 assert!(uri.contains("&prefetch=all"));
710 }
711
712 #[test]
713 fn test_uri_builder_prefetch_omitted_by_default() {
714 let uri = UriBuilder::new("test.db", "acct", "cont").build();
715 assert!(!uri.contains("prefetch"));
716 }
717
718 #[test]
719 fn test_uri_builder_prefetch_with_cache() {
720 let uri = UriBuilder::new("test.db", "acct", "cont")
721 .cache_dir("/cache")
722 .cache_reuse(true)
723 .prefetch(PrefetchMode::None)
724 .build();
725 assert!(uri.contains("cache_dir="));
726 assert!(uri.contains("&cache_reuse=1"));
727 assert!(uri.contains("&prefetch=none"));
728
729 // prefetch should appear after cache_reuse
730 let reuse_pos = uri.find("cache_reuse=").unwrap();
731 let prefetch_pos = uri.find("prefetch=").unwrap();
732 assert!(prefetch_pos > reuse_pos);
733 }
734}