Skip to main content

tauri_plugin_sql/
wrapper.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5#[cfg(feature = "sqlite")]
6use std::fs::create_dir_all;
7
8use indexmap::IndexMap;
9use serde_json::Value as JsonValue;
10#[cfg(any(feature = "sqlite", feature = "mysql", feature = "postgres"))]
11use sqlx::{Column, Executor, Pool, Row, migrate::MigrateDatabase};
12#[cfg(any(feature = "sqlite", feature = "mysql", feature = "postgres"))]
13use tauri::Manager;
14use tauri::{AppHandle, Runtime};
15
16#[cfg(feature = "mysql")]
17use sqlx::MySql;
18#[cfg(feature = "postgres")]
19use sqlx::Postgres;
20#[cfg(feature = "sqlite")]
21use sqlx::Sqlite;
22
23use crate::LastInsertId;
24
25/// A connection pool for one of the supported database drivers.
26///
27/// The variant is picked from the scheme of the connection string
28/// (`sqlite:`, `mysql:` or `postgres:`) and only the variants whose Cargo
29/// feature is enabled exist.
30pub enum DbPool {
31    /// A SQLite connection pool. Only available with the `sqlite` Cargo feature.
32    #[cfg(feature = "sqlite")]
33    Sqlite(Pool<Sqlite>),
34    /// A MySQL connection pool. Only available with the `mysql` Cargo feature.
35    #[cfg(feature = "mysql")]
36    MySql(Pool<MySql>),
37    /// A PostgreSQL connection pool. Only available with the `postgres` Cargo feature.
38    #[cfg(feature = "postgres")]
39    Postgres(Pool<Postgres>),
40    /// Placeholder used when none of the `sqlite`, `mysql` and `postgres` Cargo
41    /// features is enabled. Connecting always fails and every other operation is a no-op.
42    #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
43    None,
44}
45
46// public methods
47/* impl DbPool {
48    /// Get the inner Sqlite Pool. Returns None for MySql and Postgres pools.
49    #[cfg(feature = "sqlite")]
50    pub fn sqlite(&self) -> Option<&Pool<Sqlite>> {
51        match self {
52            DbPool::Sqlite(pool) => Some(pool),
53            _ => None,
54        }
55    }
56
57    /// Get the inner MySql Pool. Returns None for Sqlite and Postgres pools.
58    #[cfg(feature = "mysql")]
59    pub fn mysql(&self) -> Option<&Pool<MySql>> {
60        match self {
61            DbPool::MySql(pool) => Some(pool),
62            _ => None,
63        }
64    }
65
66    /// Get the inner Postgres Pool. Returns None for MySql and Sqlite pools.
67    #[cfg(feature = "postgres")]
68    pub fn postgres(&self) -> Option<&Pool<Postgres>> {
69        match self {
70            DbPool::Postgres(pool) => Some(pool),
71            _ => None,
72        }
73    }
74} */
75
76// private methods
77impl DbPool {
78    pub(crate) async fn connect<R: Runtime>(
79        conn_url: &str,
80        _app: &AppHandle<R>,
81    ) -> Result<Self, crate::Error> {
82        match conn_url
83            .split_once(':')
84            .ok_or_else(|| crate::Error::InvalidDbUrl(conn_url.to_string()))?
85            .0
86        {
87            #[cfg(feature = "sqlite")]
88            "sqlite" => {
89                let app_path = _app
90                    .path()
91                    .app_config_dir()
92                    .expect("No App config path was found!");
93
94                create_dir_all(&app_path).expect("Couldn't create app config dir");
95
96                let conn_url = &path_mapper(app_path, conn_url);
97
98                if !Sqlite::database_exists(conn_url).await.unwrap_or(false) {
99                    Sqlite::create_database(conn_url).await?;
100                }
101                Ok(Self::Sqlite(Pool::connect(conn_url).await?))
102            }
103            #[cfg(feature = "mysql")]
104            "mysql" => {
105                if !MySql::database_exists(conn_url).await.unwrap_or(false) {
106                    MySql::create_database(conn_url).await?;
107                }
108                Ok(Self::MySql(Pool::connect(conn_url).await?))
109            }
110            #[cfg(feature = "postgres")]
111            "postgres" => {
112                if !Postgres::database_exists(conn_url).await.unwrap_or(false) {
113                    Postgres::create_database(conn_url).await?;
114                }
115                Ok(Self::Postgres(Pool::connect(conn_url).await?))
116            }
117            #[cfg(not(any(feature = "sqlite", feature = "postgres", feature = "mysql")))]
118            _ => Err(crate::Error::InvalidDbUrl(format!(
119                "{conn_url} - No database driver enabled!"
120            ))),
121            #[cfg(any(feature = "sqlite", feature = "postgres", feature = "mysql"))]
122            _ => Err(crate::Error::InvalidDbUrl(conn_url.to_string())),
123        }
124    }
125
126    pub(crate) async fn migrate(
127        &self,
128        _migrator: &sqlx::migrate::Migrator,
129    ) -> Result<(), crate::Error> {
130        match self {
131            #[cfg(feature = "sqlite")]
132            DbPool::Sqlite(pool) => _migrator.run(pool).await?,
133            #[cfg(feature = "mysql")]
134            DbPool::MySql(pool) => _migrator.run(pool).await?,
135            #[cfg(feature = "postgres")]
136            DbPool::Postgres(pool) => _migrator.run(pool).await?,
137            #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
138            DbPool::None => (),
139        }
140        Ok(())
141    }
142
143    pub(crate) async fn close(&self) {
144        match self {
145            #[cfg(feature = "sqlite")]
146            DbPool::Sqlite(pool) => pool.close().await,
147            #[cfg(feature = "mysql")]
148            DbPool::MySql(pool) => pool.close().await,
149            #[cfg(feature = "postgres")]
150            DbPool::Postgres(pool) => pool.close().await,
151            #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
152            DbPool::None => (),
153        }
154    }
155
156    pub(crate) async fn execute(
157        &self,
158        _query: String,
159        _values: Vec<JsonValue>,
160    ) -> Result<(u64, LastInsertId), crate::Error> {
161        Ok(match self {
162            #[cfg(feature = "sqlite")]
163            DbPool::Sqlite(pool) => {
164                let mut query = sqlx::query(&_query);
165                for value in _values {
166                    if value.is_null() {
167                        query = query.bind(None::<JsonValue>);
168                    } else if value.is_string() {
169                        query = query.bind(value.as_str().unwrap().to_owned())
170                    } else if let Some(number) = value.as_number() {
171                        query = query.bind(number.as_f64().unwrap_or_default())
172                    } else {
173                        query = query.bind(value);
174                    }
175                }
176                let result = pool.execute(query).await?;
177                (
178                    result.rows_affected(),
179                    LastInsertId::Sqlite(result.last_insert_rowid()),
180                )
181            }
182            #[cfg(feature = "mysql")]
183            DbPool::MySql(pool) => {
184                let mut query = sqlx::query(&_query);
185                for value in _values {
186                    if value.is_null() {
187                        query = query.bind(None::<JsonValue>);
188                    } else if value.is_string() {
189                        query = query.bind(value.as_str().unwrap().to_owned())
190                    } else if let Some(number) = value.as_number() {
191                        query = query.bind(number.as_f64().unwrap_or_default())
192                    } else {
193                        query = query.bind(value);
194                    }
195                }
196                let result = pool.execute(query).await?;
197                (
198                    result.rows_affected(),
199                    LastInsertId::MySql(result.last_insert_id()),
200                )
201            }
202            #[cfg(feature = "postgres")]
203            DbPool::Postgres(pool) => {
204                let mut query = sqlx::query(&_query);
205                for value in _values {
206                    if value.is_null() {
207                        query = query.bind(None::<JsonValue>);
208                    } else if value.is_string() {
209                        query = query.bind(value.as_str().unwrap().to_owned())
210                    } else if let Some(number) = value.as_number() {
211                        query = query.bind(number.as_f64().unwrap_or_default())
212                    } else {
213                        query = query.bind(value);
214                    }
215                }
216                let result = pool.execute(query).await?;
217                (result.rows_affected(), LastInsertId::Postgres(()))
218            }
219            #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
220            DbPool::None => (0, LastInsertId::None),
221        })
222    }
223
224    pub(crate) async fn select(
225        &self,
226        _query: String,
227        _values: Vec<JsonValue>,
228    ) -> Result<Vec<IndexMap<String, JsonValue>>, crate::Error> {
229        Ok(match self {
230            #[cfg(feature = "sqlite")]
231            DbPool::Sqlite(pool) => {
232                let mut query = sqlx::query(&_query);
233                for value in _values {
234                    if value.is_null() {
235                        query = query.bind(None::<JsonValue>);
236                    } else if value.is_string() {
237                        query = query.bind(value.as_str().unwrap().to_owned())
238                    } else if let Some(number) = value.as_number() {
239                        query = query.bind(number.as_f64().unwrap_or_default())
240                    } else {
241                        query = query.bind(value);
242                    }
243                }
244                let rows = pool.fetch_all(query).await?;
245                let mut values = Vec::new();
246                for row in rows {
247                    let mut value = IndexMap::default();
248                    for (i, column) in row.columns().iter().enumerate() {
249                        let v = row.try_get_raw(i)?;
250
251                        let v = crate::decode::sqlite::to_json(v)?;
252
253                        value.insert(column.name().to_string(), v);
254                    }
255
256                    values.push(value);
257                }
258                values
259            }
260            #[cfg(feature = "mysql")]
261            DbPool::MySql(pool) => {
262                let mut query = sqlx::query(&_query);
263                for value in _values {
264                    if value.is_null() {
265                        query = query.bind(None::<JsonValue>);
266                    } else if value.is_string() {
267                        query = query.bind(value.as_str().unwrap().to_owned())
268                    } else if let Some(number) = value.as_number() {
269                        query = query.bind(number.as_f64().unwrap_or_default())
270                    } else {
271                        query = query.bind(value);
272                    }
273                }
274                let rows = pool.fetch_all(query).await?;
275                let mut values = Vec::new();
276                for row in rows {
277                    let mut value = IndexMap::default();
278                    for (i, column) in row.columns().iter().enumerate() {
279                        let v = row.try_get_raw(i)?;
280
281                        let v = crate::decode::mysql::to_json(v)?;
282
283                        value.insert(column.name().to_string(), v);
284                    }
285
286                    values.push(value);
287                }
288                values
289            }
290            #[cfg(feature = "postgres")]
291            DbPool::Postgres(pool) => {
292                let mut query = sqlx::query(&_query);
293                for value in _values {
294                    if value.is_null() {
295                        query = query.bind(None::<JsonValue>);
296                    } else if value.is_string() {
297                        query = query.bind(value.as_str().unwrap().to_owned())
298                    } else if let Some(number) = value.as_number() {
299                        query = query.bind(number.as_f64().unwrap_or_default())
300                    } else {
301                        query = query.bind(value);
302                    }
303                }
304                let rows = pool.fetch_all(query).await?;
305                let mut values = Vec::new();
306                for row in rows {
307                    let mut value = IndexMap::default();
308                    for (i, column) in row.columns().iter().enumerate() {
309                        let v = row.try_get_raw(i)?;
310
311                        let v = crate::decode::postgres::to_json(v)?;
312
313                        value.insert(column.name().to_string(), v);
314                    }
315
316                    values.push(value);
317                }
318                values
319            }
320            #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
321            DbPool::None => Vec::new(),
322        })
323    }
324}
325
326#[cfg(feature = "sqlite")]
327/// Maps the user supplied DB connection string to a connection string
328/// with a fully qualified file path to the App's designed "app_path"
329fn path_mapper(mut app_path: std::path::PathBuf, connection_string: &str) -> String {
330    app_path.push(
331        connection_string
332            .split_once(':')
333            .expect("Couldn't parse the connection string for DB!")
334            .1,
335    );
336
337    format!(
338        "sqlite:{}",
339        app_path
340            .to_str()
341            .expect("Problem creating fully qualified path to Database file!")
342    )
343}