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}