picodata_plugin/sql/
mod.rs

1//! Picodata SQL API.
2
3use crate::internal::ffi;
4use crate::sql::types::SqlValue;
5use abi_stable::derive_macro_reexports::RResult;
6use abi_stable::std_types::{ROk, RVec};
7use serde::de::DeserializeOwned;
8use serde::Deserialize;
9use std::collections::HashMap;
10use tarantool::error::{BoxError, IntoBoxError, TarantoolErrorCode};
11use tarantool::tuple::Tuple;
12
13pub mod types;
14
15/// Execute SQL Query.
16///
17/// # Arguments
18///
19/// * `query`: query string
20/// * `params`: query params - list of SQL values
21///
22/// # Examples
23///
24/// ```no_run
25/// # use picodata_plugin::sql::query_raw;
26/// # use picodata_plugin::sql::types::SqlValue;
27///  query_raw(
28///     "INSERT INTO book (id, name) VALUES (?, ?)",
29///     vec![
30///         SqlValue::unsigned(1),
31///         SqlValue::string("Ruslan and Ludmila"),
32///     ],
33///  )
34///  .unwrap();
35/// ```
36pub fn query_raw(query: &str, params: Vec<SqlValue>) -> Result<Tuple, BoxError> {
37    let query_len = query.len();
38    let query_ptr = query.as_ptr();
39
40    // `pico_ffi_sql_query` is defined in `src/plugin/ffi.rs`
41    let res = unsafe { ffi::pico_ffi_sql_query(query_ptr, query_len, RVec::from(params)) };
42    let ptr = match res {
43        ROk(v) => v,
44        RResult::RErr(_) => return Err(BoxError::last()),
45    };
46
47    // SAFETY: `pico_ffi_sql_query` doesn't unref the returned tuple, so it
48    // always has at least 1 ref. If we add another ref here, then that would be
49    // a memory leak
50    let tuple = unsafe { Tuple::try_from_ptr_dont_ref(ptr) };
51    let tuple = tuple.expect("always non null");
52
53    Ok(tuple)
54}
55
56pub struct Query<'a> {
57    query: &'a str,
58    params: Vec<SqlValue>,
59}
60
61impl Query<'_> {
62    /// Bind a value for use with this SQL query.
63    #[inline(always)]
64    pub fn bind<T: Into<SqlValue>>(mut self, value: T) -> Self {
65        self.params.push(value.into());
66        self
67    }
68
69    /// Execute the query and return the total number of rows affected.
70    ///
71    /// # Examples
72    ///
73    /// ```no_run
74    /// # use picodata_plugin::sql::query;
75    ///  let inserted: u64 = query("INSERT INTO book (id, name) VALUES (?, ?)")
76    ///     .bind(1)
77    ///     .bind("Ruslan and Ludmila")
78    ///     .execute()
79    ///     .unwrap();
80    /// assert_eq!(inserted, 1);
81    /// ```
82    pub fn execute(self) -> Result<u64, BoxError> {
83        let tuple = query_raw(self.query, self.params)?;
84        #[derive(Deserialize)]
85        struct Output {
86            row_count: u64,
87        }
88
89        let result = tuple
90            .decode::<Vec<Output>>()
91            .map_err(|tt| tt.into_box_error())?;
92
93        let result = result.first().ok_or_else(|| {
94            BoxError::new(
95                TarantoolErrorCode::InvalidMsgpack,
96                "sql result should contains at least one row",
97            )
98        })?;
99
100        Ok(result.row_count)
101    }
102
103    /// Execute the query and return list of selected values.
104    ///
105    /// # Examples
106    ///
107    /// ```no_run
108    /// # use picodata_plugin::sql::query;
109    /// # use serde::Deserialize;
110    /// #[derive(Deserialize, Debug, PartialEq)]
111    /// struct Book {
112    ///     id: u64,
113    ///     name: String,
114    /// }
115    /// let books = query("SELECT * from book").fetch::<Book>().unwrap();
116    /// assert_eq!(&books, &[Book { id: 1, name: "Ruslan and Ludmila".to_string()}]);
117    /// ```
118    pub fn fetch<T: DeserializeOwned>(self) -> Result<Vec<T>, BoxError> {
119        let tuple = query_raw(self.query, self.params)?;
120
121        let mut res = tuple
122            .decode::<Vec<HashMap<String, rmpv::Value>>>()
123            .map_err(|tt| tt.into_box_error())?;
124
125        let Some(mut map) = res.pop() else {
126            return Err(BoxError::new(
127                TarantoolErrorCode::InvalidMsgpack,
128                "fetch result array should contains at least one element",
129            ));
130        };
131        let Some(rows) = map.remove("rows") else {
132            return Err(BoxError::new(
133                TarantoolErrorCode::InvalidMsgpack,
134                "fetch result map should contains `rows` key",
135            ));
136        };
137
138        let data: Vec<T> = rmpv::ext::from_value(rows)
139            .map_err(|e| BoxError::new(TarantoolErrorCode::InvalidMsgpack, e.to_string()))?;
140
141        Ok(data)
142    }
143}
144
145/// Execute a single SQL query as a prepared statement (transparently cached).
146pub fn query(query: &str) -> Query<'_> {
147    Query {
148        query,
149        params: vec![],
150    }
151}