Vmxes
Vmxes

Reputation: 2755

Generic database connection in Tauri using Rust

As a Tauri and Rust beginner, I am trying to develop a basic Tauri 2 application with React and Typescript frontend and Rust backend. I want a basic database manager-like app, where users can enter database connection data, including the DB engine. After the app is connected to the DB, users can execute predefined queries.

I want to support MySQL, PostgreSQL, and SQLite so I use sqlx crate, that supports all of them. The engine is selected by the user during runtime.

I've been struggling for days how to solve this simple task. What I figured out until now is that I need a Tauri state to store the connection and use this later for query execution.

So my main tauri code manages DatabaseManager:

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    //let db_state = Arc::new(Mutex::new(DatabaseState::new()));

    tauri::Builder::default()
        .setup(|app| {
            #[cfg(debug_assertions)]
            app.get_webview_window("main").unwrap().open_devtools();
            Ok(())
        })
        .plugin(tauri_plugin_shell::init())
        .manage(DatabaseManager::new())
        .invoke_handler(tauri::generate_handler![initialize_database_connection])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

My DatabaseManager looks like this:

pub struct DatabaseManager {
    pub connection: Option<Arc<Mutex<DatabaseConnection>>>,
}

impl DatabaseManager {
    pub fn new() -> Self {
        DatabaseManager { connection: None }
    }

    pub fn get_connection(&self) -> Option<Arc<Mutex<DatabaseConnection>>> {
        self.connection.clone()
    }

    pub fn set_connection(&mut self, connection: DatabaseConnection) {
        self.connection = Some(Arc::new(Mutex::new(connection)));
    }
}   

Where DatabaseConnection is:

pub enum DatabaseConnection {
    MySql(Box<MySqlConnection>),
    Postgres(Box<PgConnection>),
    Sqlite(Box<SqliteConnection>),
}

impl DatabaseConnection {
    pub async fn new(
        engine: &str,
        host: &str,
        port: &str,
        database: &str,
        username: &str,
        password: &str,
    ) -> Result<Self, Box<dyn Error>> {
        let connection = match engine {
            "mysql" | "mariadb" => {
                let connection = MySqlConnection::connect(&format!(
                    "mysql://{}:{}@{}:{}/{}",
                    username, password, host, port, database
                ))
                    .await?;
                DatabaseConnection::MySql(Box::new(connection))
            }
            "postgres" => {
                let connection = PgConnection::connect(&format!(
                    "postgres://{}:{}@{}:{}/{}",
                    username, password, host, port, database
                ))
                    .await?;
                DatabaseConnection::Postgres(Box::new(connection))
            }
            "sqlite" => {
                let connection = SqliteConnection::connect(&format!(
                    "sqlite://{}",
                    database
                ))
                    .await?;
                DatabaseConnection::Sqlite(Box::new(connection))
            }
            _ => return Err("Unsupported engine type".into()),
        };

        Ok(connection)
    }
}   

And the commend to invoke on the frontend looks like this:

#[tauri::command]
pub async fn initialize_database_connection(
    engine: String,
    host: String,
    port: String,
    database: String,
    username: String,
    password: String,
    state: State<'_, Arc<Mutex<DatabaseManager>>>,
) -> Result<String, String> {
    let connection = DatabaseConnection::new(
        &engine, &host, &port, &database, &username, &password,
    ).await;

    match connection {
        Ok(conn) => {
            let mut state = state.lock().await;
            state.set_connection(conn);
            Ok("Database connection established".into())
        }
        Err(e) => Err(format!("Failed to connect: {}", e)),
    }
}   

The code doesn't compile as future returned by initialize_database_connection is not Send

The full error message is:

error: future cannot be sent between threads safely         
   --> src\commands\db_commands.rs:7:1
    |
7   | #[tauri::command]
    | ^^^^^^^^^^^^^^^^^ future returned by `initialize_database_connection` is not `Send`
    |
   ::: src\lib.rs:23:25
    |
23  |         .invoke_handler(tauri::generate_handler![initialize_database_connection])
    |                         -------------------------------------------------------- in this macro invocation
    |
    = help: the trait `Send` is not implemented for `dyn std::error::Error`, which is required by `impl Future<Output = Result<std::string::String, std::string::String>>: Send`
note: future is not `Send` as this value is used across an await
   --> src\commands\db_commands.rs:25:42
    |
19  |     let connection = DatabaseConnection::new(
    |         ---------- has type `Result<DatabaseConnection, Box<dyn std::error::Error>>` which is not `Send`
...
25  |             let mut state = state.lock().await;
    |                                          ^^^^^ await occurs here, with `connection` maybe used later
note: required by a bound in `ResultFutureTag::future`
   --> C:\Users\Balázs\.cargo\registry\src\index.crates.io-6f17d22bba15001f\tauri-2.0.6\src\ipc\command.rs:331:42
    |
324 |     pub fn future<T, E, F>(
    |            ------ required by a bound in this associated function
...
331 |       F: Future<Output = Result<T, E>> + Send,
    |                                          ^^^^ required by this bound in `ResultFutureTag::future`
    = note: this error originates in the macro `__cmd__initialize_database_connection` which comes from the expansion of the macro `tauri::generate_handler` (in Nightly builds, run with -Z macro-backtrace for more info)        

I don't really know what to do with this error. To be honest I don't even know if the above concept is a good starting point at all.

Upvotes: 0

Views: 181

Answers (0)

Related Questions