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    match unsafe { ffi::pico_ffi_sql_query(query_ptr, query_len, RVec::from(params)) } {
41        ROk(ptr) => Ok(Tuple::try_from_ptr(ptr).expect("should not be null")),
42        RResult::RErr(_) => Err(BoxError::last()),
43    }
44}
45
46pub struct Query<'a> {
47    query: &'a str,
48    params: Vec<SqlValue>,
49}
50
51impl Query<'_> {
52    /// Bind a value for use with this SQL query.
53    #[inline(always)]
54    pub fn bind<T: Into<SqlValue>>(mut self, value: T) -> Self {
55        self.params.push(value.into());
56        self
57    }
58
59    /// Execute the query and return the total number of rows affected.
60    ///
61    /// # Examples
62    ///
63    /// ```no_run
64    /// # use picodata_plugin::sql::query;
65    ///  let inserted: u64 = query("INSERT INTO book (id, name) VALUES (?, ?)")
66    ///     .bind(1)
67    ///     .bind("Ruslan and Ludmila")
68    ///     .execute()
69    ///     .unwrap();
70    /// assert_eq!(inserted, 1);
71    /// ```
72    pub fn execute(self) -> Result<u64, BoxError> {
73        let tuple = query_raw(self.query, self.params)?;
74        #[derive(Deserialize)]
75        struct Output {
76            row_count: u64,
77        }
78
79        let result = tuple
80            .decode::<Vec<Output>>()
81            .map_err(|tt| tt.into_box_error())?;
82
83        let result = result.first().ok_or_else(|| {
84            BoxError::new(
85                TarantoolErrorCode::InvalidMsgpack,
86                "sql result should contains at least one row",
87            )
88        })?;
89
90        Ok(result.row_count)
91    }
92
93    /// Execute the query and return list of selected values.
94    ///
95    /// # Examples
96    ///
97    /// ```no_run
98    /// # use picodata_plugin::sql::query;
99    /// # use serde::Deserialize;
100    /// #[derive(Deserialize, Debug, PartialEq)]
101    /// struct Book {
102    ///     id: u64,
103    ///     name: String,
104    /// }
105    /// let books = query("SELECT * from book").fetch::<Book>().unwrap();
106    /// assert_eq!(&books, &[Book { id: 1, name: "Ruslan and Ludmila".to_string()}]);
107    /// ```
108    pub fn fetch<T: DeserializeOwned>(self) -> Result<Vec<T>, BoxError> {
109        let tuple = query_raw(self.query, self.params)?;
110
111        let mut res = tuple
112            .decode::<Vec<HashMap<String, rmpv::Value>>>()
113            .map_err(|tt| tt.into_box_error())?;
114
115        let Some(mut map) = res.pop() else {
116            return Err(BoxError::new(
117                TarantoolErrorCode::InvalidMsgpack,
118                "fetch result array should contains at least one element",
119            ));
120        };
121        let Some(rows) = map.remove("rows") else {
122            return Err(BoxError::new(
123                TarantoolErrorCode::InvalidMsgpack,
124                "fetch result map should contains `rows` key",
125            ));
126        };
127
128        let data: Vec<T> = rmpv::ext::from_value(rows)
129            .map_err(|e| BoxError::new(TarantoolErrorCode::InvalidMsgpack, e.to_string()))?;
130
131        Ok(data)
132    }
133}
134
135/// Execute a single SQL query as a prepared statement (transparently cached).
136pub fn query(query: &str) -> Query<'_> {
137    Query {
138        query,
139        params: vec![],
140    }
141}