1#![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#[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
114fn 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#[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 #[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}