Skip to main content

tauri_plugin_sql/
lib.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//! Interface with SQL databases through [sqlx](https://github.com/launchbadge/sqlx).
6//!
7//! ## Cargo features
8//!
9//! No database driver is enabled by default. Enable at least one driver;
10//! multiple drivers may be enabled together.
11//!
12//! - **sqlite**: Adds support for SQLite.
13//! - **mysql**: Adds support for MySQL.
14//! - **postgres**: Adds support for PostgreSQL.
15
16#![doc(
17    html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
18    html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
19)]
20
21mod commands;
22mod decode;
23mod error;
24mod wrapper;
25
26pub use error::Error;
27pub use wrapper::DbPool;
28
29use futures_core::future::BoxFuture;
30use serde::{Deserialize, Serialize};
31use sqlx::{
32    error::BoxDynError,
33    migrate::{Migration as SqlxMigration, MigrationSource, MigrationType, Migrator},
34};
35use tauri::{
36    plugin::{Builder as PluginBuilder, TauriPlugin},
37    Manager, RunEvent, Runtime,
38};
39use tokio::sync::{Mutex, RwLock};
40
41use std::collections::HashMap;
42
43#[derive(Default)]
44pub struct DbInstances(pub RwLock<HashMap<String, DbPool>>);
45
46#[derive(Serialize)]
47#[serde(untagged)]
48pub(crate) enum LastInsertId {
49    #[cfg(feature = "sqlite")]
50    Sqlite(i64),
51    #[cfg(feature = "mysql")]
52    MySql(u64),
53    #[cfg(feature = "postgres")]
54    Postgres(()),
55    #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
56    None,
57}
58
59struct Migrations(Mutex<HashMap<String, MigrationList>>);
60
61#[derive(Default, Clone, Deserialize)]
62pub struct PluginConfig {
63    #[serde(default)]
64    preload: Vec<String>,
65}
66
67#[derive(Debug)]
68pub enum MigrationKind {
69    Up,
70    Down,
71}
72
73impl From<MigrationKind> for MigrationType {
74    fn from(kind: MigrationKind) -> Self {
75        match kind {
76            MigrationKind::Up => Self::ReversibleUp,
77            MigrationKind::Down => Self::ReversibleDown,
78        }
79    }
80}
81
82/// A migration definition.
83#[derive(Debug)]
84pub struct Migration {
85    pub version: i64,
86    pub description: &'static str,
87    pub sql: &'static str,
88    pub kind: MigrationKind,
89}
90
91#[derive(Debug)]
92struct MigrationList(Vec<Migration>);
93
94impl MigrationSource<'static> for MigrationList {
95    fn resolve(self) -> BoxFuture<'static, std::result::Result<Vec<SqlxMigration>, BoxDynError>> {
96        Box::pin(async move {
97            let mut migrations = Vec::new();
98            for migration in self.0 {
99                if matches!(migration.kind, MigrationKind::Up) {
100                    migrations.push(SqlxMigration::new(
101                        migration.version,
102                        migration.description.into(),
103                        migration.kind.into(),
104                        migration.sql.into(),
105                        false,
106                    ));
107                }
108            }
109            Ok(migrations)
110        })
111    }
112}
113
114/// Allows blocking on async code without creating a nested runtime.
115fn run_async_command<F: std::future::Future>(cmd: F) -> F::Output {
116    if tokio::runtime::Handle::try_current().is_ok() {
117        tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(cmd))
118    } else {
119        tauri::async_runtime::block_on(cmd)
120    }
121}
122
123/// Tauri SQL plugin builder.
124#[derive(Default)]
125pub struct Builder {
126    migrations: Option<HashMap<String, MigrationList>>,
127}
128
129impl Builder {
130    pub fn new() -> Self {
131        #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
132        eprintln!("No sql driver enabled. Please set at least one of the \"sqlite\", \"mysql\", \"postgres\" feature flags.");
133
134        Self::default()
135    }
136
137    /// Add migrations to a database.
138    #[must_use]
139    pub fn add_migrations(mut self, db_url: &str, migrations: Vec<Migration>) -> Self {
140        self.migrations
141            .get_or_insert(Default::default())
142            .insert(db_url.to_string(), MigrationList(migrations));
143        self
144    }
145
146    pub fn build<R: Runtime>(mut self) -> TauriPlugin<R, Option<PluginConfig>> {
147        PluginBuilder::<R, Option<PluginConfig>>::new("sql")
148            .invoke_handler(tauri::generate_handler![
149                commands::load,
150                commands::execute,
151                commands::select,
152                commands::close
153            ])
154            .setup(|app, api| {
155                let config = api.config().clone().unwrap_or_default();
156
157                run_async_command(async move {
158                    let instances = DbInstances::default();
159                    let mut lock = instances.0.write().await;
160
161                    for db in config.preload {
162                        let pool = DbPool::connect(&db, app).await?;
163
164                        if let Some(migrations) =
165                            self.migrations.as_mut().and_then(|mm| mm.remove(&db))
166                        {
167                            let migrator = Migrator::new(migrations).await?;
168                            pool.migrate(&migrator).await?;
169                        }
170
171                        lock.insert(db, pool);
172                    }
173                    drop(lock);
174
175                    app.manage(instances);
176                    app.manage(Migrations(Mutex::new(
177                        self.migrations.take().unwrap_or_default(),
178                    )));
179
180                    Ok(())
181                })
182            })
183            .on_event(|app, event| {
184                if let RunEvent::Exit = event {
185                    run_async_command(async move {
186                        let instances = &*app.state::<DbInstances>();
187                        let instances = instances.0.read().await;
188                        for value in instances.values() {
189                            value.close().await;
190                        }
191                    });
192                }
193            })
194            .build()
195    }
196}