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    Manager, RunEvent, Runtime,
37    plugin::{Builder as PluginBuilder, TauriPlugin},
38};
39use tokio::sync::{Mutex, RwLock};
40
41use std::collections::HashMap;
42
43/// The connection pools the plugin currently holds, keyed by the connection
44/// string the database was loaded with.
45///
46/// It is managed as Tauri state, so Rust code can reach the pools with
47/// [`tauri::Manager::state`] and run its own queries.
48#[derive(Default)]
49pub struct DbInstances(pub RwLock<HashMap<String, DbPool>>);
50
51#[derive(Serialize)]
52#[serde(untagged)]
53pub(crate) enum LastInsertId {
54    #[cfg(feature = "sqlite")]
55    Sqlite(i64),
56    #[cfg(feature = "mysql")]
57    MySql(u64),
58    #[cfg(feature = "postgres")]
59    Postgres(()),
60    #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
61    None,
62}
63
64struct Migrations(Mutex<HashMap<String, MigrationList>>);
65
66/// The `plugins > sql` section of the Tauri configuration file.
67#[derive(Default, Clone, Deserialize)]
68pub struct PluginConfig {
69    /// Connection strings of the databases to connect to when the application
70    /// starts. Empty by default.
71    #[serde(default)]
72    preload: Vec<String>,
73}
74
75/// The direction of a [`Migration`].
76#[derive(Debug)]
77pub enum MigrationKind {
78    /// Moves the schema forward. Only migrations of this kind are executed by the plugin.
79    Up,
80    /// Reverts an [`Up`](Self::Up) migration. Migrations of this kind are currently
81    /// never executed by the plugin.
82    Down,
83}
84
85impl From<MigrationKind> for MigrationType {
86    fn from(kind: MigrationKind) -> Self {
87        match kind {
88            MigrationKind::Up => Self::ReversibleUp,
89            MigrationKind::Down => Self::ReversibleDown,
90        }
91    }
92}
93
94/// A migration definition.
95///
96/// Migrations are attached to a database with [`Builder::add_migrations`] and run
97/// the first time that database is connected to - on startup for the databases
98/// listed in the `preload` configuration, otherwise when the frontend loads it.
99/// Only [`MigrationKind::Up`] migrations are executed, in ascending
100/// [`version`](Self::version) order, and sqlx keeps track of the versions that
101/// already ran so each one is applied at most once per database.
102#[derive(Debug)]
103pub struct Migration {
104    /// The version of this migration. Determines the order in which migrations
105    /// run and identifies the migration in the database.
106    pub version: i64,
107    /// A human readable description of what the migration does.
108    pub description: &'static str,
109    /// The SQL executed when the migration runs.
110    pub sql: &'static str,
111    /// Whether this migration moves the schema forward or reverts it.
112    pub kind: MigrationKind,
113}
114
115#[derive(Debug)]
116struct MigrationList(Vec<Migration>);
117
118impl MigrationSource<'static> for MigrationList {
119    fn resolve(self) -> BoxFuture<'static, std::result::Result<Vec<SqlxMigration>, BoxDynError>> {
120        Box::pin(async move {
121            let mut migrations = Vec::new();
122            for migration in self.0 {
123                if matches!(migration.kind, MigrationKind::Up) {
124                    migrations.push(SqlxMigration::new(
125                        migration.version,
126                        migration.description.into(),
127                        migration.kind.into(),
128                        migration.sql.into(),
129                        false,
130                    ));
131                }
132            }
133            Ok(migrations)
134        })
135    }
136}
137
138/// Allows blocking on async code without creating a nested runtime.
139fn run_async_command<F: std::future::Future>(cmd: F) -> F::Output {
140    if tokio::runtime::Handle::try_current().is_ok() {
141        tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(cmd))
142    } else {
143        tauri::async_runtime::block_on(cmd)
144    }
145}
146
147/// Tauri SQL plugin builder.
148#[derive(Default)]
149pub struct Builder {
150    migrations: Option<HashMap<String, MigrationList>>,
151}
152
153impl Builder {
154    /// Creates a new builder with no migrations registered.
155    ///
156    /// Prints a message to stderr when none of the `sqlite`, `mysql` and
157    /// `postgres` Cargo features is enabled, since no database can be
158    /// connected to in that case.
159    pub fn new() -> Self {
160        #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
161        eprintln!(
162            "No sql driver enabled. Please set at least one of the \"sqlite\", \"mysql\", \"postgres\" feature flags."
163        );
164
165        Self::default()
166    }
167
168    /// Add migrations to a database.
169    #[must_use]
170    pub fn add_migrations(mut self, db_url: &str, migrations: Vec<Migration>) -> Self {
171        self.migrations
172            .get_or_insert(Default::default())
173            .insert(db_url.to_string(), MigrationList(migrations));
174        self
175    }
176
177    /// Builds the SQL plugin.
178    ///
179    /// On setup the plugin connects to every database listed in the `preload`
180    /// array of its [configuration](PluginConfig), running the migrations
181    /// registered for them, and it closes all connection pools when the
182    /// application exits.
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// use tauri_plugin_sql::{Builder, Migration, MigrationKind};
188    ///
189    /// fn sql_plugin<R: tauri::Runtime>(
190    /// ) -> tauri::plugin::TauriPlugin<R, Option<tauri_plugin_sql::PluginConfig>> {
191    ///     Builder::new()
192    ///         .add_migrations(
193    ///             "sqlite:mydatabase.db",
194    ///             vec![Migration {
195    ///                 version: 1,
196    ///                 description: "create todos table",
197    ///                 sql: "CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT);",
198    ///                 kind: MigrationKind::Up,
199    ///             }],
200    ///         )
201    ///         .build()
202    /// }
203    /// ```
204    pub fn build<R: Runtime>(mut self) -> TauriPlugin<R, Option<PluginConfig>> {
205        PluginBuilder::<R, Option<PluginConfig>>::new("sql")
206            .invoke_handler(tauri::generate_handler![
207                commands::load,
208                commands::execute,
209                commands::select,
210                commands::close
211            ])
212            .setup(|app, api| {
213                let config = api.config().clone().unwrap_or_default();
214
215                run_async_command(async move {
216                    let instances = DbInstances::default();
217                    let mut lock = instances.0.write().await;
218
219                    for db in config.preload {
220                        let pool = DbPool::connect(&db, app).await?;
221
222                        if let Some(migrations) =
223                            self.migrations.as_mut().and_then(|mm| mm.remove(&db))
224                        {
225                            let migrator = Migrator::new(migrations).await?;
226                            pool.migrate(&migrator).await?;
227                        }
228
229                        lock.insert(db, pool);
230                    }
231                    drop(lock);
232
233                    app.manage(instances);
234                    app.manage(Migrations(Mutex::new(
235                        self.migrations.take().unwrap_or_default(),
236                    )));
237
238                    Ok(())
239                })
240            })
241            .on_event(|app, event| {
242                if let RunEvent::Exit = event {
243                    run_async_command(async move {
244                        let instances = &*app.state::<DbInstances>();
245                        let instances = instances.0.read().await;
246                        for value in instances.values() {
247                            value.close().await;
248                        }
249                    });
250                }
251            })
252            .build()
253    }
254}