// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. //! Lower-level, generic SQLite connection management. //! //! This module is inspired by, and borrows concepts from, the //! Application Services `sql-support` crate. use std::sync::{Mutex, MutexGuard}; use std::{fmt::Debug, num::NonZeroU32, path::Path}; use rusqlite::{OpenFlags, Transaction, TransactionBehavior}; /// Sets up an SQLite database connection, and either /// initializes an empty physical database with the latest schema, or /// upgrades an existing physical database to the latest schema. pub trait ConnectionOpener { /// The highest schema version that we support. const MAX_SCHEMA_VERSION: u32; type Error: From; /// Sets up an opened connection for use. This is a good place to /// set pragmas and configuration options, register functions, and /// load extensions. fn setup(_conn: &mut rusqlite::Connection) -> Result<(), Self::Error> { Ok(()) } /// Initializes an empty physical database with the latest schema. fn create(tx: &mut Transaction<'_>) -> Result<(), Self::Error>; /// Upgrades an existing physical database to the schema with /// the given version. fn upgrade(tx: &mut Transaction<'_>, to_version: NonZeroU32) -> Result<(), Self::Error>; /// Validate that the database is usable. /// /// Called after `create` / `upgrade`. fn validate(_tx: &mut Transaction<'_>) -> Result<(), Self::Error> { Ok(()) } } /// A thread-safe wrapper around a connection to a physical SQLite database. pub struct Connection { /// The inner connection. conn: Mutex, } impl Connection { /// Opens a connection to a physical database at the given path. pub fn new(path: &Path) -> Result where O: ConnectionOpener, { let flags = OpenFlags::SQLITE_OPEN_NO_MUTEX // Send/Sync is guaranteed by Rust already | OpenFlags::SQLITE_OPEN_EXRESCODE // Extended result codes | OpenFlags::SQLITE_OPEN_CREATE // Create if it doesn't exist | OpenFlags::SQLITE_OPEN_READ_WRITE; // opened for reading and writing let mut conn = rusqlite::Connection::open_with_flags(path, flags)?; O::setup(&mut conn)?; // On open upgrade the schema to the latest version. let mut tx = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; match tx.query_row_and_then("PRAGMA user_version", [], |row| row.get(0)) { Ok(mut version @ 1..) => { while version < O::MAX_SCHEMA_VERSION { O::upgrade(&mut tx, NonZeroU32::new(version + 1).unwrap())?; version += 1; } } Ok(0) => O::create(&mut tx)?, Err(err) => Err(err)?, } // Set the schema version to the highest that we support. // If the current version is higher than ours, downgrade it, // so that upgrading to it again in the future can fix up any // invariants that our version might not uphold. tx.execute_batch(&format!("PRAGMA user_version = {}", O::MAX_SCHEMA_VERSION))?; O::validate(&mut tx)?; tx.commit()?; Ok(Self::with_connection(conn)) } fn with_connection(conn: rusqlite::Connection) -> Self { Self { conn: Mutex::new(conn), } } /// Get ahold of the connection with no transaction opened. pub fn lock<'a>(&'a self) -> MutexGuard<'a, rusqlite::Connection> { self.conn.lock().unwrap() } /// Accesses the database for reading. pub fn read( &self, f: impl FnOnce(&rusqlite::Connection) -> Result, ) -> Result { let conn = self.conn.lock().unwrap(); f(&conn) } /// Accesses the database in a transaction for reading and writing. pub fn write(&self, f: impl FnOnce(&mut Transaction<'_>) -> Result) -> Result where E: From, { let mut conn = self.conn.lock().unwrap(); let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; let result = f(&mut tx)?; tx.commit()?; Ok(result) } } impl Debug for Connection { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Connection { .. }") } }