Skip to content

Commit

Permalink
refactor(sql): Allow multiple drivers at the same time (#1838)
Browse files Browse the repository at this point in the history
* refactor(sql): Allow multiple drivers at the same time

* fmt

* remove default feature comment [skip ci]

* what was that doing there [skip ci]

* disable public methods for now
  • Loading branch information
FabianLars authored Oct 1, 2024
1 parent 6857993 commit 30bcf5d
Show file tree
Hide file tree
Showing 11 changed files with 615 additions and 393 deletions.
5 changes: 5 additions & 0 deletions .changes/feat-multiple-sql-backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
sql: patch
---

It is now possible to enable multiple SQL backends at the same time. There will be no compile error anymore if no backends are enabled!
10 changes: 2 additions & 8 deletions .github/workflows/lint-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,7 @@ jobs:
- uses: Swatinem/rust-cache@v2

- name: clippy ${{ matrix.package }}
if: matrix.package != 'tauri-plugin-sql'
run: cargo clippy --package ${{ matrix.package }} --all-targets -- -D warnings

- name: clippy ${{ matrix.package }} mysql
if: matrix.package == 'tauri-plugin-sql'
run: cargo clippy --package ${{ matrix.package }} --all-targets --no-default-features --features mysql -- -D warnings

- name: clippy ${{ matrix.package }} postgres
if: matrix.package == 'tauri-plugin-sql'
run: cargo clippy --package ${{ matrix.package }} --all-targets --no-default-features --features postgres -- -D warnings
- name: clippy ${{ matrix.package }} --all-features
run: cargo clippy --package ${{ matrix.package }} --all-targets --all-features -- -D warnings
14 changes: 1 addition & 13 deletions .github/workflows/test-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,21 +215,9 @@ jobs:
run: cargo +stable install cross --git https://github.com/cross-rs/cross

- name: test ${{ matrix.package }}
if: matrix.package != 'tauri-plugin-sql' && matrix.package != 'tauri-plugin-http'
if: matrix.package != 'tauri-plugin-http'
run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets --all-features

- name: test ${{ matrix.package }}
if: matrix.package == 'tauri-plugin-http'
run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets

- name: test ${{ matrix.package }} sqlite
if: matrix.package == 'tauri-plugin-sql'
run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets --features sqlite

- name: test ${{ matrix.package }} mysql
if: matrix.package == 'tauri-plugin-sql'
run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets --features mysql

- name: test ${{ matrix.package }} postgres
if: matrix.package == 'tauri-plugin-sql'
run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets --features postgres
2 changes: 1 addition & 1 deletion plugins/localhost/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ impl Builder {
let asset_resolver = app.asset_resolver();
std::thread::spawn(move || {
let server =
Server::http(&format!("localhost:{port}")).expect("Unable to spawn server");
Server::http(format!("localhost:{port}")).expect("Unable to spawn server");
for req in server.incoming_requests() {
let path = req
.url()
Expand Down
1 change: 1 addition & 0 deletions plugins/sql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ indexmap = { version = "2", features = ["serde"] }
sqlite = ["sqlx/sqlite", "sqlx/runtime-tokio"]
mysql = ["sqlx/mysql", "sqlx/runtime-tokio-rustls"]
postgres = ["sqlx/postgres", "sqlx/runtime-tokio-rustls"]
# TODO: bundled-cipher etc
82 changes: 82 additions & 0 deletions plugins/sql/src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use indexmap::IndexMap;
use serde_json::Value as JsonValue;
use sqlx::migrate::Migrator;
use tauri::{command, AppHandle, Runtime, State};

use crate::{DbInstances, DbPool, Error, LastInsertId, Migrations};

#[command]
pub(crate) async fn load<R: Runtime>(
app: AppHandle<R>,
db_instances: State<'_, DbInstances>,
migrations: State<'_, Migrations>,
db: String,
) -> Result<String, crate::Error> {
let pool = DbPool::connect(&db, &app).await?;

if let Some(migrations) = migrations.0.lock().await.remove(&db) {
let migrator = Migrator::new(migrations).await?;
pool.migrate(&migrator).await?;
}

db_instances.0.lock().await.insert(db.clone(), pool);

Ok(db)
}

/// Allows the database connection(s) to be closed; if no database
/// name is passed in then _all_ database connection pools will be
/// shut down.
#[command]
pub(crate) async fn close(
db_instances: State<'_, DbInstances>,
db: Option<String>,
) -> Result<bool, crate::Error> {
let mut instances = db_instances.0.lock().await;

let pools = if let Some(db) = db {
vec![db]
} else {
instances.keys().cloned().collect()
};

for pool in pools {
let db = instances
.get_mut(&pool)
.ok_or(Error::DatabaseNotLoaded(pool))?;
db.close().await;
}

Ok(true)
}

/// Execute a command against the database
#[command]
pub(crate) async fn execute(
db_instances: State<'_, DbInstances>,
db: String,
query: String,
values: Vec<JsonValue>,
) -> Result<(u64, LastInsertId), crate::Error> {
let mut instances = db_instances.0.lock().await;

let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?;
db.execute(query, values).await
}

#[command]
pub(crate) async fn select(
db_instances: State<'_, DbInstances>,
db: String,
query: String,
values: Vec<JsonValue>,
) -> Result<Vec<IndexMap<String, JsonValue>>, crate::Error> {
let mut instances = db_instances.0.lock().await;

let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?;
db.select(query, values).await
}
15 changes: 3 additions & 12 deletions plugins/sql/src/decode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,8 @@
// SPDX-License-Identifier: MIT

#[cfg(feature = "mysql")]
mod mysql;
pub(crate) mod mysql;
#[cfg(feature = "postgres")]
mod postgres;
pub(crate) mod postgres;
#[cfg(feature = "sqlite")]
mod sqlite;

#[cfg(feature = "mysql")]
pub(crate) use mysql::to_json;

#[cfg(feature = "postgres")]
pub(crate) use postgres::to_json;

#[cfg(feature = "sqlite")]
pub(crate) use sqlite::to_json;
pub(crate) mod sqlite;
28 changes: 28 additions & 0 deletions plugins/sql/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use serde::{Serialize, Serializer};

#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Sql(#[from] sqlx::Error),
#[error(transparent)]
Migration(#[from] sqlx::migrate::MigrateError),
#[error("invalid connection url: {0}")]
InvalidDbUrl(String),
#[error("database {0} not loaded")]
DatabaseNotLoaded(String),
#[error("unsupported datatype: {0}")]
UnsupportedDatatype(String),
}

impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
180 changes: 164 additions & 16 deletions plugins/sql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,168 @@
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]

#[cfg(any(
all(feature = "sqlite", feature = "mysql"),
all(feature = "sqlite", feature = "postgres"),
all(feature = "mysql", feature = "postgres")
))]
compile_error!(
"Only one database driver can be enabled. Set the feature flag for the driver of your choice."
);

#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
compile_error!(
"Database driver not defined. Please set the feature flag for the driver of your choice."
);

mod commands;
mod decode;
mod plugin;
pub use plugin::*;
mod error;
mod wrapper;

pub use error::Error;
pub use wrapper::DbPool;

use futures_core::future::BoxFuture;
use serde::{Deserialize, Serialize};
use sqlx::{
error::BoxDynError,
migrate::{Migration as SqlxMigration, MigrationSource, MigrationType, Migrator},
};
use tauri::{
plugin::{Builder as PluginBuilder, TauriPlugin},
Manager, RunEvent, Runtime,
};
use tokio::sync::Mutex;

use std::collections::HashMap;

#[derive(Default)]
pub struct DbInstances(pub Mutex<HashMap<String, DbPool>>);

#[derive(Serialize)]
#[serde(untagged)]
pub(crate) enum LastInsertId {
#[cfg(feature = "sqlite")]
Sqlite(i64),
#[cfg(feature = "mysql")]
MySql(u64),
#[cfg(feature = "postgres")]
Postgres(()),
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
None,
}

struct Migrations(Mutex<HashMap<String, MigrationList>>);

#[derive(Default, Clone, Deserialize)]
pub struct PluginConfig {
#[serde(default)]
preload: Vec<String>,
}

#[derive(Debug)]
pub enum MigrationKind {
Up,
Down,
}

impl From<MigrationKind> for MigrationType {
fn from(kind: MigrationKind) -> Self {
match kind {
MigrationKind::Up => Self::ReversibleUp,
MigrationKind::Down => Self::ReversibleDown,
}
}
}

/// A migration definition.
#[derive(Debug)]
pub struct Migration {
pub version: i64,
pub description: &'static str,
pub sql: &'static str,
pub kind: MigrationKind,
}

#[derive(Debug)]
struct MigrationList(Vec<Migration>);

impl MigrationSource<'static> for MigrationList {
fn resolve(self) -> BoxFuture<'static, std::result::Result<Vec<SqlxMigration>, BoxDynError>> {
Box::pin(async move {
let mut migrations = Vec::new();
for migration in self.0 {
if matches!(migration.kind, MigrationKind::Up) {
migrations.push(SqlxMigration::new(
migration.version,
migration.description.into(),
migration.kind.into(),
migration.sql.into(),
false,
));
}
}
Ok(migrations)
})
}
}

