How to Use a Local SQLite Database with Tauri and Rust
Create the database file from Rust inside Tauri's setup hook, then layer Diesel migrations on top so schema changes apply on launch.
While building Orion, a desktop app that lets you create multiple chatGPT-powered assistants, I needed a local database to store each assistant's chat history. SQLite was the obvious choice: fast, file-based, and a natural fit for a Tauri app. Here's how I wired it up.
I.Decide where the file lives
The first step is to pick a location for the database file. I chose
~/.config/orion/, which keeps it out of the user's way while staying
easy to find.
To handle creation and lookup, I added a db.rs module with a handful
of small functions:
use std::fs;
use std::path::Path;
// Check if a database file exists, and create one if it does not.
pub fn init() {
if !db_file_exists() {
create_db_file();
}
}
// Create the database file.
fn create_db_file() {
let db_path = get_db_path();
let db_dir = Path::new(&db_path).parent().unwrap();
// If the parent directory does not exist, create it.
if !db_dir.exists() {
fs::create_dir_all(db_dir).unwrap();
}
// Create the database file.
fs::File::create(db_path).unwrap();
}
// Check whether the database file exists.
fn db_file_exists() -> bool {
let db_path = get_db_path();
Path::new(&db_path).exists()
}
// Get the path where the database file should be located.
fn get_db_path() -> String {
let home_dir = dirs::home_dir().unwrap();
home_dir.to_str().unwrap().to_string() + "/.config/orion/database.sqlite"
}init() is the only public entry point. It calls db_file_exists() and
creates the file with create_db_file() when it's missing.
create_db_file() resolves the parent directory via Path::parent(),
creates it with fs::create_dir_all() if needed, and then writes an
empty file with fs::File::create(). get_db_path() uses the dirs
crate to resolve the user's home directory and appends the relative
path.
II.Call init from Tauri's setup hook
To make sure the database is ready before the UI loads, call init()
inside the setup closure of the Tauri builder:
//...
mod db;
async fn main() {
// Create a new Tauri application builder with default settings.
tauri::Builder::default()
.setup(|_app| {
// Initialize the database.
db::init();
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}That's it — the file is created on first launch and reused on every subsequent one.
III.Run Diesel migrations on startup
An empty SQLite file isn't very useful. I wanted to talk to it through
Diesel and have pending migrations applied
automatically whenever the app starts. diesel_migrations makes this
straightforward with embed_migrations!() and run_pending_migrations:
use std::fs;
use std::path::Path;
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
pub fn init() {
if !db_file_exists() {
create_db_file();
}
run_migrations();
}
pub fn establish_db_connection() -> SqliteConnection {
let db_path = get_db_path().clone();
SqliteConnection::establish(db_path.as_str())
.unwrap_or_else(|_| panic!("Error connecting to {}", db_path))
}
fn run_migrations() {
let mut connection = establish_connection();
connection.run_pending_migrations(MIGRATIONS).unwrap();
}
fn establish_connection() -> SqliteConnection {
let db_path = "sqlite://".to_string() + get_db_path().as_str();
SqliteConnection::establish(&db_path)
.unwrap_or_else(|_| panic!("Error connecting to {}", db_path))
}
fn create_db_file() {
let db_path = get_db_path();
let db_dir = Path::new(&db_path).parent().unwrap();
if !db_dir.exists() {
fs::create_dir_all(db_dir).unwrap();
}
fs::File::create(db_path).unwrap();
}
fn db_file_exists() -> bool {
let db_path = get_db_path();
Path::new(&db_path).exists()
}
fn get_db_path() -> String {
let home_dir = dirs::home_dir().unwrap();
home_dir.to_str().unwrap().to_string() + "/.config/orion/database.sqlite"
}init() now also runs any pending migrations after making sure the
file exists, and establish_db_connection() is what the rest of the app
uses to get a SqliteConnection for queries.
That's all you need to interact with a local SQLite database from a Tauri app. If you want to dig deeper into persistent state in Tauri, Aptabase has a good overview in What you need to know about persistent state in Tauri apps.