Skip to main content

pqlite/
lib.rs

1// PQLite - Post-Quantum SQLite
2// Copyright (c) 2025-2026 Dyber, Inc. All rights reserved.
3//
4// Safe Rust bindings for PQLite.
5//
6// Usage:
7//   use pqlite::{Connection, Result};
8//
9//   let conn = Connection::open("secure.db")?;
10//   conn.execute_batch("PRAGMA pqc_key='my-password'")?;
11//   conn.execute("CREATE TABLE t(x TEXT)", [])?;
12//   conn.execute("INSERT INTO t VALUES(?1)", ["quantum-safe"])?;
13//
14//   let version: String = conn.query_row(
15//       "SELECT pqc_version()", [], |row| row.get(0)
16//   )?;
17//   println!("PQLite version: {}", version);
18//
19// PQLite is a product of Dyber, Inc.
20
21use libpqlite3_sys as ffi;
22use std::ffi::{CStr, CString};
23use std::os::raw::c_int;
24use std::ptr;
25
26/// PQLite error type.
27#[derive(Debug)]
28pub enum Error {
29    /// SQLite error with code and message.
30    SqliteError { code: i32, message: String },
31    /// UTF-8 conversion error.
32    Utf8Error(std::str::Utf8Error),
33    /// Null pointer error.
34    NullError,
35}
36
37impl std::fmt::Display for Error {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Error::SqliteError { code, message } => write!(f, "SQLite error {}: {}", code, message),
41            Error::Utf8Error(e) => write!(f, "UTF-8 error: {}", e),
42            Error::NullError => write!(f, "Null pointer"),
43        }
44    }
45}
46
47impl std::error::Error for Error {}
48
49pub type Result<T> = std::result::Result<T, Error>;
50
51/// A PQLite database connection.
52pub struct Connection {
53    db: *mut ffi::sqlite3,
54}
55
56// Safety: sqlite3 is thread-safe when compiled with SQLITE_THREADSAFE=1
57unsafe impl Send for Connection {}
58
59impl Connection {
60    /// Open a database file.
61    pub fn open(path: &str) -> Result<Self> {
62        let c_path = CString::new(path).map_err(|_| Error::NullError)?;
63        let mut db: *mut ffi::sqlite3 = ptr::null_mut();
64        let rc = unsafe { ffi::sqlite3_open(c_path.as_ptr(), &mut db) };
65        if rc != ffi::SQLITE_OK {
66            let msg = unsafe { Self::errmsg_raw(db) };
67            unsafe { ffi::sqlite3_close(db) };
68            return Err(Error::SqliteError { code: rc as i32, message: msg });
69        }
70        Ok(Connection { db })
71    }
72
73    /// Open an in-memory database.
74    pub fn open_in_memory() -> Result<Self> {
75        Self::open(":memory:")
76    }
77
78    /// Execute a SQL statement that returns no rows.
79    pub fn execute(&self, sql: &str, params: &[&str]) -> Result<usize> {
80        // Simple case: no params
81        if params.is_empty() {
82            return self.execute_batch(sql);
83        }
84
85        let c_sql = CString::new(sql).map_err(|_| Error::NullError)?;
86        let mut stmt: *mut ffi::sqlite3_stmt = ptr::null_mut();
87        let rc = unsafe {
88            ffi::sqlite3_prepare_v2(self.db, c_sql.as_ptr(), -1, &mut stmt, ptr::null_mut())
89        };
90        if rc != ffi::SQLITE_OK {
91            return Err(self.last_error());
92        }
93
94        // Bind parameters
95        for (i, param) in params.iter().enumerate() {
96            let c_param = CString::new(*param).map_err(|_| Error::NullError)?;
97            unsafe {
98                ffi::sqlite3_bind_text(stmt, (i + 1) as c_int, c_param.as_ptr(), -1, ffi::SQLITE_TRANSIENT);
99            }
100        }
101
102        let rc = unsafe { ffi::sqlite3_step(stmt) };
103        unsafe { ffi::sqlite3_finalize(stmt) };
104
105        match rc {
106            ffi::SQLITE_DONE | ffi::SQLITE_ROW => Ok(0),
107            _ => Err(self.last_error()),
108        }
109    }
110
111    /// Execute one or more SQL statements (no result).
112    pub fn execute_batch(&self, sql: &str) -> Result<usize> {
113        let c_sql = CString::new(sql).map_err(|_| Error::NullError)?;
114        let mut errmsg: *mut i8 = ptr::null_mut();
115        let rc = unsafe {
116            ffi::sqlite3_exec(self.db, c_sql.as_ptr(), None, ptr::null_mut(), &mut errmsg)
117        };
118        if rc != ffi::SQLITE_OK {
119            let msg = if !errmsg.is_null() {
120                let s = unsafe { CStr::from_ptr(errmsg) }.to_string_lossy().into_owned();
121                unsafe { ffi::sqlite3_free(errmsg as *mut std::os::raw::c_void) };
122                s
123            } else {
124                "unknown error".to_string()
125            };
126            return Err(Error::SqliteError { code: rc as i32, message: msg });
127        }
128        Ok(0)
129    }
130
131    /// Set the PQC encryption key.
132    pub fn pqc_key(&self, password: &str) -> Result<()> {
133        self.execute_batch(&format!("PRAGMA pqc_key='{}'", password))
134            .map(|_| ())
135    }
136
137    /// Get the PQLite version string.
138    pub fn pqc_version(&self) -> Result<String> {
139        let c_sql = CString::new("SELECT pqc_version()").unwrap();
140        let mut stmt: *mut ffi::sqlite3_stmt = ptr::null_mut();
141        let rc = unsafe {
142            ffi::sqlite3_prepare_v2(self.db, c_sql.as_ptr(), -1, &mut stmt, ptr::null_mut())
143        };
144        if rc != ffi::SQLITE_OK { return Err(self.last_error()); }
145
146        let rc = unsafe { ffi::sqlite3_step(stmt) };
147        if rc == ffi::SQLITE_ROW {
148            let text = unsafe { ffi::sqlite3_column_text(stmt, 0) };
149            let result = if !text.is_null() {
150                unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned()
151            } else {
152                String::new()
153            };
154            unsafe { ffi::sqlite3_finalize(stmt) };
155            Ok(result)
156        } else {
157            unsafe { ffi::sqlite3_finalize(stmt) };
158            Err(self.last_error())
159        }
160    }
161
162    fn last_error(&self) -> Error {
163        Error::SqliteError {
164            code: unsafe { ffi::sqlite3_errcode(self.db) } as i32,
165            message: unsafe { Self::errmsg_raw(self.db) },
166        }
167    }
168
169    unsafe fn errmsg_raw(db: *mut ffi::sqlite3) -> String {
170        if db.is_null() { return "null database".to_string(); }
171        let msg = ffi::sqlite3_errmsg(db);
172        if msg.is_null() { return "unknown error".to_string(); }
173        CStr::from_ptr(msg).to_string_lossy().into_owned()
174    }
175}
176
177impl Drop for Connection {
178    fn drop(&mut self) {
179        if !self.db.is_null() {
180            unsafe { ffi::sqlite3_close_v2(self.db) };
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_open_memory() {
191        let conn = Connection::open_in_memory().unwrap();
192        conn.execute_batch("CREATE TABLE t(x)").unwrap();
193        conn.execute_batch("INSERT INTO t VALUES('hello')").unwrap();
194    }
195}