Skip to main content

overdrive/
lib.rs

1//! # OverDrive InCode SDK
2//! 
3//! An embeddable document database — like SQLite for JSON.
4//! 
5//! Import the package, open a file, query your data. **No server needed.**
6//! 
7//! ## Quick Start (Rust)
8//! 
9//! ```no_run
10//! use overdrive::OverDriveDB;
11//! 
12//! let mut db = OverDriveDB::open("myapp.odb").unwrap();
13//! db.create_table("users").unwrap();
14//! 
15//! let id = db.insert("users", &serde_json::json!({
16//!     "name": "Alice",
17//!     "email": "alice@example.com",
18//!     "age": 30
19//! })).unwrap();
20//! 
21//! let results = db.query("SELECT * FROM users WHERE age > 25").unwrap();
22//! println!("{:?}", results.rows);
23//! ```
24//!
25//! ## Setup
26//!
27//! 1. `cargo add overdrive-sdk`
28//! 2. Download the native library from [GitHub Releases](https://github.com/ALL-FOR-ONE-TECH/OverDrive-DB_SDK/releases/latest)
29//! 3. Place it in your project directory or on your system PATH
30
31pub mod result;
32pub mod query_engine;
33pub mod ffi;
34pub mod shared;
35mod dynamic;
36
37use result::{SdkResult, SdkError};
38use dynamic::NativeDB;
39use serde_json::Value;
40use std::path::Path;
41use std::time::Instant;
42use zeroize::{Zeroize, ZeroizeOnDrop};
43
44// ─────────────────────────────────────────────
45// SECURITY: Secret key wrapper
46// Zero bytes from RAM automatically on drop
47// ─────────────────────────────────────────────
48
49/// A secret key that is automatically zeroed from memory when dropped.
50/// Use this to hold AES encryption keys — prevents leak via memory dump.
51///
52/// ```no_run
53/// use overdrive::SecretKey;
54/// let key = SecretKey::from_env("ODB_KEY").unwrap();
55/// // ...key bytes are wiped from RAM when `key` is dropped
56/// ```
57#[derive(Zeroize, ZeroizeOnDrop)]
58pub struct SecretKey(Vec<u8>);
59
60impl SecretKey {
61    /// Create a `SecretKey` from raw bytes.
62    pub fn new(bytes: Vec<u8>) -> Self {
63        Self(bytes)
64    }
65
66    /// Read key bytes from an environment variable.
67    ///
68    /// Returns `SecurityError` if the env var is not set or is empty.
69    pub fn from_env(env_var: &str) -> SdkResult<Self> {
70        let val = std::env::var(env_var).map_err(|_| {
71            SdkError::SecurityError(format!(
72                "Encryption key env var '{}' is not set. \
73                 Set it with: $env:{}=\"your-secret-key\" (PowerShell) \
74                 or export {}=\"your-secret-key\" (bash)",
75                env_var, env_var, env_var
76            ))
77        })?;
78        if val.is_empty() {
79            return Err(SdkError::SecurityError(format!(
80                "Encryption key env var '{}' is set but empty.", env_var
81            )));
82        }
83        Ok(Self(val.into_bytes()))
84    }
85
86    /// Raw key bytes (use sparingly — minimize time in scope).
87    pub fn as_bytes(&self) -> &[u8] {
88        &self.0
89    }
90}
91
92// ─────────────────────────────────────────────
93// SECURITY: OS-level file permission hardening
94// ─────────────────────────────────────────────
95
96/// Set restrictive OS-level permissions on the `.odb` file:
97/// - **Windows**: `icacls` — removes all inherit ACEs, grants only current user Full Control
98/// - **Linux/macOS**: `chmod 600` — owner read/write only
99///
100/// Called automatically inside `OverDriveDB::open()`.
101pub fn set_secure_permissions(path: &str) -> SdkResult<()> {
102    #[cfg(target_os = "windows")]
103    {
104        // Reset all inherited permissions and grant only current user
105        let output = std::process::Command::new("icacls")
106            .args([path, "/inheritance:r", "/grant:r", "%USERNAME%:F"])
107            .output();
108        match output {
109            Ok(out) if out.status.success() => {}
110            Ok(out) => {
111                let stderr = String::from_utf8_lossy(&out.stderr);
112                // Non-fatal: log but don't fail (icacls may not be available on all setups)
113                eprintln!("[overdrive-sdk] WARNING: Could not set file permissions on '{}': {}", path, stderr);
114            }
115            Err(e) => {
116                eprintln!("[overdrive-sdk] WARNING: icacls not available, file permissions not hardened: {}", e);
117            }
118        }
119    }
120    #[cfg(not(target_os = "windows"))]
121    {
122        use std::os::unix::fs::PermissionsExt;
123        if Path::new(path).exists() {
124            std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
125                .map_err(|e| SdkError::SecurityError(format!(
126                    "Failed to chmod 600 '{}': {}", path, e
127                )))?;
128        }
129    }
130    Ok(())
131}
132
133/// Query result returned by `query()`
134#[derive(Debug, Clone)]
135pub struct QueryResult {
136    /// Result rows (JSON objects)
137    pub rows: Vec<Value>,
138    /// Column names (for SELECT queries)
139    pub columns: Vec<String>,
140    /// Number of rows affected (for INSERT/UPDATE/DELETE)
141    pub rows_affected: usize,
142    /// Query execution time in milliseconds
143    pub execution_time_ms: f64,
144}
145
146impl QueryResult {
147    fn empty() -> Self {
148        Self {
149            rows: Vec::new(),
150            columns: Vec::new(),
151            rows_affected: 0,
152            execution_time_ms: 0.0,
153        }
154    }
155}
156
157/// Database statistics (expanded for Phase 5)
158#[derive(Debug, Clone)]
159pub struct Stats {
160    pub tables: usize,
161    pub total_records: usize,
162    pub file_size_bytes: u64,
163    pub path: String,
164    /// Number of active MVCC versions in memory
165    pub mvcc_active_versions: usize,
166    /// Database page size in bytes (typically 4096)
167    pub page_size: usize,
168    /// SDK version string
169    pub sdk_version: String,
170}
171
172/// MVCC Isolation level for transactions
173#[derive(Debug, Clone, Copy, PartialEq)]
174pub enum IsolationLevel {
175    /// Read uncommitted data (fastest, least safe)
176    ReadUncommitted = 0,
177    /// Read only committed data (default)
178    ReadCommitted = 1,
179    /// Repeatable reads within the transaction
180    RepeatableRead = 2,
181    /// Full serializable isolation (slowest, safest)
182    Serializable = 3,
183}
184
185impl IsolationLevel {
186    pub fn from_i32(val: i32) -> Self {
187        match val {
188            0 => IsolationLevel::ReadUncommitted,
189            1 => IsolationLevel::ReadCommitted,
190            2 => IsolationLevel::RepeatableRead,
191            3 => IsolationLevel::Serializable,
192            _ => IsolationLevel::ReadCommitted,
193        }
194    }
195}
196
197/// A handle for an active MVCC transaction
198#[derive(Debug, Clone)]
199pub struct TransactionHandle {
200    /// Unique transaction ID
201    pub txn_id: u64,
202    /// Isolation level of this transaction
203    pub isolation: IsolationLevel,
204    /// Whether this transaction is still active
205    pub active: bool,
206}
207
208/// Result of an integrity verification check
209#[derive(Debug, Clone)]
210pub struct IntegrityReport {
211    /// Whether the database passed all checks
212    pub is_valid: bool,
213    /// Total pages checked
214    pub pages_checked: usize,
215    /// Total tables verified
216    pub tables_verified: usize,
217    /// List of issues found (empty if valid)
218    pub issues: Vec<String>,
219}
220
221/// OverDrive InCode SDK — Embeddable document database
222/// 
223/// Use this struct to create, open, and interact with OverDrive databases
224/// directly in your application. No server required.
225pub struct OverDriveDB {
226    native: NativeDB,
227    path: String,
228}
229
230impl OverDriveDB {
231    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
232    // DATABASE LIFECYCLE
233    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
234
235    /// Open an existing database or create a new one.
236    ///
237    /// File permissions are automatically hardened on open (chmod 600 / Windows ACL).
238    pub fn open(path: &str) -> SdkResult<Self> {
239        let native = NativeDB::open(path)?;
240        // Fix 6: Harden file permissions immediately on open
241        let _ = set_secure_permissions(path);
242        Ok(Self {
243            native,
244            path: path.to_string(),
245        })
246    }
247
248    /// Create a new database. Returns an error if the file already exists.
249    pub fn create(path: &str) -> SdkResult<Self> {
250        if Path::new(path).exists() {
251            return Err(SdkError::DatabaseAlreadyExists(path.to_string()));
252        }
253        Self::open(path)
254    }
255
256    /// Open an existing database. Returns an error if the file doesn't exist.
257    pub fn open_existing(path: &str) -> SdkResult<Self> {
258        if !Path::new(path).exists() {
259            return Err(SdkError::DatabaseNotFound(path.to_string()));
260        }
261        Self::open(path)
262    }
263
264    /// Force sync all data to disk.
265    pub fn sync(&self) -> SdkResult<()> {
266        self.native.sync();
267        Ok(())
268    }
269
270    /// Close the database and release all resources.
271    pub fn close(mut self) -> SdkResult<()> {
272        self.native.close();
273        Ok(())
274    }
275
276    /// Delete a database file from disk.
277    pub fn destroy(path: &str) -> SdkResult<()> {
278        if Path::new(path).exists() {
279            std::fs::remove_file(path)?;
280        }
281        Ok(())
282    }
283
284    /// Get the database file path.
285    pub fn path(&self) -> &str {
286        &self.path
287    }
288
289    /// Get the SDK version.
290    pub fn version() -> String {
291        NativeDB::version()
292    }
293
294    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
295    // SECURITY: Encrypted open, backup, WAL cleanup
296    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
297
298    /// Open a database with an encryption key loaded securely from an environment variable.
299    ///
300    /// **Never hardcode** the key — always read from env or a secrets manager.
301    ///
302    /// ```no_run
303    /// // In your shell: $env:ODB_KEY="my-secret-32-char-aes-key!!!!"
304    /// use overdrive::OverDriveDB;
305    /// let mut db = OverDriveDB::open_encrypted("app.odb", "ODB_KEY").unwrap();
306    /// ```
307    pub fn open_encrypted(path: &str, key_env_var: &str) -> SdkResult<Self> {
308        let key = SecretKey::from_env(key_env_var)?;
309        // Pass key to the engine via a dedicated env var the engine reads internally.
310        // This avoids passing the key as a command-line argument (visible in process list).
311        std::env::set_var("__OVERDRIVE_KEY", std::str::from_utf8(key.as_bytes())
312            .map_err(|_| SdkError::SecurityError("Key contains non-UTF8 bytes".to_string()))?);
313        let db = Self::open(path)?;
314        // Immediately remove from env after handoff
315        std::env::remove_var("__OVERDRIVE_KEY");
316        // SecretKey is dropped here — bytes are zeroed
317        Ok(db)
318    }
319
320    /// Create an encrypted backup of the database to `dest_path`.
321    ///
322    /// Syncs all in-memory data to disk first, then copies the `.odb` and `.wal` files.
323    /// Store the backup in a separate physical location or cloud storage.
324    ///
325    /// ```no_run
326    /// # use overdrive::OverDriveDB;
327    /// # let db = OverDriveDB::open("app.odb").unwrap();
328    /// db.backup("backups/app_2026-03-04.odb").unwrap();
329    /// ```
330    pub fn backup(&self, dest_path: &str) -> SdkResult<()> {
331        // Flush all in-memory pages to disk first
332        self.sync()?;
333
334        // Copy the main .odb file
335        std::fs::copy(&self.path, dest_path)
336            .map_err(|e| SdkError::BackupError(format!(
337                "Failed to copy '{}' -> '{}': {}", self.path, dest_path, e
338            )))?;
339
340        // Also copy the WAL file if it exists (crash consistency)
341        let wal_src = format!("{}.wal", self.path);
342        let wal_dst = format!("{}.wal", dest_path);
343        if Path::new(&wal_src).exists() {
344            std::fs::copy(&wal_src, &wal_dst)
345                .map_err(|e| SdkError::BackupError(format!(
346                    "Failed to copy WAL '{}' -> '{}': {}", wal_src, wal_dst, e
347                )))?;
348        }
349
350        // Harden permissions on the backup file too
351        let _ = set_secure_permissions(dest_path);
352        Ok(())
353    }
354
355    /// Delete the WAL (Write-Ahead Log) file after a confirmed commit.
356    ///
357    /// **Call this after `commit_transaction()`** to prevent attackers from replaying
358    /// the WAL file to restore deleted data.
359    ///
360    /// ```no_run
361    /// # use overdrive::{OverDriveDB, IsolationLevel};
362    /// # let mut db = OverDriveDB::open("app.odb").unwrap();
363    /// let txn = db.begin_transaction(IsolationLevel::ReadCommitted).unwrap();
364    /// // ... writes ...
365    /// db.commit_transaction(&txn).unwrap();
366    /// db.cleanup_wal().unwrap(); // Remove stale WAL
367    /// ```
368    pub fn cleanup_wal(&self) -> SdkResult<()> {
369        let wal_path = format!("{}.wal", self.path);
370        if Path::new(&wal_path).exists() {
371            std::fs::remove_file(&wal_path)
372                .map_err(|e| SdkError::IoError(e))?;
373        }
374        Ok(())
375    }
376
377    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
378    // TABLE MANAGEMENT
379    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
380
381    /// Create a new table (schemaless, NoSQL mode).
382    pub fn create_table(&mut self, name: &str) -> SdkResult<()> {
383        self.native.create_table(name)?;
384        Ok(())
385    }
386
387    /// Drop (delete) a table and all its data.
388    pub fn drop_table(&mut self, name: &str) -> SdkResult<()> {
389        self.native.drop_table(name)?;
390        Ok(())
391    }
392
393    /// List all tables in the database.
394    pub fn list_tables(&self) -> SdkResult<Vec<String>> {
395        Ok(self.native.list_tables()?)
396    }
397
398    /// Check if a table exists.
399    pub fn table_exists(&self, name: &str) -> SdkResult<bool> {
400        Ok(self.native.table_exists(name))
401    }
402
403    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
404    // CRUD OPERATIONS
405    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
406
407    /// Insert a JSON document into a table. Returns the auto-generated `_id`.
408    /// 
409    /// ```ignore
410    /// let id = db.insert("users", &serde_json::json!({
411    ///     "name": "Alice",
412    ///     "age": 30,
413    ///     "tags": ["admin", "developer"]
414    /// })).unwrap();
415    /// println!("Inserted: {}", id);
416    /// ```
417    pub fn insert(&mut self, table: &str, doc: &Value) -> SdkResult<String> {
418        let json_str = serde_json::to_string(doc)?;
419        let id = self.native.insert(table, &json_str)?;
420        Ok(id)
421    }
422
423    /// Insert multiple documents in a batch. Returns a list of generated `_id`s.
424    pub fn insert_batch(&mut self, table: &str, docs: &[Value]) -> SdkResult<Vec<String>> {
425        let mut ids = Vec::with_capacity(docs.len());
426        for doc in docs {
427            let id = self.insert(table, doc)?;
428            ids.push(id);
429        }
430        Ok(ids)
431    }
432
433    /// Get a document by its `_id`.
434    pub fn get(&self, table: &str, id: &str) -> SdkResult<Option<Value>> {
435        match self.native.get(table, id)? {
436            Some(json_str) => {
437                let value: Value = serde_json::from_str(&json_str)?;
438                Ok(Some(value))
439            }
440            None => Ok(None),
441        }
442    }
443
444    /// Update a document by its `_id`. Returns `true` if the document was found and updated.
445    /// 
446    /// ```ignore
447    /// db.update("users", &id, &serde_json::json!({
448    ///     "age": 31,
449    ///     "email": "alice@newmail.com"
450    /// })).unwrap();
451    /// ```
452    pub fn update(&mut self, table: &str, id: &str, updates: &Value) -> SdkResult<bool> {
453        let json_str = serde_json::to_string(updates)?;
454        Ok(self.native.update(table, id, &json_str)?)
455    }
456
457    /// Delete a document by its `_id`. Returns `true` if found and deleted.
458    pub fn delete(&mut self, table: &str, id: &str) -> SdkResult<bool> {
459        Ok(self.native.delete(table, id)?)
460    }
461
462    /// Count all documents in a table.
463    pub fn count(&self, table: &str) -> SdkResult<usize> {
464        let count = self.native.count(table)?;
465        Ok(count.max(0) as usize)
466    }
467
468    /// Scan all documents in a table (no filter).
469    pub fn scan(&self, table: &str) -> SdkResult<Vec<Value>> {
470        let result_str = self.native.query(&format!("SELECT * FROM {}", table))?;
471        let result: Value = serde_json::from_str(&result_str)?;
472        let rows = result.get("rows")
473            .and_then(|r| r.as_array())
474            .cloned()
475            .unwrap_or_default();
476        Ok(rows)
477    }
478
479    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
480    // QUERY ENGINE
481    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
482
483    /// Execute an SQL query and return results.
484    /// 
485    /// Supports: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE, SHOW TABLES
486    /// 
487    /// ```ignore
488    /// let result = db.query("SELECT * FROM users WHERE age > 25 ORDER BY name LIMIT 10").unwrap();
489    /// for row in &result.rows {
490    ///     println!("{}", row);
491    /// }
492    /// ```
493    pub fn query(&mut self, sql: &str) -> SdkResult<QueryResult> {
494        let start = Instant::now();
495        let result_str = self.native.query(sql)?;
496        let elapsed = start.elapsed().as_secs_f64() * 1000.0;
497
498        let result: Value = serde_json::from_str(&result_str)?;
499        let rows = result.get("rows")
500            .and_then(|r| r.as_array())
501            .cloned()
502            .unwrap_or_default();
503        let columns = result.get("columns")
504            .and_then(|c| c.as_array())
505            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
506            .unwrap_or_default();
507        let rows_affected = result.get("rows_affected")
508            .and_then(|r| r.as_u64())
509            .unwrap_or(0) as usize;
510
511        Ok(QueryResult {
512            rows,
513            columns,
514            rows_affected,
515            execution_time_ms: elapsed,
516        })
517    }
518
519    /// Execute a **parameterized** SQL query — the safe way to include user input.
520    ///
521    /// Use `?` as placeholders in the SQL template; values are sanitized and
522    /// escaped before substitution. Any param containing SQL injection patterns
523    /// (`DROP`, `DELETE`, `--`, `;`) is rejected with `SecurityError`.
524    ///
525    /// ```ignore
526    /// // SAFE: user input via params, never via string concat
527    /// let name: &str = get_user_input(); // could be "Alice'; DROP TABLE users--"
528    /// let result = db.query_safe(
529    ///     "SELECT * FROM users WHERE name = ?",
530    ///     &[name],
531    /// ).unwrap(); // Blocked: SecurityError if injection detected
532    /// ```
533    pub fn query_safe(&mut self, sql_template: &str, params: &[&str]) -> SdkResult<QueryResult> {
534        /// Dangerous SQL keywords/tokens that signal injection attempts
535        const DANGEROUS: &[&str] = &[
536            "DROP", "TRUNCATE", "ALTER", "EXEC", "EXECUTE",
537            "--", ";--", "/*", "*/", "xp_", "UNION",
538        ];
539
540        // Sanitize each param
541        let mut sanitized: Vec<String> = Vec::with_capacity(params.len());
542        for &param in params {
543            let upper = param.to_uppercase();
544            for &danger in DANGEROUS {
545                if upper.contains(danger) {
546                    return Err(SdkError::SecurityError(format!(
547                        "SQL injection detected in parameter: '{}' contains forbidden token '{}'",
548                        param, danger
549                    )));
550                }
551            }
552            // Escape single quotes by doubling them (SQL standard)
553            let escaped = param.replace('\'', "''");
554            sanitized.push(format!("'{}'", escaped));
555        }
556
557        // Replace ? placeholders in order
558        let mut sql = sql_template.to_string();
559        for value in &sanitized {
560            if let Some(pos) = sql.find('?') {
561                sql.replace_range(pos..pos + 1, value);
562            } else {
563                return Err(SdkError::SecurityError(
564                    "More params than '?' placeholders in SQL template".to_string()
565                ));
566            }
567        }
568
569        // Check no unresolved placeholders remain
570        let remaining = params.len();
571        let placeholder_count = sql_template.chars().filter(|&c| c == '?').count();
572        if remaining < placeholder_count {
573            return Err(SdkError::SecurityError(format!(
574                "SQL template has {} '?' placeholders but only {} params were provided",
575                placeholder_count, remaining
576            )));
577        }
578
579        self.query(&sql)
580    }
581
582    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
583    // SEARCH
584    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
585
586    /// Full-text search across a table.
587    pub fn search(&self, table: &str, text: &str) -> SdkResult<Vec<Value>> {
588        let result_str = self.native.search(table, text)?;
589        let values: Vec<Value> = serde_json::from_str(&result_str).unwrap_or_default();
590        Ok(values)
591    }
592
593    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
594    // STATISTICS
595    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
596
597    /// Get database statistics (expanded with MVCC info).
598    pub fn stats(&self) -> SdkResult<Stats> {
599        let file_size = std::fs::metadata(&self.path)
600            .map(|m| m.len())
601            .unwrap_or(0);
602        let tables = self.list_tables().unwrap_or_default();
603        let mut total_records = 0;
604        for table in &tables {
605            total_records += self.count(table).unwrap_or(0);
606        }
607        Ok(Stats {
608            tables: tables.len(),
609            total_records,
610            file_size_bytes: file_size,
611            path: self.path.clone(),
612            mvcc_active_versions: 0, // Populated by engine when available
613            page_size: 4096,
614            sdk_version: Self::version(),
615        })
616    }
617
618    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
619    // MVCC TRANSACTIONS (Phase 5)
620    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
621
622    /// Begin a new MVCC transaction with the specified isolation level.
623    ///
624    /// ```ignore
625    /// let txn = db.begin_transaction(IsolationLevel::ReadCommitted).unwrap();
626    /// // ... perform reads/writes ...
627    /// db.commit_transaction(&txn).unwrap();
628    /// ```
629    pub fn begin_transaction(&mut self, isolation: IsolationLevel) -> SdkResult<TransactionHandle> {
630        let txn_id = self.native.begin_transaction(isolation as i32)?;
631        Ok(TransactionHandle {
632            txn_id,
633            isolation,
634            active: true,
635        })
636    }
637
638    /// Commit a transaction, making all its changes permanent.
639    pub fn commit_transaction(&mut self, txn: &TransactionHandle) -> SdkResult<()> {
640        self.native.commit_transaction(txn.txn_id)?;
641        Ok(())
642    }
643
644    /// Abort (rollback) a transaction, discarding all its changes.
645    pub fn abort_transaction(&mut self, txn: &TransactionHandle) -> SdkResult<()> {
646        self.native.abort_transaction(txn.txn_id)?;
647        Ok(())
648    }
649
650    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
651    // INTEGRITY VERIFICATION (Phase 5)
652    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
653
654    /// Verify the integrity of the database.
655    ///
656    /// Checks B-Tree consistency, page checksums, and MVCC version chains.
657    /// Returns a detailed report.
658    ///
659    /// ```ignore
660    /// let report = db.verify_integrity().unwrap();
661    /// assert!(report.is_valid);
662    /// println!("Checked {} pages across {} tables", report.pages_checked, report.tables_verified);
663    /// ```
664    pub fn verify_integrity(&self) -> SdkResult<IntegrityReport> {
665        let result_str = self.native.verify_integrity()?;
666        let result: Value = serde_json::from_str(&result_str).unwrap_or_default();
667
668        let is_valid = result.get("valid")
669            .and_then(|v| v.as_bool())
670            .unwrap_or(true);
671        let pages_checked = result.get("pages_checked")
672            .and_then(|v| v.as_u64())
673            .unwrap_or(0) as usize;
674        let tables_verified = result.get("tables_verified")
675            .and_then(|v| v.as_u64())
676            .unwrap_or(0) as usize;
677        let issues = result.get("issues")
678            .and_then(|v| v.as_array())
679            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
680            .unwrap_or_default();
681
682        Ok(IntegrityReport {
683            is_valid,
684            pages_checked,
685            tables_verified,
686            issues,
687        })
688    }
689}
690
691impl Drop for OverDriveDB {
692    fn drop(&mut self) {
693        self.native.close();
694    }
695}