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