/// Tauri SQL plugin builder.
#[derive(Default)]
pub struct Builder {
migrations: Option<HashMap<String, MigrationList>>,
}

impl Builder {
pub fn new() -> Self {
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))]
eprintln!("No sql driver enabled. Please set at least one of the \"sqlite\", \"mysql\", \"postgres\" feature flags.");

Self::default()
}

/// Add migrations to a database.
#[must_use]
pub fn add_migrations(mut self, db_url: &str, migrations: Vec<Migration>) -> Self {
self.migrations
.get_or_insert(Default::default())
.insert(db_url.to_string(), MigrationList(migrations));
self
}

pub fn build<R: Runtime>(mut self) -> TauriPlugin<R, Option<PluginConfig>> {
PluginBuilder::<R, Option<PluginConfig>>::new("sql")
.invoke_handler(tauri::generate_handler![
commands::load,
commands::execute,
commands::select,
commands::close
])
.setup(|app, api| {
let config = api.config().clone().unwrap_or_default();

tauri::async_runtime::block_on(async move {
let instances = DbInstances::default();
let mut lock = instances.0.lock().await;

for db in config.preload {
let pool = DbPool::connect(&db, app).await?;

if let Some(migrations) = self.migrations.as_mut().unwrap().remove(&db) {
let migrator = Migrator::new(migrations).await?;
pool.migrate(&migrator).await?;
}

lock.insert(db, pool);
}
drop(lock);

app.manage(instances);
app.manage(Migrations(Mutex::new(
self.migrations.take().unwrap_or_default(),
)));

Ok(())
})
})
.on_event(|app, event| {
if let RunEvent::Exit = event {
tauri::async_runtime::block_on(async move {
let instances = &*app.state::<DbInstances>();
let instances = instances.0.lock().await;
for value in instances.values() {
value.close().await;
}
});
}
})
.build()
}
}
Loading

0 comments on commit 30bcf5d

Please sign in to comment.