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 ¶m 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}