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