Skip to content

Commit

Permalink
Merge pull request #130 from kinode-dao/dr/sqlite-overhaul
Browse files Browse the repository at this point in the history
sqlite: new types from runtime
  • Loading branch information
dr-frmr authored Dec 22, 2024
2 parents bf89fd6 + 7444568 commit c5dee62
Showing 1 changed file with 129 additions and 39 deletions.
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

0 comments on commit c5dee62

Please sign in to comment.