orbis_plugin_api/sdk/
db.rs

1//! Database access for plugins.
2//!
3//! Provides type-safe database querying and mutation.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use orbis_plugin_api::sdk::db;
9//!
10//! // Query with parameters
11//! let users = db::query::<User>(
12//!     "SELECT * FROM users WHERE active = ? LIMIT ?",
13//!     &[&true, &10]
14//! )?;
15//!
16//! // Execute a mutation
17//! let rows_affected = db::execute(
18//!     "UPDATE users SET last_login = ? WHERE id = ?",
19//!     &[&now, &user_id]
20//! )?;
21//! ```
22
23use super::error::{Error, Result};
24use serde::{de::DeserializeOwned, Deserialize, Serialize};
25
26/// A value that can be used as a database parameter
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(untagged)]
29pub enum DbValue {
30    /// Null value
31    Null,
32    /// Boolean value
33    Bool(bool),
34    /// Integer value
35    Int(i64),
36    /// Float value
37    Float(f64),
38    /// String value
39    String(String),
40    /// Binary data
41    Bytes(Vec<u8>),
42    /// JSON value
43    Json(serde_json::Value),
44}
45
46impl From<()> for DbValue {
47    fn from(_: ()) -> Self {
48        Self::Null
49    }
50}
51
52impl From<bool> for DbValue {
53    fn from(v: bool) -> Self {
54        Self::Bool(v)
55    }
56}
57
58impl From<i32> for DbValue {
59    fn from(v: i32) -> Self {
60        Self::Int(i64::from(v))
61    }
62}
63
64impl From<i64> for DbValue {
65    fn from(v: i64) -> Self {
66        Self::Int(v)
67    }
68}
69
70impl From<f64> for DbValue {
71    fn from(v: f64) -> Self {
72        Self::Float(v)
73    }
74}
75
76impl From<&str> for DbValue {
77    fn from(v: &str) -> Self {
78        Self::String(v.to_string())
79    }
80}
81
82impl From<String> for DbValue {
83    fn from(v: String) -> Self {
84        Self::String(v)
85    }
86}
87
88impl From<Vec<u8>> for DbValue {
89    fn from(v: Vec<u8>) -> Self {
90        Self::Bytes(v)
91    }
92}
93
94impl From<serde_json::Value> for DbValue {
95    fn from(v: serde_json::Value) -> Self {
96        Self::Json(v)
97    }
98}
99
100/// Trait for types that can be converted to database parameters
101pub trait ToDbParams {
102    fn to_db_params(&self) -> Vec<DbValue>;
103}
104
105impl ToDbParams for () {
106    fn to_db_params(&self) -> Vec<DbValue> {
107        vec![]
108    }
109}
110
111impl<T: Into<DbValue> + Clone> ToDbParams for &[T] {
112    fn to_db_params(&self) -> Vec<DbValue> {
113        self.iter().map(|v| v.clone().into()).collect()
114    }
115}
116
117impl<T: Into<DbValue> + Clone, const N: usize> ToDbParams for [T; N] {
118    fn to_db_params(&self) -> Vec<DbValue> {
119        self.iter().map(|v| v.clone().into()).collect()
120    }
121}
122
123impl ToDbParams for Vec<DbValue> {
124    fn to_db_params(&self) -> Vec<DbValue> {
125        self.clone()
126    }
127}
128
129/// A row from a database query result
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct DbRow {
132    /// Column values by name
133    #[serde(flatten)]
134    pub columns: std::collections::HashMap<String, serde_json::Value>,
135}
136
137impl DbRow {
138    /// Get a column value by name
139    #[inline]
140    pub fn get(&self, name: &str) -> Option<&serde_json::Value> {
141        self.columns.get(name)
142    }
143
144    /// Get a column value as a specific type
145    pub fn get_as<T: DeserializeOwned>(&self, name: &str) -> Result<Option<T>> {
146        match self.columns.get(name) {
147            Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(Error::from),
148            None => Ok(None),
149        }
150    }
151
152    /// Get a required column value
153    pub fn get_required<T: DeserializeOwned>(&self, name: &str) -> Result<T> {
154        self.get_as(name)?
155            .ok_or_else(|| Error::database(format!("Missing column: {}", name)))
156    }
157
158    /// Try to convert the row to a typed struct
159    pub fn into_typed<T: DeserializeOwned>(self) -> Result<T> {
160        let value = serde_json::to_value(self.columns)?;
161        serde_json::from_value(value).map_err(Error::from)
162    }
163}
164
165/// Database query request
166#[derive(Debug, Serialize)]
167#[allow(dead_code)]
168struct QueryRequest<'a> {
169    sql: &'a str,
170    params: Vec<DbValue>,
171}
172
173/// Database query response
174#[derive(Debug, Deserialize)]
175#[allow(dead_code)]
176struct QueryResponse {
177    rows: Vec<DbRow>,
178    #[serde(default)]
179    error: Option<String>,
180}
181
182/// Database execute response
183#[derive(Debug, Deserialize)]
184#[allow(dead_code)]
185struct ExecuteResponse {
186    rows_affected: i64,
187    #[serde(default)]
188    last_insert_id: Option<i64>,
189    #[serde(default)]
190    error: Option<String>,
191}
192
193/// Execute a database query and return typed results.
194///
195/// # Example
196///
197/// ```rust,ignore
198/// #[derive(Deserialize)]
199/// struct User {
200///     id: i64,
201///     name: String,
202///     email: String,
203/// }
204///
205/// let users = db::query::<User>(
206///     "SELECT id, name, email FROM users WHERE active = ?",
207///     &[true]
208/// )?;
209/// ```
210#[cfg(target_arch = "wasm32")]
211pub fn query<T: DeserializeOwned>(sql: &str, params: impl ToDbParams) -> Result<Vec<T>> {
212    let request = QueryRequest {
213        sql,
214        params: params.to_db_params(),
215    };
216
217    let request_json = serde_json::to_vec(&request)?;
218
219    let result_ptr = unsafe {
220        super::ffi::db_query(
221            sql.as_ptr() as i32,
222            sql.len() as i32,
223            request_json.as_ptr() as i32,
224            request_json.len() as i32,
225        )
226    };
227
228    if result_ptr == 0 {
229        return Err(Error::database("Database query failed"));
230    }
231
232    let result_bytes = unsafe { super::ffi::read_length_prefixed(result_ptr) };
233    let response: QueryResponse = serde_json::from_slice(&result_bytes)?;
234
235    if let Some(err) = response.error {
236        return Err(Error::database(err));
237    }
238
239    response
240        .rows
241        .into_iter()
242        .map(|row| row.into_typed())
243        .collect()
244}
245
246/// Execute a database query (non-WASM stub)
247#[cfg(not(target_arch = "wasm32"))]
248pub fn query<T: DeserializeOwned>(_sql: &str, _params: impl ToDbParams) -> Result<Vec<T>> {
249    Ok(vec![])
250}
251
252/// Execute a query and return raw rows (for dynamic queries)
253#[cfg(target_arch = "wasm32")]
254pub fn query_raw(sql: &str, params: impl ToDbParams) -> Result<Vec<DbRow>> {
255    let request = QueryRequest {
256        sql,
257        params: params.to_db_params(),
258    };
259
260    let request_json = serde_json::to_vec(&request)?;
261
262    let result_ptr = unsafe {
263        super::ffi::db_query(
264            sql.as_ptr() as i32,
265            sql.len() as i32,
266            request_json.as_ptr() as i32,
267            request_json.len() as i32,
268        )
269    };
270
271    if result_ptr == 0 {
272        return Err(Error::database("Database query failed"));
273    }
274
275    let result_bytes = unsafe { super::ffi::read_length_prefixed(result_ptr) };
276    let response: QueryResponse = serde_json::from_slice(&result_bytes)?;
277
278    if let Some(err) = response.error {
279        return Err(Error::database(err));
280    }
281
282    Ok(response.rows)
283}
284
285/// Execute a query and return raw rows (non-WASM stub)
286#[cfg(not(target_arch = "wasm32"))]
287pub fn query_raw(_sql: &str, _params: impl ToDbParams) -> Result<Vec<DbRow>> {
288    Ok(vec![])
289}
290
291/// Query for a single row
292pub fn query_one<T: DeserializeOwned>(sql: &str, params: impl ToDbParams) -> Result<Option<T>> {
293    let results = query::<T>(sql, params)?;
294    Ok(results.into_iter().next())
295}
296
297/// Query for a single required row
298pub fn query_one_required<T: DeserializeOwned>(sql: &str, params: impl ToDbParams) -> Result<T> {
299    query_one::<T>(sql, params)?.ok_or_else(|| Error::not_found("No rows found"))
300}
301
302/// Query for a single scalar value
303pub fn query_scalar<T: DeserializeOwned>(sql: &str, params: impl ToDbParams) -> Result<Option<T>> {
304    let rows = query_raw(sql, params)?;
305    if let Some(row) = rows.into_iter().next() {
306        if let Some((_, value)) = row.columns.into_iter().next() {
307            return Ok(Some(serde_json::from_value(value)?));
308        }
309    }
310    Ok(None)
311}
312
313/// Execute a database mutation (INSERT, UPDATE, DELETE).
314///
315/// Returns the number of affected rows.
316///
317/// # Example
318///
319/// ```rust,ignore
320/// let rows = db::execute(
321///     "UPDATE users SET last_login = NOW() WHERE id = ?",
322///     &[&user_id]
323/// )?;
324/// ```
325#[cfg(target_arch = "wasm32")]
326pub fn execute(sql: &str, params: impl ToDbParams) -> Result<i64> {
327    let params_json = serde_json::to_vec(&params.to_db_params())?;
328
329    let result = unsafe {
330        super::ffi::db_execute(
331            sql.as_ptr() as i32,
332            sql.len() as i32,
333            params_json.as_ptr() as i32,
334            params_json.len() as i32,
335        )
336    };
337
338    if result < 0 {
339        return Err(Error::database("Database execute failed"));
340    }
341
342    Ok(i64::from(result))
343}
344
345/// Execute a database mutation (non-WASM stub)
346#[cfg(not(target_arch = "wasm32"))]
347pub fn execute(_sql: &str, _params: impl ToDbParams) -> Result<i64> {
348    Ok(0)
349}
350
351/// Insert a row and return the last insert ID
352#[cfg(target_arch = "wasm32")]
353pub fn insert_returning_id(sql: &str, params: impl ToDbParams) -> Result<i64> {
354    // For PostgreSQL, append RETURNING id
355    let returning_sql = if sql.to_uppercase().contains("RETURNING") {
356        sql.to_string()
357    } else {
358        format!("{} RETURNING id", sql)
359    };
360
361    query_scalar::<i64>(&returning_sql, params)?
362        .ok_or_else(|| Error::database("Insert did not return an ID"))
363}
364
365/// Insert a row and return the last insert ID (non-WASM stub)
366#[cfg(not(target_arch = "wasm32"))]
367pub fn insert_returning_id(_sql: &str, _params: impl ToDbParams) -> Result<i64> {
368    Ok(0)
369}
370
371/// Transaction builder for multiple operations
372pub struct Transaction {
373    operations: Vec<(String, Vec<DbValue>)>,
374}
375
376impl Transaction {
377    /// Create a new transaction builder
378    #[must_use]
379    pub const fn new() -> Self {
380        Self {
381            operations: Vec::new(),
382        }
383    }
384
385    /// Add an operation to the transaction
386    pub fn add(&mut self, sql: impl Into<String>, params: impl ToDbParams) -> &mut Self {
387        self.operations.push((sql.into(), params.to_db_params()));
388        self
389    }
390
391    /// Execute all operations in a transaction
392    ///
393    /// Note: Actual transaction support depends on host implementation
394    pub fn commit(self) -> Result<()> {
395        for (sql, params) in self.operations {
396            execute(&sql, params.as_slice())?;
397        }
398        Ok(())
399    }
400}
401
402impl Default for Transaction {
403    fn default() -> Self {
404        Self::new()
405    }
406}
407
408/// Start building a transaction
409#[inline]
410#[must_use]
411pub const fn transaction() -> Transaction {
412    Transaction::new()
413}