/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ //! A Rust wrapper for mozStorage. //! //! mozStorage wraps the SQLite C API with support for XPCOM data structures, //! asynchronous statement execution, cleanup on shutdown, and connection //! cloning that propagates attached databases, pragmas, functions, and //! temporary entities. It also collects timing and memory usage stats for //! telemetry, and supports detailed statement logging. Additionally, mozStorage //! makes it possible to use the same connection handle from JS and native //! (C++ and Rust) code. //! //! Most mozStorage objects, like connections, statements, result rows, //! and variants, are thread-safe. Each connection manages a background //! thread that can be used to execute statements asynchronously, without //! blocking the main thread. //! //! This crate provides a thin wrapper to make mozStorage easier to use //! from Rust. It only wraps the synchronous API, so you can either manage //! the entire connection from a background thread, or use the `moz_task` //! crate to dispatch tasks to the connection's async thread. Executing //! synchronous statements on the main thread is not supported, and will //! assert in debug builds. #![allow(non_snake_case)] use std::{borrow::Cow, convert::TryFrom, error, fmt, ops::Deref, result}; use nserror::{nsresult, NS_ERROR_NO_INTERFACE, NS_ERROR_UNEXPECTED}; use nsstring::nsCString; use storage_variant::VariantType; use xpcom::{ getter_addrefs, interfaces::{ mozIStorageAsyncConnection, mozIStorageConnection, mozIStorageStatement, nsIEventTarget, nsIThread, }, RefPtr, XpCom, }; const SQLITE_OK: i32 = 0; pub type Result = result::Result; /// `Conn` wraps a `mozIStorageConnection`. #[derive(Clone)] pub struct Conn { handle: RefPtr, } // This is safe as long as our `mozIStorageConnection` is an instance of // `mozilla::storage::Connection`, which is atomically reference counted. unsafe impl Send for Conn {} unsafe impl Sync for Conn {} impl Conn { /// Wraps a `mozIStorageConnection` in a `Conn`. #[inline] pub fn wrap(connection: RefPtr) -> Conn { Conn { handle: connection } } /// Returns the wrapped `mozIStorageConnection`. #[inline] pub fn connection(&self) -> &mozIStorageConnection { &self.handle } /// Returns the maximum number of bound parameters for statements executed /// on this connection. pub fn variable_limit(&self) -> Result { let mut limit = 0i32; let rv = unsafe { self.handle.GetVariableLimit(&mut limit) }; if rv.failed() { return Err(Error::Limit); } usize::try_from(limit).map_err(|_| Error::Limit) } /// Returns the async thread for this connection. This can be used /// with `moz_task` to run synchronous statements on the storage /// thread, without blocking the main thread. pub fn thread(&self) -> Result> { let target = self.handle.get_interface::(); target .and_then(|t| t.query_interface::()) .ok_or(Error::NoThread) } /// Prepares a SQL statement. `query` should only contain one SQL statement. /// If `query` contains multiple statements, only the first will be prepared, /// and the rest will be ignored. pub fn prepare>(&self, query: Q) -> Result { let statement = self.call_and_wrap_error(DatabaseOp::Prepare, || { getter_addrefs(|p| unsafe { self.handle .CreateStatement(&*nsCString::from(query.as_ref()), p) }) })?; Ok(Statement { conn: self, handle: statement, }) } /// Executes a SQL statement. `query` may contain one or more /// semicolon-separated SQL statements. pub fn exec>(&self, query: Q) -> Result<()> { self.call_and_wrap_error(DatabaseOp::Exec, || { unsafe { self.handle .ExecuteSimpleSQL(&*nsCString::from(query.as_ref())) } .to_result() }) } /// Opens a transaction with the default transaction behavior for this /// connection. The transaction should be committed when done. Uncommitted /// `Transaction`s will automatically roll back when they go out of scope. pub fn transaction(&mut self) -> Result { let behavior = self.get_default_transaction_behavior(); Transaction::new(self, behavior) } /// Indicates if a transaction is currently open on this connection. /// Attempting to open a new transaction when one is already in progress /// will fail with a "cannot start a transaction within a transaction" /// error. /// /// Note that this is `true` even if the transaction was started by another /// caller, like `Sqlite.sys.mjs` or `mozStorageTransaction` from C++. See the /// explanation above `mozIStorageConnection.transactionInProgress` for why /// this matters. pub fn transaction_in_progress(&self) -> Result { let mut in_progress = false; unsafe { self.handle.GetTransactionInProgress(&mut in_progress) }.to_result()?; Ok(in_progress) } /// Opens a transaction with the requested behavior. pub fn transaction_with_behavior( &mut self, behavior: TransactionBehavior, ) -> Result { Transaction::new(self, behavior) } fn get_default_transaction_behavior(&self) -> TransactionBehavior { let mut typ = 0i32; let rv = unsafe { self.handle.GetDefaultTransactionType(&mut typ) }; if rv.failed() { return TransactionBehavior::Deferred; } match typ { mozIStorageAsyncConnection::TRANSACTION_IMMEDIATE => TransactionBehavior::Immediate, mozIStorageAsyncConnection::TRANSACTION_EXCLUSIVE => TransactionBehavior::Exclusive, _ => TransactionBehavior::Deferred, } } /// Invokes a storage operation and returns the last SQLite error if the /// operation fails. This lets `Conn::{prepare, exec}` and /// `Statement::{step, execute}` return more detailed errors, as the /// `nsresult` codes that mozStorage uses are often too generic. For /// example, `NS_ERROR_FAILURE` might be anything from a SQL syntax error /// to an invalid column name in a trigger. /// /// Note that the last error may not be accurate if the underlying /// `mozIStorageConnection` is used concurrently from multiple threads. /// Multithreaded callers that share a connection should serialize their /// uses. fn call_and_wrap_error( &self, op: DatabaseOp, func: impl FnOnce() -> result::Result, ) -> Result { func().or_else(|rv| -> Result { let mut code = 0i32; unsafe { self.handle.GetLastError(&mut code) }.to_result()?; Err(if code != SQLITE_OK { let mut message = nsCString::new(); unsafe { self.handle.GetLastErrorString(&mut *message) }.to_result()?; Error::Database { rv, op, code, message, } } else { rv.into() }) }) } } pub enum TransactionBehavior { Deferred, Immediate, Exclusive, } pub struct Transaction<'c> { conn: &'c mut Conn, active: bool, } impl<'c> Transaction<'c> { /// Opens a transaction on `conn` with the given `behavior`. fn new(conn: &'c mut Conn, behavior: TransactionBehavior) -> Result> { conn.exec(match behavior { TransactionBehavior::Deferred => "BEGIN DEFERRED", TransactionBehavior::Immediate => "BEGIN IMMEDIATE", TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE", })?; Ok(Transaction { conn, active: true }) } /// Commits the transaction. pub fn commit(mut self) -> Result<()> { if self.active { self.conn.exec("COMMIT")?; self.active = false; } Ok(()) } /// Rolls the transaction back. pub fn rollback(mut self) -> Result<()> { self.abort() } fn abort(&mut self) -> Result<()> { if self.active { self.conn.exec("ROLLBACK")?; self.active = false; } Ok(()) } } impl<'c> Deref for Transaction<'c> { type Target = Conn; fn deref(&self) -> &Conn { self.conn } } impl<'c> Drop for Transaction<'c> { fn drop(&mut self) { let _ = self.abort(); } } pub struct Statement<'c> { conn: &'c Conn, handle: RefPtr, } impl<'c> Statement<'c> { /// Binds a parameter at the given `index` to the prepared statement. /// `value` is any type that can be converted into a `Variant`. pub fn bind_by_index(&mut self, index: u32, value: V) -> Result<()> { let variant = value.into_variant(); unsafe { self.handle.BindByIndex(index as u32, variant.coerce()) } .to_result() .map_err(|rv| Error::BindByIndex { rv, data_type: V::type_name(), index, }) } /// Binds a parameter with the given `name` to the prepared statement. pub fn bind_by_name, V: VariantType>(&mut self, name: N, value: V) -> Result<()> { let name = name.as_ref(); let variant = value.into_variant(); unsafe { self.handle .BindByName(&*nsCString::from(name), variant.coerce()) } .to_result() .map_err(|rv| Error::BindByName { rv, data_type: V::type_name(), name: name.into(), }) } /// Executes the statement and returns the next row of data. pub fn step<'s>(&'s mut self) -> Result>> { let has_more = self.conn.call_and_wrap_error(DatabaseOp::Step, || { let mut has_more = false; unsafe { self.handle.ExecuteStep(&mut has_more) }.to_result()?; Ok(has_more) })?; Ok(if has_more { Some(Step(self)) } else { None }) } /// Executes the statement once, discards any data, and resets the /// statement. pub fn execute(&mut self) -> Result<()> { self.conn.call_and_wrap_error(DatabaseOp::Execute, || { unsafe { self.handle.Execute() }.to_result() }) } /// Resets the prepared statement so that it's ready to be executed /// again, and clears any bound parameters. pub fn reset(&mut self) -> Result<()> { unsafe { self.handle.Reset() }.to_result()?; Ok(()) } fn get_column_index(&self, name: &str) -> Result { let mut index = 0u32; let rv = unsafe { self.handle .GetColumnIndex(&*nsCString::from(name), &mut index) }; if rv.succeeded() { Ok(index) } else { Err(Error::InvalidColumn { rv, name: name.into(), }) } } fn get_column_value(&self, index: u32) -> result::Result { let variant = getter_addrefs(|p| unsafe { self.handle.GetVariant(index, p) })?; let value = T::from_variant(variant.coerce())?; Ok(value) } } impl<'c> Drop for Statement<'c> { fn drop(&mut self) { unsafe { self.handle.Finalize() }; } } /// A step is the next row in the result set for a statement. pub struct Step<'c, 's>(&'s mut Statement<'c>); impl<'c, 's> Step<'c, 's> { /// Returns the value of the column at `index` for the current row. pub fn get_by_index(&self, index: u32) -> Result { self.0 .get_column_value(index) .map_err(|rv| Error::GetByIndex { rv, data_type: T::type_name(), index, }) } /// A convenience wrapper that returns the default value for the column /// at `index` if `NULL`. pub fn get_by_index_or_default(&self, index: u32) -> T { self.get_by_index(index).unwrap_or_default() } /// Returns the value of the column specified by `name` for the current row. pub fn get_by_name, T: VariantType>(&self, name: N) -> Result { let name = name.as_ref(); let index = self.0.get_column_index(name)?; self.0 .get_column_value(index) .map_err(|rv| Error::GetByName { rv, data_type: T::type_name(), name: name.into(), }) } /// Returns the default value for the column with the given `name`, or the /// default if the column is `NULL`. pub fn get_by_name_or_default, T: VariantType + Default>(&self, name: N) -> T { self.get_by_name(name).unwrap_or_default() } } /// A database operation, included for better context in error messages. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DatabaseOp { Exec, Prepare, Step, Execute, } impl DatabaseOp { /// Returns a description of the operation to include in an error message. pub fn what(&self) -> &'static str { match self { DatabaseOp::Exec => "execute SQL string", DatabaseOp::Prepare => "prepare statement", DatabaseOp::Step => "step statement", DatabaseOp::Execute => "execute statement", } } } /// Storage errors. #[derive(Debug)] pub enum Error { /// A connection doesn't have a usable async thread. The connection might be /// closed, or the thread manager may have shut down. NoThread, /// Failed to get a limit for a database connection. Limit, /// A database operation failed. The error includes a SQLite result code, /// and an explanation string. Database { rv: nsresult, op: DatabaseOp, code: i32, message: nsCString, }, /// A parameter with the given data type couldn't be bound at this index, /// likely because the index is out of range. BindByIndex { rv: nsresult, data_type: Cow<'static, str>, index: u32, }, /// A parameter with the given type couldn't be bound to this name, likely /// because the statement doesn't have a matching `:`-prefixed parameter /// with the name. BindByName { rv: nsresult, data_type: Cow<'static, str>, name: String, }, /// A column with this name doesn't exist. InvalidColumn { rv: nsresult, name: String }, /// A value of the given type couldn't be accessed at this index. This is /// the error returned when a type conversion fails; for example, requesting /// an `nsString` instead of an `Option` when the column is `NULL`. GetByIndex { rv: nsresult, data_type: Cow<'static, str>, index: u32, }, /// A value of the given type couldn't be accessed for the column with /// this name. GetByName { rv: nsresult, data_type: Cow<'static, str>, name: String, }, /// A storage operation failed for other reasons. Other(nsresult), } impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { None } } impl From for Error { fn from(rv: nsresult) -> Error { Error::Other(rv) } } impl From for nsresult { fn from(err: Error) -> nsresult { match err { Error::NoThread => NS_ERROR_NO_INTERFACE, Error::Limit => NS_ERROR_UNEXPECTED, Error::Database { rv, .. } | Error::BindByIndex { rv, .. } | Error::BindByName { rv, .. } | Error::InvalidColumn { rv, .. } | Error::GetByIndex { rv, .. } | Error::GetByName { rv, .. } | Error::Other(rv) => rv, } } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Error::NoThread => f.write_str("Async thread unavailable for storage connection"), Error::Limit => f.write_str("Failed to get limit for storage connection"), Error::Database { op, code, message, .. } => { if message.is_empty() { write!(f, "Failed to {} with code {}", op.what(), code) } else { write!( f, "Failed to {} with code {} ({})", op.what(), code, message ) } } Error::BindByIndex { data_type, index, .. } => write!(f, "Can't bind {} at {}", data_type, index), Error::BindByName { data_type, name, .. } => write!(f, "Can't bind {} to named parameter {}", data_type, name), Error::InvalidColumn { name, .. } => write!(f, "Column {} doesn't exist", name), Error::GetByIndex { data_type, index, .. } => write!(f, "Can't get {} at {}", data_type, index), Error::GetByName { data_type, name, .. } => write!(f, "Can't get {} for column {}", data_type, name), Error::Other(rv) => write!(f, "Storage operation failed with {}", rv.error_name()), } } }