Replace literal SQL with Diesel abstractions

This commit is contained in:
2026-07-18 17:00:32 +02:00
parent a727c9073d
commit ca5eb4e1ac
69 changed files with 3508 additions and 4285 deletions

View File

@@ -1,8 +1,8 @@
use std::time::Duration;
use crate::db::DbConnection as Connection;
use keyring::Entry;
use reqwest::blocking::Client;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
@@ -681,7 +681,7 @@ fn get_optional_setting(conn: &Connection, key: &str) -> EngineResult<Option<Str
match setting::get_setting_by_key(conn, key) {
Ok(setting) if setting.value.trim().is_empty() => Ok(None),
Ok(setting) => Ok(Some(setting.value)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(diesel::result::Error::NotFound) => Ok(None),
Err(error) => Err(EngineError::Db(error)),
}
}
@@ -724,7 +724,7 @@ mod tests {
use crate::db::Database;
fn setup() -> Database {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
db
}

View File

@@ -3,7 +3,7 @@
use std::collections::BTreeMap;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use crate::db::queries::post as post_q;
use crate::engine::EngineResult;
@@ -65,7 +65,7 @@ mod tests {
#[test]
fn calendar_empty_project() {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
let _ = db.migrate();
let tmp = tempfile::tempdir().unwrap();

View File

@@ -2,7 +2,7 @@ use std::path::PathBuf;
/// Shared context passed to engine operations.
pub struct EngineContext<'a> {
pub conn: &'a rusqlite::Connection,
pub conn: &'a crate::db::DbConnection,
pub project_id: String,
pub data_dir: PathBuf,
}

View File

@@ -3,7 +3,8 @@ use std::fmt;
/// Errors produced by engine operations.
#[derive(Debug)]
pub enum EngineError {
Db(rusqlite::Error),
Db(diesel::result::Error),
DbConnection(diesel::ConnectionError),
Io(std::io::Error),
Parse(String),
NotFound(String),
@@ -15,6 +16,7 @@ impl fmt::Display for EngineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Db(e) => write!(f, "database error: {e}"),
Self::DbConnection(e) => write!(f, "database connection error: {e}"),
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::Parse(msg) => write!(f, "parse error: {msg}"),
Self::NotFound(msg) => write!(f, "not found: {msg}"),
@@ -28,18 +30,34 @@ impl std::error::Error for EngineError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Db(e) => Some(e),
Self::DbConnection(e) => Some(e),
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<rusqlite::Error> for EngineError {
fn from(e: rusqlite::Error) -> Self {
impl From<diesel::result::Error> for EngineError {
fn from(e: diesel::result::Error) -> Self {
Self::Db(e)
}
}
impl From<diesel::ConnectionError> for EngineError {
fn from(e: diesel::ConnectionError) -> Self {
Self::DbConnection(e)
}
}
impl From<crate::db::DatabaseError> for EngineError {
fn from(e: crate::db::DatabaseError) -> Self {
match e {
crate::db::DatabaseError::Connection(e) => Self::DbConnection(e),
crate::db::DatabaseError::Query(e) => Self::Db(e),
}
}
}
impl From<std::io::Error> for EngineError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)

View File

@@ -1,10 +1,10 @@
use std::collections::{HashMap, HashSet};
use std::path::Path;
use crate::db::DbConnection as Connection;
use chrono::{DateTime, TimeZone, Utc};
use pagefind::api::PagefindIndex;
use pagefind::options::PagefindServiceConfig;
use rusqlite::Connection;
use walkdir::WalkDir;
use crate::db::queries;

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use uuid::Uuid;
use walkdir::WalkDir;
@@ -670,7 +670,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
ensure_fts_tables(db.conn()).unwrap();
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use walkdir::WalkDir;
use crate::db::from_row::{script_kind_to_str, template_kind_to_str};
@@ -629,7 +629,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
ensure_fts_tables(db.conn()).unwrap();
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::Path;
use rusqlite::{Connection, params};
use crate::db::DbConnection as Connection;
use uuid::Uuid;
use walkdir::WalkDir;
@@ -189,7 +189,7 @@ pub fn update_post(
/// Publish a post: write file, clear content, set published_at.
pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> EngineResult<Post> {
let mut post = qp::get_post_by_id(conn, post_id)?;
let post = qp::get_post_by_id(conn, post_id)?;
// Require Draft or Archived status
match post.status {
@@ -201,10 +201,27 @@ pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> Engine
}
}
// Compute file_path from created_at + slug
// Use a savepoint for atomicity
// Note: savepoint auto-rolls-back if not released (on error propagation)
conn.execute_batch("SAVEPOINT publish_post")?;
conn.begin_savepoint()?;
match publish_post_in_savepoint(conn, data_dir, post) {
Ok(post) => {
conn.release_savepoint()?;
Ok(post)
}
Err(error) => {
let _ = conn.rollback_savepoint();
Err(error)
}
}
}
fn publish_post_in_savepoint(
conn: &Connection,
data_dir: &Path,
mut post: Post,
) -> EngineResult<Post> {
let post_id = post.id.clone();
// Compute file_path from created_at + slug.
let rel_path = post_file_path(post.created_at, &post.slug);
let abs_path = data_dir.join(&rel_path);
@@ -245,7 +262,7 @@ pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> Engine
qp::set_published_snapshot(
conn,
post_id,
&post_id,
&post.title,
&body,
&tags_json,
@@ -256,27 +273,24 @@ pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> Engine
)?;
// Set file_path and checksum in DB
qp::set_post_file_path(conn, post_id, &post.file_path, now)?;
conn.execute(
"UPDATE posts SET checksum = ?1 WHERE id = ?2",
params![post.checksum, post_id],
)?;
qp::set_post_file_path(conn, &post_id, &post.file_path, now)?;
qp::set_post_checksum(conn, &post_id, post.checksum.as_deref())?;
// Clear content in DB
qp::clear_post_content(conn, post_id, now)?;
qp::clear_post_content(conn, &post_id, now)?;
post.content = None;
// Set status = Published
qp::update_post_status(conn, post_id, &PostStatus::Published, now)?;
qp::update_post_status(conn, &post_id, &PostStatus::Published, now)?;
// Publish all translations
let translations = qt::list_post_translations_by_post(conn, post_id)?;
let translations = qt::list_post_translations_by_post(conn, &post_id)?;
for mut t in translations {
publish_translation(conn, data_dir, &mut t, &post)?;
}
// Parse inter-post links and update link graph
ql::delete_links_by_source(conn, post_id)?;
ql::delete_links_by_source(conn, &post_id)?;
let link_body = if let Some(ref pc) = post.published_content {
pc.as_str()
} else {
@@ -287,7 +301,7 @@ pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> Engine
if let Ok(target) = qp::get_post_by_project_and_slug(conn, &post.project_id, target_slug) {
let link = PostLink {
id: Uuid::new_v4().to_string(),
source_post_id: post_id.to_string(),
source_post_id: post_id.clone(),
target_post_id: target.id.clone(),
link_text: Some(link_text.clone()),
created_at: now,
@@ -299,8 +313,6 @@ pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> Engine
// Re-index FTS
fts_index_post(conn, &post)?;
conn.execute_batch("RELEASE publish_post")?;
Ok(post)
}
@@ -515,16 +527,10 @@ pub fn delete_post(conn: &Connection, data_dir: &Path, post_id: &str) -> EngineR
// Delete post links (source and target)
ql::delete_links_by_source(conn, post_id)?;
conn.execute(
"DELETE FROM post_links WHERE target_post_id = ?1",
params![post_id],
)?;
ql::delete_links_by_target(conn, post_id)?;
// Delete post-media associations
conn.execute(
"DELETE FROM post_media WHERE post_id = ?1",
params![post_id],
)?;
crate::db::queries::post_media::delete_post_media_by_post(conn, post_id)?;
// Remove from FTS
fts::remove_post_from_index(conn, post_id)?;
@@ -882,13 +888,6 @@ fn publish_translation(
t.content = None;
qt::update_post_translation(conn, t)?;
// Clear content after update (the update already set content=None via the struct)
// but we also do an explicit clear to be safe
conn.execute(
"UPDATE post_translations SET content = NULL WHERE id = ?1",
params![t.id],
)?;
Ok(())
}
@@ -1149,7 +1148,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
ensure_fts_tables(db.conn()).unwrap();
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();

View File

@@ -1,6 +1,6 @@
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use uuid::Uuid;
use crate::db::queries::media as qm;
@@ -109,22 +109,17 @@ mod tests {
use crate::db::Database;
use crate::db::queries::media::{insert_media, make_test_media};
use crate::db::queries::post::{insert_post, make_test_post};
use crate::db::queries::post_media::list_post_media_by_post;
use crate::db::queries::project::{insert_project, make_test_project};
use crate::util::sidecar::read_sidecar;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
let dir = TempDir::new().unwrap();
// Create a post
db.conn()
.execute(
"INSERT INTO posts (id, project_id, title, slug, status, file_path, created_at, updated_at) VALUES ('post1', 'p1', 'Test', 'test', 'draft', '', 1000, 1000)",
[],
)
.unwrap();
insert_post(db.conn(), &make_test_post("post1", "p1", "test")).unwrap();
(db, dir)
}
@@ -197,13 +192,10 @@ mod tests {
let (db, dir) = setup();
insert_test_media(&db, dir.path(), "m1");
// Create a second post
db.conn()
.execute(
"INSERT INTO posts (id, project_id, title, slug, status, file_path, created_at, updated_at) VALUES ('post2', 'p1', 'Test2', 'test2', 'draft', '', 2000, 2000)",
[],
)
.unwrap();
let mut post = make_test_post("post2", "p1", "test2");
post.created_at = 2000;
post.updated_at = 2000;
insert_post(db.conn(), &post).unwrap();
link_media_to_post(db.conn(), dir.path(), "p1", "post1", "m1", 0).unwrap();
link_media_to_post(db.conn(), dir.path(), "p1", "post2", "m1", 0).unwrap();

View File

@@ -453,7 +453,7 @@ mod tests {
meta::write_project_json(dir.path(), &make_metadata()).unwrap();
let db_path = dir.path().join("bds.db");
let mut db = Database::open(&db_path).unwrap();
let db = Database::open(&db_path).unwrap();
db.migrate().unwrap();
queries::project::insert_project(
db.conn(),

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use uuid::Uuid;
use crate::db::queries::project as q;
@@ -64,7 +64,7 @@ pub fn ensure_default_project(
) -> EngineResult<Project> {
match q::get_project_by_id(conn, DEFAULT_PROJECT_ID) {
Ok(p) => Ok(p),
Err(rusqlite::Error::QueryReturnedNoRows) => {
Err(diesel::result::Error::NotFound) => {
let now = now_unix_ms();
let project = Project {
id: DEFAULT_PROJECT_ID.to_string(),
@@ -94,7 +94,7 @@ pub fn ensure_default_project(
pub fn get_active_project(conn: &Connection) -> EngineResult<Option<Project>> {
match q::get_active_project(conn) {
Ok(p) => Ok(Some(p)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(diesel::result::Error::NotFound) => Ok(None),
Err(e) => Err(EngineError::Db(e)),
}
}
@@ -280,7 +280,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
let dir = TempDir::new().unwrap();
(db, dir)

View File

@@ -1,7 +1,7 @@
use std::path::Path;
use std::sync::Arc;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use crate::db::fts;
use crate::engine::EngineResult;
@@ -157,7 +157,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
fts::ensure_fts_tables(db.conn()).unwrap();
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use uuid::Uuid;
use crate::db::queries::script as qs;
@@ -305,7 +305,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
let dir = TempDir::new().unwrap();

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use walkdir::WalkDir;
use crate::db::queries::script as qs;
@@ -145,7 +145,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
let dir = TempDir::new().unwrap();

View File

@@ -1,6 +1,6 @@
//! Full-text search reindexing engine functions.
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use crate::db::fts;
use crate::db::queries::{media as media_q, media_translation, post as post_q, post_translation};
@@ -32,8 +32,7 @@ pub fn reindex_all_with_progress(
on_item: Option<ItemProgressFn>,
) -> EngineResult<ReindexReport> {
// Wipe existing FTS content
conn.execute("DELETE FROM posts_fts", [])?;
conn.execute("DELETE FROM media_fts", [])?;
fts::clear_indexes(conn)?;
// Reindex all posts
let posts = post_q::list_posts_by_project(conn, project_id)?;
@@ -124,7 +123,7 @@ mod tests {
use crate::engine;
fn setup() -> (Database, String) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
let _ = db.migrate();
ensure_fts_tables(db.conn()).unwrap();

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use crate::engine::{EngineError, EngineResult};
use crate::render::{GeneratedWriteOutcome, write_generated_bytes};

View File

@@ -1,6 +1,6 @@
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use uuid::Uuid;
use crate::db::queries::post as post_q;
@@ -360,7 +360,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("meta")).unwrap();

View File

@@ -1,7 +1,9 @@
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use crate::db::schema::{posts, tags};
use diesel::prelude::*;
use uuid::Uuid;
use crate::db::queries::template as qt;
@@ -383,36 +385,40 @@ fn validate_liquid_blocks(content: &str) -> Result<(), String> {
}
fn count_posts_using_template(conn: &Connection, slug: &str) -> EngineResult<usize> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM posts WHERE template_slug = ?1",
rusqlite::params![slug],
|row| row.get(0),
)?;
let count: i64 = conn.with(|c| {
posts::table
.filter(posts::template_slug.eq(slug))
.count()
.get_result(c)
})?;
Ok(count as usize)
}
fn count_tags_using_template(conn: &Connection, slug: &str) -> EngineResult<usize> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM tags WHERE post_template_slug = ?1",
rusqlite::params![slug],
|row| row.get(0),
)?;
let count: i64 = conn.with(|c| {
tags::table
.filter(tags::post_template_slug.eq(slug))
.count()
.get_result(c)
})?;
Ok(count as usize)
}
fn null_template_slug_on_posts(conn: &Connection, slug: &str) -> EngineResult<()> {
conn.execute(
"UPDATE posts SET template_slug = NULL WHERE template_slug = ?1",
rusqlite::params![slug],
)?;
conn.with(|c| {
diesel::update(posts::table.filter(posts::template_slug.eq(slug)))
.set(posts::template_slug.eq(None::<String>))
.execute(c)
})?;
Ok(())
}
fn null_template_slug_on_tags(conn: &Connection, slug: &str) -> EngineResult<()> {
conn.execute(
"UPDATE tags SET post_template_slug = NULL WHERE post_template_slug = ?1",
rusqlite::params![slug],
)?;
conn.with(|c| {
diesel::update(tags::table.filter(tags::post_template_slug.eq(slug)))
.set(tags::post_template_slug.eq(None::<String>))
.execute(c)
})?;
Ok(())
}
@@ -424,7 +430,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
let dir = TempDir::new().unwrap();

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use walkdir::WalkDir;
use crate::db::queries::template as qt;
@@ -144,7 +144,7 @@ mod tests {
use tempfile::TempDir;
fn setup() -> (Database, TempDir) {
let mut db = Database::open_in_memory().unwrap();
let db = Database::open_in_memory().unwrap();
db.migrate().unwrap();
insert_project(db.conn(), &make_test_project("p1", "blog")).unwrap();
let dir = TempDir::new().unwrap();

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use crate::db::queries::media as mq;
use crate::db::queries::post_media as pmq;

View File

@@ -1,7 +1,7 @@
use std::collections::HashSet;
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use walkdir::WalkDir;
use crate::db::queries;

View File

@@ -2,7 +2,7 @@
use std::path::Path;
use rusqlite::Connection;
use crate::db::DbConnection as Connection;
use crate::db::queries::{post as post_q, post_translation};
use crate::engine::EngineResult;