Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 129 additions & 39 deletions src/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,115 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;

/// Actions are sent to a specific sqlite database, `db` is the name,
/// `package_id` is the [`PackageId`]. Capabilities are checked, you can access another process's
/// database if it has given you the [`crate::Capability`].
#[derive(Debug, Serialize, Deserialize)]
/// Actions are sent to a specific SQLite database. `db` is the name,
/// `package_id` is the [`PackageId`] that created the database. Capabilities
/// are checked: you can access another process's database if it has given
/// you the read and/or write capability to do so.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SqliteRequest {
pub package_id: PackageId,
pub db: String,
pub action: SqliteAction,
}

#[derive(Debug, Serialize, Deserialize)]
/// IPC Action format representing operations that can be performed on the
/// SQLite runtime module. These actions are included in a [`SqliteRequest`]
/// sent to the `sqlite:distro:sys` runtime module.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SqliteAction {
/// Opens an existing key-value database or creates a new one if it doesn't exist.
/// Requires `package_id` in [`SqliteRequest`] to match the package ID of the sender.
/// The sender will own the database and can remove it with [`SqliteAction::RemoveDb`].
///
/// A successful open will respond with [`SqliteResponse::Ok`]. Any error will be
/// contained in the [`SqliteResponse::Err`] variant.
Open,
/// Permanently deletes the entire key-value database.
/// Requires `package_id` in [`SqliteRequest`] to match the package ID of the sender.
/// Only the owner can remove the database.
///
/// A successful remove will respond with [`SqliteResponse::Ok`]. Any error will be
/// contained in the [`SqliteResponse::Err`] variant.
RemoveDb,
/// Executes a write statement (INSERT/UPDATE/DELETE)
///
/// * `statement` - SQL statement to execute
/// * `tx_id` - Optional transaction ID
/// * blob: Vec<SqlValue> - Parameters for the SQL statement, where SqlValue can be:
/// - null
/// - boolean
/// - i64
/// - f64
/// - String
/// - Vec<u8> (binary data)
///
/// Using this action requires the sender to have the write capability
/// for the database.
///
/// A successful write will respond with [`SqliteResponse::Ok`]. Any error will be
/// contained in the [`SqliteResponse::Err`] variant.
Write {
statement: String,
tx_id: Option<u64>,
},
Read {
query: String,
},
/// Executes a read query (SELECT)
///
/// * blob: Vec<SqlValue> - Parameters for the SQL query, where SqlValue can be:
/// - null
/// - boolean
/// - i64
/// - f64
/// - String
/// - Vec<u8> (binary data)
///
/// Using this action requires the sender to have the read capability
/// for the database.
///
/// A successful query will respond with [`SqliteResponse::Query`], where the
/// response blob contains the results of the query. Any error will be contained
/// in the [`SqliteResponse::Err`] variant.
Query(String),
/// Begins a new transaction for atomic operations.
///
/// Sending this will prompt a [`SqliteResponse::BeginTx`] response with the
/// transaction ID. Any error will be contained in the [`SqliteResponse::Err`] variant.
BeginTx,
Commit {
tx_id: u64,
},
Backup,
/// Commits all operations in the specified transaction.
///
/// # Parameters
/// * `tx_id` - The ID of the transaction to commit
///
/// A successful commit will respond with [`SqliteResponse::Ok`]. Any error will be
/// contained in the [`SqliteResponse::Err`] variant.
Commit { tx_id: u64 },
}

#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SqliteResponse {
/// Indicates successful completion of an operation.
/// Sent in response to actions Open, RemoveDb, Write, Query, BeginTx, and Commit.
Ok,
/// Returns the results of a query.
///
/// * blob: Vec<Vec<SqlValue>> - Array of rows, where each row contains SqlValue types:
/// - null
/// - boolean
/// - i64
/// - f64
/// - String
/// - Vec<u8> (binary data)
Read,
/// Returns the transaction ID for a newly created transaction.
///
/// # Fields
/// * `tx_id` - The ID of the newly created transaction
BeginTx { tx_id: u64 },
/// Indicates an error occurred during the operation.
Err(SqliteError),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
/// Used in blobs to represent array row values in SQLite.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum SqlValue {
Integer(i64),
Real(f64),
Expand All @@ -49,28 +121,50 @@ pub enum SqlValue {
Null,
}

#[derive(Debug, Serialize, Deserialize, Error)]
#[derive(Clone, Debug, Serialize, Deserialize, Error)]
pub enum SqliteError {
#[error("sqlite: DbDoesNotExist")]
NoDb,
#[error("sqlite: NoTx")]
NoTx,
#[error("sqlite: No capability: {error}")]
NoCap { error: String },
#[error("sqlite: UnexpectedResponse")]
UnexpectedResponse,
#[error("sqlite: NotAWriteKeyword")]
#[error("db [{0}, {1}] does not exist")]
NoDb(PackageId, String),
#[error("no transaction {0} found")]
NoTx(u64),
#[error("no write capability for requested DB")]
NoWriteCap,
#[error("no read capability for requested DB")]
NoReadCap,
#[error("request to open or remove DB with mismatching package ID")]
MismatchingPackageId,
#[error("failed to generate capability for new DB")]
AddCapFailed,
#[error("write statement started with non-existent write keyword")]
NotAWriteKeyword,
#[error("sqlite: NotAReadKeyword")]
#[error("read query started with non-existent read keyword")]
NotAReadKeyword,
#[error("sqlite: Invalid Parameters")]
#[error("parameters blob in read/write was misshapen or contained invalid JSON objects")]
InvalidParameters,
#[error("sqlite: IO error: {error}")]
IOError { error: String },
#[error("sqlite: rusqlite error: {error}")]
RusqliteError { error: String },
#[error("sqlite: input bytes/json/key error: {error}")]
InputError { error: String },
#[error("sqlite got a malformed request that failed to deserialize")]
MalformedRequest,
#[error("rusqlite error: {0}")]
RusqliteError(String),
#[error("IO error: {0}")]
IOError(String),
}

/// The JSON parameters contained in all capabilities issued by `sqlite:distro:sys`.
///
/// # Fields
/// * `kind` - The kind of capability, either [`SqliteCapabilityKind::Read`] or [`SqliteCapabilityKind::Write`]
/// * `db_key` - The database key, a tuple of the [`PackageId`] that created the database and the database name
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SqliteCapabilityParams {
pub kind: SqliteCapabilityKind,
pub db_key: (PackageId, String),
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SqliteCapabilityKind {
Read,
Write,
}

/// Sqlite helper struct for a db.
Expand All @@ -95,7 +189,7 @@ impl Sqlite {
.body(serde_json::to_vec(&SqliteRequest {
package_id: self.package_id.clone(),
db: self.db.clone(),
action: SqliteAction::Read { query },
action: SqliteAction::Query(query),
})?)
.blob_bytes(serde_json::to_vec(&params)?)
.send_and_await_response(self.timeout)?;
Expand All @@ -106,15 +200,11 @@ impl Sqlite {

match response {
SqliteResponse::Read => {
let blob = get_blob().ok_or_else(|| SqliteError::InputError {
error: "sqlite: no blob".to_string(),
})?;
let blob = get_blob().ok_or_else(|| SqliteError::MalformedRequest)?;
let values = serde_json::from_slice::<
Vec<HashMap<String, serde_json::Value>>,
>(&blob.bytes)
.map_err(|e| SqliteError::InputError {
error: format!("sqlite: gave unparsable response: {}", e),
})?;
.map_err(|_| SqliteError::MalformedRequest)?;
Ok(values)
}
SqliteResponse::Err(error) => Err(error.into()),
Expand Down
Loading