158 lines
4.8 KiB
Rust
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()
|
|
);
|
|
}
|
|
}
|