Files
RuDS/crates/bds-core/src/db/connection.rs

158 lines
4.8 KiB
Rust

use std::cell::RefCell;
use std::path::Path;
use diesel::connection::SimpleConnection;
use diesel::prelude::*;
use diesel::sql_types::Text;
use crate::db::migrations;
#[derive(Debug, thiserror::Error)]
pub enum DatabaseError {
#[error("{0}")]
Connection(#[from] diesel::ConnectionError),
#[error("{0}")]
Query(#[from] diesel::result::Error),
}
/// Shared synchronous Diesel connection used by the engine query API.
pub struct DbConnection(RefCell<SqliteConnection>);
#[derive(QueryableByName)]
struct DatabasePathRow {
#[diesel(sql_type = Text)]
file: String,
}
impl DbConnection {
pub fn with<T>(
&self,
operation: impl FnOnce(&mut SqliteConnection) -> diesel::QueryResult<T>,
) -> diesel::QueryResult<T> {
operation(&mut self.0.borrow_mut())
}
pub(crate) fn with_migrations<T>(
&self,
operation: impl FnOnce(&mut SqliteConnection) -> T,
) -> T {
operation(&mut self.0.borrow_mut())
}
pub(crate) fn begin_savepoint(&self) -> diesel::QueryResult<()> {
self.0.borrow_mut().batch_execute("SAVEPOINT bds_operation")
}
pub(crate) fn release_savepoint(&self) -> diesel::QueryResult<()> {
self.0.borrow_mut().batch_execute("RELEASE bds_operation")
}
pub(crate) fn rollback_savepoint(&self) -> diesel::QueryResult<()> {
self.0
.borrow_mut()
.batch_execute("ROLLBACK TO bds_operation; RELEASE bds_operation")
}
#[cfg(test)]
pub(crate) fn reject_tag_template_updates_for_test(&self) -> diesel::QueryResult<()> {
self.0.borrow_mut().batch_execute(
"CREATE TRIGGER reject_template_tag_cascade \
BEFORE UPDATE OF post_template_slug ON tags \
BEGIN SELECT RAISE(ABORT, 'reject cascade'); END",
)
}
#[cfg(test)]
pub(crate) fn insert_legacy_null_mcp_proposal_for_test(&self) -> diesel::QueryResult<()> {
self.0.borrow_mut().batch_execute(
"INSERT INTO mcp_proposals \
(id, project_id, kind, status, entity_id, data, created_at, expires_at) \
VALUES ('null-entity', 'p1', 'draft_post', 'pending', NULL, '{}', 3, 99)",
)
}
/// Filesystem database path for sibling surfaces that must open their own
/// short-lived connection (gallery workers, preview servers, Lua hosts).
pub fn database_path(&self) -> diesel::QueryResult<std::path::PathBuf> {
self.with(|conn| {
diesel::sql_query("SELECT file FROM pragma_database_list WHERE name = 'main'")
.get_result::<DatabasePathRow>(conn)
.and_then(|row| {
if row.file.is_empty() {
Err(diesel::result::Error::NotFound)
} else {
Ok(row.file.into())
}
})
})
}
}
/// Database wrapper managing a SQLite connection.
pub struct Database {
conn: DbConnection,
}
impl Database {
/// Open an existing bDS project database.
pub fn open(path: &Path) -> Result<Self, DatabaseError> {
Self::establish(path.to_string_lossy().as_ref(), true)
}
/// Open an in-memory database (for tests).
pub fn open_in_memory() -> Result<Self, DatabaseError> {
Self::establish(":memory:", false)
}
fn establish(database_url: &str, wal: bool) -> Result<Self, DatabaseError> {
let mut conn = SqliteConnection::establish(database_url)?;
// SQLite connection configuration is backend-specific and not expressible in Diesel's DSL.
conn.batch_execute(if wal {
"PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA foreign_keys=ON;"
} else {
"PRAGMA foreign_keys=ON;"
})?;
Ok(Self {
conn: DbConnection(RefCell::new(conn)),
})
}
pub fn conn(&self) -> &DbConnection {
&self.conn
}
/// Run all pending embedded Diesel migrations.
pub fn migrate(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
migrations::run_migrations(&self.conn)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_in_memory() {
let db = Database::open_in_memory().expect("should open in-memory db");
let result = db
.conn()
.with(|conn| {
diesel::select(1.into_sql::<diesel::sql_types::Integer>()).get_result::<i32>(conn)
})
.unwrap();
assert_eq!(result, 1);
}
#[test]
fn reports_the_disk_database_path() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("project.sqlite3");
let db = Database::open(&path).unwrap();
assert_eq!(
db.conn().database_path().unwrap().canonicalize().unwrap(),
path.canonicalize().unwrap()
);
}
}