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,62 +1,74 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use crate::db::from_row::{GENERATED_FILE_HASH_COLUMNS, generated_file_hash_from_row};
use crate::db::DbConnection;
use crate::db::from_row::GeneratedFileHashRecord;
use crate::db::schema::generated_file_hashes;
use crate::model::GeneratedFileHash;
pub fn get_generated_file_hash(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
relative_path: &str,
) -> rusqlite::Result<GeneratedFileHash> {
conn.query_row(
&format!(
"SELECT {GENERATED_FILE_HASH_COLUMNS} FROM generated_file_hashes WHERE project_id = ?1 AND relative_path = ?2"
),
params![project_id, relative_path],
generated_file_hash_from_row,
)
) -> QueryResult<GeneratedFileHash> {
conn.with(|c| {
generated_file_hashes::table
.filter(generated_file_hashes::project_id.eq(project_id))
.filter(generated_file_hashes::relative_path.eq(relative_path))
.select(GeneratedFileHashRecord::as_select())
.first(c)
.map(Into::into)
})
}
pub fn upsert_generated_file_hash(
conn: &Connection,
conn: &DbConnection,
hash: &GeneratedFileHash,
) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO generated_file_hashes (project_id, relative_path, content_hash, updated_at)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(project_id, relative_path)
DO UPDATE SET content_hash = excluded.content_hash, updated_at = excluded.updated_at",
params![
hash.project_id,
hash.relative_path,
hash.content_hash,
hash.updated_at
],
)?;
Ok(())
) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(generated_file_hashes::table)
.values(GeneratedFileHashRecord::from(hash))
.on_conflict((
generated_file_hashes::project_id,
generated_file_hashes::relative_path,
))
.do_update()
.set((
generated_file_hashes::content_hash.eq(&hash.content_hash),
generated_file_hashes::updated_at.eq(hash.updated_at),
))
.execute(c)
.map(|_| ())
})
}
pub fn delete_generated_file_hash(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
relative_path: &str,
) -> rusqlite::Result<()> {
conn.execute(
"DELETE FROM generated_file_hashes WHERE project_id = ?1 AND relative_path = ?2",
params![project_id, relative_path],
)?;
Ok(())
) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(
generated_file_hashes::table
.filter(generated_file_hashes::project_id.eq(project_id))
.filter(generated_file_hashes::relative_path.eq(relative_path)),
)
.execute(c)
.map(|_| ())
})
}
pub fn list_generated_file_hashes_by_project(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
) -> rusqlite::Result<Vec<GeneratedFileHash>> {
let mut stmt = conn.prepare(&format!(
"SELECT {GENERATED_FILE_HASH_COLUMNS} FROM generated_file_hashes WHERE project_id = ?1 ORDER BY relative_path"
))?;
let rows = stmt.query_map(params![project_id], generated_file_hash_from_row)?;
rows.collect()
) -> QueryResult<Vec<GeneratedFileHash>> {
conn.with(|c| {
generated_file_hashes::table
.filter(generated_file_hashes::project_id.eq(project_id))
.order(generated_file_hashes::relative_path)
.select(GeneratedFileHashRecord::as_select())
.load(c)
.map(|rows: Vec<GeneratedFileHashRecord>| rows.into_iter().map(Into::into).collect())
})
}
#[cfg(test)]
@@ -66,7 +78,7 @@ mod tests {
use crate::db::queries::project::{insert_project, make_test_project};
fn setup() -> Database {
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();
db

View File

@@ -1,120 +1,87 @@
use rusqlite::{Connection, params};
use chrono::{Datelike, TimeZone, Utc};
use diesel::prelude::*;
use diesel::sql_types::Text;
use crate::db::from_row::{MEDIA_COLUMNS, media_from_row};
use crate::db::DbConnection;
use crate::db::from_row::{MediaRecord, convert, convert_all};
use crate::db::schema::media;
use crate::model::Media;
use crate::util::calendar_range_unix_ms;
fn tags_to_json(tags: &[String]) -> String {
serde_json::to_string(tags).unwrap_or_else(|_| "[]".into())
diesel::define_sql_function!(fn instr(haystack: Text, needle: Text) -> Integer);
pub fn insert_media(conn: &DbConnection, m: &Media) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(media::table)
.values(MediaRecord::from(m))
.execute(c)
.map(|_| ())
})
}
pub fn insert_media(conn: &Connection, m: &Media) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO media (
id, project_id, filename, original_name, mime_type, size,
width, height, title, alt, caption, author, language,
file_path, sidecar_path, checksum, tags, created_at, updated_at
) VALUES (
?1, ?2, ?3, ?4, ?5, ?6,
?7, ?8, ?9, ?10, ?11, ?12, ?13,
?14, ?15, ?16, ?17, ?18, ?19
)",
params![
m.id,
m.project_id,
m.filename,
m.original_name,
m.mime_type,
m.size,
m.width,
m.height,
m.title,
m.alt,
m.caption,
m.author,
m.language,
m.file_path,
m.sidecar_path,
m.checksum,
tags_to_json(&m.tags),
m.created_at,
m.updated_at,
],
)?;
Ok(())
pub fn get_media_by_id(conn: &DbConnection, id: &str) -> QueryResult<Media> {
conn.with(|c| {
media::table
.filter(media::id.eq(id))
.select(MediaRecord::as_select())
.first(c)
.and_then(convert)
})
}
pub fn get_media_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Media> {
conn.query_row(
&format!("SELECT {MEDIA_COLUMNS} FROM media WHERE id = ?1"),
params![id],
media_from_row,
)
}
pub fn list_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<Media>> {
let mut stmt = conn.prepare(&format!(
"SELECT {MEDIA_COLUMNS} FROM media WHERE project_id = ?1 ORDER BY created_at DESC"
))?;
let rows = stmt.query_map(params![project_id], media_from_row)?;
rows.collect()
pub fn list_media_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<Media>> {
conn.with(|c| {
media::table
.filter(media::project_id.eq(project_id))
.order(media::created_at.desc())
.select(MediaRecord::as_select())
.load(c)
.and_then(convert_all)
})
}
pub fn list_media_by_project_limited(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
limit: i64,
offset: i64,
) -> rusqlite::Result<Vec<Media>> {
let mut stmt = conn.prepare(&format!(
"SELECT {MEDIA_COLUMNS} FROM media WHERE project_id = ?1 ORDER BY created_at DESC LIMIT ?2 OFFSET ?3"
))?;
let rows = stmt.query_map(params![project_id, limit, offset], media_from_row)?;
rows.collect()
) -> QueryResult<Vec<Media>> {
conn.with(|c| {
media::table
.filter(media::project_id.eq(project_id))
.order(media::created_at.desc())
.limit(limit)
.offset(offset)
.select(MediaRecord::as_select())
.load(c)
.and_then(convert_all)
})
}
pub fn count_media_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<i64> {
conn.query_row(
"SELECT COUNT(*) FROM media WHERE project_id = ?1",
params![project_id],
|row| row.get(0),
)
pub fn count_media_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<i64> {
conn.with(|c| {
media::table
.filter(media::project_id.eq(project_id))
.count()
.get_result(c)
})
}
pub fn update_media(conn: &Connection, m: &Media) -> rusqlite::Result<()> {
conn.execute(
"UPDATE media SET
filename = ?1, original_name = ?2, mime_type = ?3, size = ?4,
width = ?5, height = ?6, title = ?7, alt = ?8, caption = ?9,
author = ?10, language = ?11, file_path = ?12, sidecar_path = ?13,
checksum = ?14, tags = ?15, updated_at = ?16
WHERE id = ?17",
params![
m.filename,
m.original_name,
m.mime_type,
m.size,
m.width,
m.height,
m.title,
m.alt,
m.caption,
m.author,
m.language,
m.file_path,
m.sidecar_path,
m.checksum,
tags_to_json(&m.tags),
m.updated_at,
m.id,
],
)?;
Ok(())
pub fn update_media(conn: &DbConnection, m: &Media) -> QueryResult<()> {
conn.with(|c| {
diesel::update(media::table.filter(media::id.eq(&m.id)))
.set(MediaRecord::from(m))
.execute(c)
.map(|_| ())
})
}
pub fn delete_media(conn: &Connection, id: &str) -> rusqlite::Result<()> {
conn.execute("DELETE FROM media WHERE id = ?1", params![id])?;
Ok(())
pub fn delete_media(conn: &DbConnection, id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(media::table.filter(media::id.eq(id)))
.execute(c)
.map(|_| ())
})
}
// ── Filtered queries (per sidebar_views.allium MediaView) ───
@@ -140,95 +107,86 @@ impl MediaFilterParams {
/// List media with optional filters applied.
pub fn list_media_filtered(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
filters: &MediaFilterParams,
limit: i64,
offset: i64,
) -> rusqlite::Result<Vec<Media>> {
let mut conditions = vec!["project_id = ?1".to_string()];
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
param_values.push(Box::new(project_id.to_string()));
if !filters.search_query.is_empty() {
let idx = param_values.len() + 1;
let pattern = format!("%{}%", filters.search_query.replace('%', "\\%"));
conditions.push(format!(
"(COALESCE(title, original_name) LIKE ?{idx} ESCAPE '\\')"
));
param_values.push(Box::new(pattern));
}
if let Some(year) = filters.year {
let (start, end) =
calendar_range_unix_ms(year, filters.month).ok_or(rusqlite::Error::InvalidQuery)?;
let idx1 = param_values.len() + 1;
let idx2 = param_values.len() + 2;
conditions.push(format!("(created_at >= ?{idx1} AND created_at < ?{idx2})"));
param_values.push(Box::new(start));
param_values.push(Box::new(end));
}
for tag in &filters.tags {
let idx = param_values.len() + 1;
let pattern = format!("%\"{}\"%", tag.replace('"', "\\\""));
conditions.push(format!("(tags LIKE ?{idx})"));
param_values.push(Box::new(pattern));
}
let where_clause = conditions.join(" AND ");
let idx_limit = param_values.len() + 1;
let idx_offset = param_values.len() + 2;
param_values.push(Box::new(limit));
param_values.push(Box::new(offset));
let sql = format!(
"SELECT {MEDIA_COLUMNS} FROM media WHERE {where_clause} ORDER BY created_at DESC LIMIT ?{idx_limit} OFFSET ?{idx_offset}"
);
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
param_values.iter().map(|b| b.as_ref()).collect();
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(params_refs.as_slice(), media_from_row)?;
rows.collect()
) -> QueryResult<Vec<Media>> {
conn.with(|c| {
let mut query = media::table
.filter(media::project_id.eq(project_id))
.into_boxed();
if !filters.search_query.is_empty() {
query = query.filter(
media::title
.is_not_null()
.and(instr(media::title.assume_not_null(), &filters.search_query).gt(0))
.or(media::title
.is_null()
.and(instr(media::original_name, &filters.search_query).gt(0))),
);
}
if let Some(year) = filters.year {
let (start, end) = calendar_range_unix_ms(year, filters.month).ok_or_else(|| {
diesel::result::Error::SerializationError("invalid calendar range".into())
})?;
query = query.filter(media::created_at.ge(start).and(media::created_at.lt(end)));
}
for tag in &filters.tags {
query = query.filter(instr(media::tags, serde_json::to_string(tag).unwrap()).gt(0));
}
query
.order(media::created_at.desc())
.limit(limit)
.offset(offset)
.select(MediaRecord::as_select())
.load(c)
.and_then(convert_all)
})
}
/// Year/month counts for the media calendar archive widget.
pub fn media_calendar_counts(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
) -> rusqlite::Result<Vec<(i32, u32, usize)>> {
let mut stmt = conn.prepare(
"SELECT
CAST(strftime('%Y', created_at / 1000, 'unixepoch') AS INTEGER) AS y,
CAST(strftime('%m', created_at / 1000, 'unixepoch') AS INTEGER) AS m,
COUNT(*) AS cnt
FROM media
WHERE project_id = ?1
GROUP BY y, m
ORDER BY y DESC, m DESC",
)?;
let rows = stmt.query_map(params![project_id], |row| {
Ok((
row.get::<_, i32>(0)?,
row.get::<_, u32>(1)?,
row.get::<_, usize>(2)?,
))
})?;
rows.collect()
) -> QueryResult<Vec<(i32, u32, usize)>> {
conn.with(|c| {
let timestamps = media::table
.filter(media::project_id.eq(project_id))
.select(media::created_at)
.load::<i64>(c)?;
let mut counts = std::collections::BTreeMap::new();
for timestamp in timestamps {
let date = Utc
.timestamp_millis_opt(timestamp)
.single()
.ok_or_else(|| {
diesel::result::Error::DeserializationError("invalid timestamp".into())
})?;
*counts.entry((date.year(), date.month())).or_insert(0) += 1;
}
Ok(counts
.into_iter()
.rev()
.map(|((year, month), count)| (year, month, count))
.collect())
})
}
/// Collect all distinct tag values across media for a project.
pub fn distinct_media_tags(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<String>> {
let mut stmt =
conn.prepare("SELECT DISTINCT tags FROM media WHERE project_id = ?1 AND tags != '[]'")?;
let rows = stmt.query_map(params![project_id], |row| row.get::<_, String>(0))?;
pub fn distinct_media_tags(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<String>> {
let rows = conn.with(|c| {
media::table
.filter(media::project_id.eq(project_id))
.filter(media::tags.ne("[]"))
.select(media::tags)
.distinct()
.load::<String>(c)
})?;
let mut all_tags = std::collections::BTreeSet::new();
for json_str in rows {
if let Ok(json_str) = json_str
&& let Ok(tags) = serde_json::from_str::<Vec<String>>(&json_str)
{
if let Ok(tags) = serde_json::from_str::<Vec<String>>(&json_str) {
all_tags.extend(tags);
}
}
@@ -268,7 +226,7 @@ mod tests {
use crate::db::queries::project::{insert_project, make_test_project};
fn setup() -> Database {
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();
db

View File

@@ -1,92 +1,82 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use crate::db::from_row::{MEDIA_TRANSLATION_COLUMNS, media_translation_from_row};
use crate::db::DbConnection;
use crate::db::from_row::MediaTranslationRecord;
use crate::db::schema::media_translations;
use crate::model::MediaTranslation;
pub fn insert_media_translation(conn: &Connection, t: &MediaTranslation) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO media_translations (
id, project_id, translation_for, language, title, alt, caption,
created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![
t.id,
t.project_id,
t.translation_for,
t.language,
t.title,
t.alt,
t.caption,
t.created_at,
t.updated_at,
],
)?;
Ok(())
pub fn insert_media_translation(conn: &DbConnection, t: &MediaTranslation) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(media_translations::table)
.values(MediaTranslationRecord::from(t))
.execute(c)
.map(|_| ())
})
}
pub fn get_media_translation_by_media_and_language(
conn: &Connection,
conn: &DbConnection,
translation_for: &str,
language: &str,
) -> rusqlite::Result<MediaTranslation> {
conn.query_row(
&format!(
"SELECT {MEDIA_TRANSLATION_COLUMNS} FROM media_translations
WHERE translation_for = ?1 AND language = ?2"
),
params![translation_for, language],
media_translation_from_row,
)
) -> QueryResult<MediaTranslation> {
conn.with(|c| {
media_translations::table
.filter(media_translations::translation_for.eq(translation_for))
.filter(media_translations::language.eq(language))
.select(MediaTranslationRecord::as_select())
.first(c)
.map(Into::into)
})
}
pub fn list_media_translations_by_media(
conn: &Connection,
conn: &DbConnection,
translation_for: &str,
) -> rusqlite::Result<Vec<MediaTranslation>> {
let mut stmt = conn.prepare(&format!(
"SELECT {MEDIA_TRANSLATION_COLUMNS} FROM media_translations
WHERE translation_for = ?1 ORDER BY language"
))?;
let rows = stmt.query_map(params![translation_for], media_translation_from_row)?;
rows.collect()
) -> QueryResult<Vec<MediaTranslation>> {
conn.with(|c| {
media_translations::table
.filter(media_translations::translation_for.eq(translation_for))
.order(media_translations::language)
.select(MediaTranslationRecord::as_select())
.load(c)
.map(|rows: Vec<MediaTranslationRecord>| rows.into_iter().map(Into::into).collect())
})
}
pub fn upsert_media_translation(conn: &Connection, t: &MediaTranslation) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO media_translations (
id, project_id, translation_for, language, title, alt, caption,
created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
ON CONFLICT(translation_for, language) DO UPDATE SET
title = excluded.title,
alt = excluded.alt,
caption = excluded.caption,
updated_at = excluded.updated_at",
params![
t.id,
t.project_id,
t.translation_for,
t.language,
t.title,
t.alt,
t.caption,
t.created_at,
t.updated_at,
],
)?;
Ok(())
pub fn upsert_media_translation(conn: &DbConnection, t: &MediaTranslation) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(media_translations::table)
.values(MediaTranslationRecord::from(t))
.on_conflict((
media_translations::translation_for,
media_translations::language,
))
.do_update()
.set((
media_translations::title.eq(&t.title),
media_translations::alt.eq(&t.alt),
media_translations::caption.eq(&t.caption),
media_translations::updated_at.eq(t.updated_at),
))
.execute(c)
.map(|_| ())
})
}
pub fn delete_media_translation(
conn: &Connection,
conn: &DbConnection,
translation_for: &str,
language: &str,
) -> rusqlite::Result<()> {
conn.execute(
"DELETE FROM media_translations WHERE translation_for = ?1 AND language = ?2",
params![translation_for, language],
)?;
Ok(())
) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(
media_translations::table
.filter(media_translations::translation_for.eq(translation_for))
.filter(media_translations::language.eq(language)),
)
.execute(c)
.map(|_| ())
})
}
#[cfg(test)]
@@ -97,7 +87,7 @@ mod tests {
use crate::db::queries::project::{insert_project, make_test_project};
fn setup() -> Database {
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();
insert_media(db.conn(), &make_test_media("m1", "p1")).unwrap();

View File

@@ -1,169 +1,142 @@
use rusqlite::{Connection, params};
use chrono::{Datelike, TimeZone, Utc};
use diesel::prelude::*;
use diesel::sql_types::Text;
use diesel::sqlite::Sqlite;
use crate::db::from_row::{POST_COLUMNS, post_from_row, post_status_to_str};
use crate::db::DbConnection;
use crate::db::from_row::{PostRecord, convert, convert_all, post_status_to_str};
use crate::db::schema::posts;
use crate::model::{Post, PostStatus};
use crate::util::calendar_range_unix_ms;
fn tags_to_json(tags: &[String]) -> String {
serde_json::to_string(tags).unwrap_or_else(|_| "[]".into())
diesel::define_sql_function!(fn instr(haystack: Text, needle: Text) -> Integer);
diesel::define_sql_function!(fn lower(value: Text) -> Text);
pub fn insert_post(conn: &DbConnection, post: &Post) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(posts::table)
.values(PostRecord::from(post))
.execute(c)
.map(|_| ())
})
}
pub fn insert_post(conn: &Connection, post: &Post) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO posts (
id, project_id, title, slug, excerpt, content, status, author,
language, do_not_translate, template_slug, file_path, checksum,
tags, categories,
published_title, published_content, published_tags,
published_categories, published_excerpt,
created_at, updated_at, published_at
) VALUES (
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8,
?9, ?10, ?11, ?12, ?13,
?14, ?15,
?16, ?17, ?18, ?19, ?20,
?21, ?22, ?23
)",
params![
post.id,
post.project_id,
post.title,
post.slug,
post.excerpt,
post.content,
post_status_to_str(&post.status),
post.author,
post.language,
post.do_not_translate as i64,
post.template_slug,
post.file_path,
post.checksum,
tags_to_json(&post.tags),
tags_to_json(&post.categories),
post.published_title,
post.published_content,
post.published_tags,
post.published_categories,
post.published_excerpt,
post.created_at,
post.updated_at,
post.published_at,
],
)?;
Ok(())
}
pub fn get_post_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Post> {
conn.query_row(
&format!("SELECT {POST_COLUMNS} FROM posts WHERE id = ?1"),
params![id],
post_from_row,
)
pub fn get_post_by_id(conn: &DbConnection, id: &str) -> QueryResult<Post> {
conn.with(|c| {
posts::table
.filter(posts::id.eq(id))
.select(PostRecord::as_select())
.first(c)
.and_then(convert)
})
}
pub fn get_post_by_project_and_slug(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
slug: &str,
) -> rusqlite::Result<Post> {
conn.query_row(
&format!("SELECT {POST_COLUMNS} FROM posts WHERE project_id = ?1 AND slug = ?2"),
params![project_id, slug],
post_from_row,
)
) -> QueryResult<Post> {
conn.with(|c| {
posts::table
.filter(posts::project_id.eq(project_id))
.filter(posts::slug.eq(slug))
.select(PostRecord::as_select())
.first(c)
.and_then(convert)
})
}
pub fn list_posts_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<Post>> {
let mut stmt = conn.prepare(&format!(
"SELECT {POST_COLUMNS} FROM posts WHERE project_id = ?1 ORDER BY created_at DESC"
))?;
let rows = stmt.query_map(params![project_id], post_from_row)?;
rows.collect()
pub fn list_posts_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<Post>> {
conn.with(|c| {
posts::table
.filter(posts::project_id.eq(project_id))
.order(posts::created_at.desc())
.select(PostRecord::as_select())
.load(c)
.and_then(convert_all)
})
}
pub fn list_posts_by_project_limited(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
limit: i64,
offset: i64,
) -> rusqlite::Result<Vec<Post>> {
let mut stmt = conn.prepare(&format!(
"SELECT {POST_COLUMNS} FROM posts WHERE project_id = ?1 ORDER BY created_at DESC LIMIT ?2 OFFSET ?3"
))?;
let rows = stmt.query_map(params![project_id, limit, offset], post_from_row)?;
rows.collect()
) -> QueryResult<Vec<Post>> {
conn.with(|c| {
posts::table
.filter(posts::project_id.eq(project_id))
.order(posts::created_at.desc())
.limit(limit)
.offset(offset)
.select(PostRecord::as_select())
.load(c)
.and_then(convert_all)
})
}
pub fn update_post(conn: &Connection, post: &Post) -> rusqlite::Result<()> {
conn.execute(
"UPDATE posts SET
title = ?1, slug = ?2, excerpt = ?3, content = ?4, status = ?5,
author = ?6, language = ?7, do_not_translate = ?8, template_slug = ?9,
file_path = ?10, checksum = ?11, tags = ?12, categories = ?13,
published_title = ?14, published_content = ?15, published_tags = ?16,
published_categories = ?17, published_excerpt = ?18,
created_at = ?19, updated_at = ?20, published_at = ?21
WHERE id = ?22",
params![
post.title,
post.slug,
post.excerpt,
post.content,
post_status_to_str(&post.status),
post.author,
post.language,
post.do_not_translate as i64,
post.template_slug,
post.file_path,
post.checksum,
tags_to_json(&post.tags),
tags_to_json(&post.categories),
post.published_title,
post.published_content,
post.published_tags,
post.published_categories,
post.published_excerpt,
post.created_at,
post.updated_at,
post.published_at,
post.id,
],
)?;
Ok(())
pub fn update_post(conn: &DbConnection, post: &Post) -> QueryResult<()> {
conn.with(|c| {
diesel::update(posts::table.filter(posts::id.eq(&post.id)))
.set(PostRecord::from(post))
.execute(c)
.map(|_| ())
})
}
pub fn update_post_status(
conn: &Connection,
conn: &DbConnection,
id: &str,
status: &PostStatus,
updated_at: i64,
) -> rusqlite::Result<()> {
conn.execute(
"UPDATE posts SET status = ?1, updated_at = ?2 WHERE id = ?3",
params![post_status_to_str(status), updated_at, id],
)?;
Ok(())
) -> QueryResult<()> {
conn.with(|c| {
diesel::update(posts::table.filter(posts::id.eq(id)))
.set((
posts::status.eq(post_status_to_str(status)),
posts::updated_at.eq(updated_at),
))
.execute(c)
.map(|_| ())
})
}
pub fn clear_post_content(conn: &Connection, id: &str, updated_at: i64) -> rusqlite::Result<()> {
conn.execute(
"UPDATE posts SET content = NULL, updated_at = ?1 WHERE id = ?2",
params![updated_at, id],
)?;
Ok(())
pub fn clear_post_content(conn: &DbConnection, id: &str, updated_at: i64) -> QueryResult<()> {
conn.with(|c| {
diesel::update(posts::table.filter(posts::id.eq(id)))
.set((
posts::content.eq(None::<String>),
posts::updated_at.eq(updated_at),
))
.execute(c)
.map(|_| ())
})
}
pub fn set_post_file_path(
conn: &Connection,
conn: &DbConnection,
id: &str,
file_path: &str,
updated_at: i64,
) -> rusqlite::Result<()> {
conn.execute(
"UPDATE posts SET file_path = ?1, updated_at = ?2 WHERE id = ?3",
params![file_path, updated_at, id],
)?;
Ok(())
) -> QueryResult<()> {
conn.with(|c| {
diesel::update(posts::table.filter(posts::id.eq(id)))
.set((
posts::file_path.eq(file_path),
posts::updated_at.eq(updated_at),
))
.execute(c)
.map(|_| ())
})
}
pub fn set_post_checksum(conn: &DbConnection, id: &str, checksum: Option<&str>) -> QueryResult<()> {
conn.with(|c| {
diesel::update(posts::table.filter(posts::id.eq(id)))
.set(posts::checksum.eq(checksum))
.execute(c)
.map(|_| ())
})
}
#[expect(
@@ -171,7 +144,7 @@ pub fn set_post_file_path(
reason = "arguments mirror the published snapshot columns"
)]
pub fn set_published_snapshot(
conn: &Connection,
conn: &DbConnection,
id: &str,
title: &str,
content: &str,
@@ -180,38 +153,38 @@ pub fn set_published_snapshot(
excerpt: Option<&str>,
published_at: i64,
updated_at: i64,
) -> rusqlite::Result<()> {
conn.execute(
"UPDATE posts SET
published_title = ?1, published_content = ?2, published_tags = ?3,
published_categories = ?4, published_excerpt = ?5,
published_at = ?6, updated_at = ?7
WHERE id = ?8",
params![
title,
content,
tags,
categories,
excerpt,
published_at,
updated_at,
id
],
)?;
Ok(())
) -> QueryResult<()> {
conn.with(|c| {
diesel::update(posts::table.filter(posts::id.eq(id)))
.set((
posts::published_title.eq(title),
posts::published_content.eq(content),
posts::published_tags.eq(tags),
posts::published_categories.eq(categories),
posts::published_excerpt.eq(excerpt),
posts::published_at.eq(published_at),
posts::updated_at.eq(updated_at),
))
.execute(c)
.map(|_| ())
})
}
pub fn delete_post(conn: &Connection, id: &str) -> rusqlite::Result<()> {
conn.execute("DELETE FROM posts WHERE id = ?1", params![id])?;
Ok(())
pub fn delete_post(conn: &DbConnection, id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(posts::table.filter(posts::id.eq(id)))
.execute(c)
.map(|_| ())
})
}
pub fn count_posts_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<i64> {
conn.query_row(
"SELECT COUNT(*) FROM posts WHERE project_id = ?1",
params![project_id],
|row| row.get(0),
)
pub fn count_posts_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<i64> {
conn.with(|c| {
posts::table
.filter(posts::project_id.eq(project_id))
.count()
.get_result(c)
})
}
// ── Filtered queries (per sidebar_views.allium PostsView) ───
@@ -266,163 +239,155 @@ impl PostFilterParams {
///
/// Returns all matching posts (up to `limit`), ordered by created_at DESC.
/// Caller splits into draft/published/archived sections.
fn post_query<'a>(
project_id: &'a str,
filters: &'a PostFilterParams,
apply_filters: bool,
exclude_drafts: bool,
) -> posts::BoxedQuery<'a, Sqlite> {
let mut query = posts::table
.filter(posts::project_id.eq(project_id))
.into_boxed();
let page = serde_json::to_string("page").unwrap();
if filters.pages_only {
query = query.filter(instr(lower(posts::categories), page.clone()).gt(0));
} else if filters.exclude_pages {
query = query.filter(instr(lower(posts::categories), page).eq(0));
}
if exclude_drafts {
query = query.filter(posts::status.ne("draft"));
}
if !apply_filters {
return query;
}
if !filters.search_query.is_empty() {
query = query.filter(instr(posts::title, &filters.search_query).gt(0));
}
if let Some(status) = &filters.status {
query = query.filter(posts::status.eq(status));
}
if let Some(language) = &filters.language {
query = query.filter(posts::language.eq(language).or(posts::language.is_null()));
}
if let Some(year) = filters.year
&& let Some((start, end)) = calendar_range_unix_ms(year, filters.month)
{
query = query.filter(posts::created_at.ge(start).and(posts::created_at.lt(end)));
}
for tag in &filters.tags {
query = query.filter(instr(posts::tags, serde_json::to_string(tag).unwrap()).gt(0));
}
for category in &filters.categories {
query =
query.filter(instr(posts::categories, serde_json::to_string(category).unwrap()).gt(0));
}
if let Some(from) = filters.from {
query = query.filter(posts::created_at.ge(from));
}
if let Some(to) = filters.to {
query = query.filter(posts::created_at.le(to));
}
query
}
pub fn list_posts_filtered(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
filters: &PostFilterParams,
limit: i64,
offset: i64,
) -> rusqlite::Result<Vec<Post>> {
// Build dynamic WHERE clause
let mut conditions = vec!["p.project_id = ?1".to_string()];
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
param_values.push(Box::new(project_id.to_string()));
// pages_only / exclude_pages
if filters.pages_only {
conditions.push("LOWER(p.categories) LIKE '%\"page\"%'".to_string());
} else if filters.exclude_pages {
conditions.push("LOWER(p.categories) NOT LIKE '%\"page\"%'".to_string());
}
// For non-draft posts, apply filters. Drafts always pass.
// We build this as: (status = 'draft') OR (filter conditions)
let mut filter_conditions: Vec<String> = Vec::new();
if !filters.search_query.is_empty() {
let idx = param_values.len() + 1;
let pattern = format!("%{}%", filters.search_query.replace('%', "\\%"));
filter_conditions.push(format!("(p.title LIKE ?{idx} ESCAPE '\\')"));
param_values.push(Box::new(pattern));
}
if let Some(status) = &filters.status {
let idx = param_values.len() + 1;
filter_conditions.push(format!("(p.status = ?{idx})"));
param_values.push(Box::new(status.clone()));
}
if let Some(language) = &filters.language {
let idx = param_values.len() + 1;
filter_conditions.push(format!("(p.language = ?{idx} OR p.language IS NULL)"));
param_values.push(Box::new(language.clone()));
}
if let Some(year) = filters.year {
let (start, end) =
calendar_range_unix_ms(year, filters.month).ok_or(rusqlite::Error::InvalidQuery)?;
let idx1 = param_values.len() + 1;
let idx2 = param_values.len() + 2;
filter_conditions.push(format!(
"(p.created_at >= ?{idx1} AND p.created_at < ?{idx2})"
));
param_values.push(Box::new(start));
param_values.push(Box::new(end));
}
for tag in &filters.tags {
let idx = param_values.len() + 1;
let pattern = format!("%\"{}\"%", tag.replace('"', "\\\""));
filter_conditions.push(format!("(p.tags LIKE ?{idx})"));
param_values.push(Box::new(pattern));
}
for cat in &filters.categories {
let idx = param_values.len() + 1;
let pattern = format!("%\"{}\"%", cat.replace('"', "\\\""));
filter_conditions.push(format!("(p.categories LIKE ?{idx})"));
param_values.push(Box::new(pattern));
}
if let Some(from) = filters.from {
let idx = param_values.len() + 1;
filter_conditions.push(format!("(p.created_at >= ?{idx})"));
param_values.push(Box::new(from));
}
if let Some(to) = filters.to {
let idx = param_values.len() + 1;
filter_conditions.push(format!("(p.created_at <= ?{idx})"));
param_values.push(Box::new(to));
}
// Without an explicit status filter, drafts remain visible regardless of the
// rest of the active filters.
if !filter_conditions.is_empty() {
let combined = filter_conditions.join(" AND ");
if filters.status.is_some() {
conditions.push(format!("({combined})"));
) -> QueryResult<Vec<Post>> {
conn.with(|c| {
let content_filters = filters.has_active_filters();
let records = if filters.status.is_some() {
post_query(project_id, filters, true, false)
.order(posts::created_at.desc())
.limit(limit)
.offset(offset)
.select(PostRecord::as_select())
.load(c)?
} else if content_filters {
let mut records: Vec<PostRecord> = post_query(project_id, filters, false, false)
.filter(posts::status.eq("draft"))
.select(PostRecord::as_select())
.load(c)?;
records.extend(
post_query(project_id, filters, true, true)
.select(PostRecord::as_select())
.load::<PostRecord>(c)?,
);
records.sort_unstable_by_key(|record| std::cmp::Reverse(record.created_at));
records
.into_iter()
.skip(offset.max(0) as usize)
.take(limit.max(0) as usize)
.collect()
} else {
conditions.push(format!("(p.status = 'draft' OR ({combined}))"));
}
}
let where_clause = conditions.join(" AND ");
let idx_limit = param_values.len() + 1;
let idx_offset = param_values.len() + 2;
param_values.push(Box::new(limit));
param_values.push(Box::new(offset));
let sql = format!(
"SELECT {POST_COLUMNS} FROM posts p WHERE {where_clause} ORDER BY p.created_at DESC LIMIT ?{idx_limit} OFFSET ?{idx_offset}"
);
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
param_values.iter().map(|b| b.as_ref()).collect();
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(params_refs.as_slice(), post_from_row)?;
rows.collect()
post_query(project_id, filters, false, false)
.order(posts::created_at.desc())
.limit(limit)
.offset(offset)
.select(PostRecord::as_select())
.load(c)?
};
convert_all(records)
})
}
/// Year/month counts for the calendar archive widget.
/// Returns (year, month, count) tuples, ordered by year DESC, month DESC.
pub fn post_calendar_counts(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
pages_only: bool,
exclude_pages: bool,
) -> rusqlite::Result<Vec<(i32, u32, usize)>> {
let page_filter = if pages_only {
" AND LOWER(categories) LIKE '%\"page\"%'"
} else if exclude_pages {
" AND LOWER(categories) NOT LIKE '%\"page\"%'"
} else {
""
};
let sql = format!(
"SELECT
CAST(strftime('%Y', created_at / 1000, 'unixepoch') AS INTEGER) AS y,
CAST(strftime('%m', created_at / 1000, 'unixepoch') AS INTEGER) AS m,
COUNT(*) AS cnt
FROM posts
WHERE project_id = ?1{page_filter}
GROUP BY y, m
ORDER BY y DESC, m DESC"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map(params![project_id], |row| {
Ok((
row.get::<_, i32>(0)?,
row.get::<_, u32>(1)?,
row.get::<_, usize>(2)?,
))
})?;
rows.collect()
) -> QueryResult<Vec<(i32, u32, usize)>> {
conn.with(|c| {
let rows = posts::table
.filter(posts::project_id.eq(project_id))
.select((posts::created_at, posts::categories))
.load::<(i64, String)>(c)?;
let mut counts = std::collections::BTreeMap::new();
for (timestamp, categories) in rows {
let categories: Vec<String> = serde_json::from_str(&categories).unwrap_or_default();
let is_page = categories
.iter()
.any(|category| category.eq_ignore_ascii_case("page"));
if (pages_only && !is_page) || (exclude_pages && is_page) {
continue;
}
let date = Utc
.timestamp_millis_opt(timestamp)
.single()
.ok_or_else(|| {
diesel::result::Error::DeserializationError("invalid timestamp".into())
})?;
*counts.entry((date.year(), date.month())).or_insert(0) += 1;
}
Ok(counts
.into_iter()
.rev()
.map(|((year, month), count)| (year, month, count))
.collect())
})
}
/// Collect all distinct tag values across posts for a project.
pub fn distinct_post_tags(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<String>> {
let mut stmt =
conn.prepare("SELECT DISTINCT tags FROM posts WHERE project_id = ?1 AND tags != '[]'")?;
let rows = stmt.query_map(params![project_id], |row| row.get::<_, String>(0))?;
pub fn distinct_post_tags(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<String>> {
let rows = conn.with(|c| {
posts::table
.filter(posts::project_id.eq(project_id))
.filter(posts::tags.ne("[]"))
.select(posts::tags)
.distinct()
.load::<String>(c)
})?;
let mut all_tags = std::collections::BTreeSet::new();
for json_str in rows {
if let Ok(json_str) = json_str
&& let Ok(tags) = serde_json::from_str::<Vec<String>>(&json_str)
{
if let Ok(tags) = serde_json::from_str::<Vec<String>>(&json_str) {
all_tags.extend(tags);
}
}
@@ -430,25 +395,53 @@ pub fn distinct_post_tags(conn: &Connection, project_id: &str) -> rusqlite::Resu
}
/// Collect all distinct category values across posts for a project.
pub fn distinct_post_categories(
conn: &Connection,
project_id: &str,
) -> rusqlite::Result<Vec<String>> {
let mut stmt = conn.prepare(
"SELECT DISTINCT categories FROM posts WHERE project_id = ?1 AND categories != '[]'",
)?;
let rows = stmt.query_map(params![project_id], |row| row.get::<_, String>(0))?;
pub fn distinct_post_categories(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<String>> {
let rows = conn.with(|c| {
posts::table
.filter(posts::project_id.eq(project_id))
.filter(posts::categories.ne("[]"))
.select(posts::categories)
.distinct()
.load::<String>(c)
})?;
let mut all_cats = std::collections::BTreeSet::new();
for json_str in rows {
if let Ok(json_str) = json_str
&& let Ok(cats) = serde_json::from_str::<Vec<String>>(&json_str)
{
if let Ok(cats) = serde_json::from_str::<Vec<String>>(&json_str) {
all_cats.extend(cats);
}
}
Ok(all_cats.into_iter().collect())
}
#[cfg(test)]
pub fn make_test_post(id: &str, project_id: &str, slug: &str) -> Post {
Post {
id: id.into(),
project_id: project_id.into(),
title: id.into(),
slug: slug.into(),
excerpt: None,
content: None,
status: PostStatus::Draft,
author: None,
language: None,
do_not_translate: false,
template_slug: None,
file_path: String::new(),
checksum: None,
tags: Vec::new(),
categories: Vec::new(),
published_title: None,
published_content: None,
published_tags: None,
published_categories: None,
published_excerpt: None,
created_at: 1000,
updated_at: 1000,
published_at: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -456,7 +449,7 @@ mod tests {
use crate::db::queries::project::{insert_project, make_test_project};
fn setup() -> Database {
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();
db

View File

@@ -1,82 +1,77 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use crate::db::from_row::{POST_LINK_COLUMNS, post_link_from_row};
use crate::db::DbConnection;
use crate::db::from_row::PostLinkRecord;
use crate::db::schema::post_links;
use crate::model::PostLink;
pub fn insert_post_link(conn: &Connection, link: &PostLink) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO post_links (id, source_post_id, target_post_id, link_text, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![
link.id,
link.source_post_id,
link.target_post_id,
link.link_text,
link.created_at,
],
)?;
Ok(())
pub fn insert_post_link(conn: &DbConnection, link: &PostLink) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(post_links::table)
.values(PostLinkRecord::from(link))
.execute(c)
.map(|_| ())
})
}
pub fn delete_links_by_source(conn: &Connection, source_post_id: &str) -> rusqlite::Result<()> {
conn.execute(
"DELETE FROM post_links WHERE source_post_id = ?1",
params![source_post_id],
)?;
Ok(())
pub fn delete_links_by_source(conn: &DbConnection, source_post_id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(post_links::table.filter(post_links::source_post_id.eq(source_post_id)))
.execute(c)
.map(|_| ())
})
}
pub fn delete_links_by_target(conn: &DbConnection, target_post_id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(post_links::table.filter(post_links::target_post_id.eq(target_post_id)))
.execute(c)
.map(|_| ())
})
}
pub fn list_links_by_source(
conn: &Connection,
conn: &DbConnection,
source_post_id: &str,
) -> rusqlite::Result<Vec<PostLink>> {
let mut stmt = conn.prepare(&format!(
"SELECT {POST_LINK_COLUMNS} FROM post_links WHERE source_post_id = ?1 ORDER BY created_at"
))?;
let rows = stmt.query_map(params![source_post_id], post_link_from_row)?;
rows.collect()
) -> QueryResult<Vec<PostLink>> {
conn.with(|c| {
post_links::table
.filter(post_links::source_post_id.eq(source_post_id))
.order(post_links::created_at)
.select(PostLinkRecord::as_select())
.load(c)
.map(|rows: Vec<PostLinkRecord>| rows.into_iter().map(Into::into).collect())
})
}
pub fn list_links_by_target(
conn: &Connection,
conn: &DbConnection,
target_post_id: &str,
) -> rusqlite::Result<Vec<PostLink>> {
let mut stmt = conn.prepare(&format!(
"SELECT {POST_LINK_COLUMNS} FROM post_links WHERE target_post_id = ?1 ORDER BY created_at"
))?;
let rows = stmt.query_map(params![target_post_id], post_link_from_row)?;
rows.collect()
) -> QueryResult<Vec<PostLink>> {
conn.with(|c| {
post_links::table
.filter(post_links::target_post_id.eq(target_post_id))
.order(post_links::created_at)
.select(PostLinkRecord::as_select())
.load(c)
.map(|rows: Vec<PostLinkRecord>| rows.into_iter().map(Into::into).collect())
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::Database;
use crate::db::queries::post::{insert_post, make_test_post};
use crate::db::queries::project::{insert_project, make_test_project};
fn setup() -> Database {
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 c = db.conn();
c.execute(
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
VALUES ('a', 'p1', 'A', 'a', 'draft', 1000, 1000)",
[],
)
.unwrap();
c.execute(
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
VALUES ('b', 'p1', 'B', 'b', 'draft', 1000, 1000)",
[],
)
.unwrap();
c.execute(
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
VALUES ('c', 'p1', 'C', 'c', 'draft', 1000, 1000)",
[],
)
.unwrap();
insert_post(db.conn(), &make_test_post("a", "p1", "a")).unwrap();
insert_post(db.conn(), &make_test_post("b", "p1", "b")).unwrap();
insert_post(db.conn(), &make_test_post("c", "p1", "c")).unwrap();
db
}

View File

@@ -1,65 +1,80 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use crate::db::from_row::{POST_MEDIA_COLUMNS, post_media_from_row};
use crate::db::DbConnection;
use crate::db::from_row::PostMediaRecord;
use crate::db::schema::post_media;
use crate::model::PostMedia;
pub fn link_media(conn: &Connection, pm: &PostMedia) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO post_media (id, project_id, post_id, media_id, sort_order, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![
pm.id,
pm.project_id,
pm.post_id,
pm.media_id,
pm.sort_order,
pm.created_at,
],
)?;
Ok(())
pub fn link_media(conn: &DbConnection, pm: &PostMedia) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(post_media::table)
.values(PostMediaRecord::from(pm))
.execute(c)
.map(|_| ())
})
}
pub fn unlink_media(conn: &Connection, post_id: &str, media_id: &str) -> rusqlite::Result<()> {
conn.execute(
"DELETE FROM post_media WHERE post_id = ?1 AND media_id = ?2",
params![post_id, media_id],
)?;
Ok(())
pub fn unlink_media(conn: &DbConnection, post_id: &str, media_id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(
post_media::table
.filter(post_media::post_id.eq(post_id))
.filter(post_media::media_id.eq(media_id)),
)
.execute(c)
.map(|_| ())
})
}
pub fn list_post_media_by_post(
conn: &Connection,
post_id: &str,
) -> rusqlite::Result<Vec<PostMedia>> {
let mut stmt = conn.prepare(&format!(
"SELECT {POST_MEDIA_COLUMNS} FROM post_media WHERE post_id = ?1 ORDER BY sort_order"
))?;
let rows = stmt.query_map(params![post_id], post_media_from_row)?;
rows.collect()
pub fn delete_post_media_by_post(conn: &DbConnection, post_id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(post_media::table.filter(post_media::post_id.eq(post_id)))
.execute(c)
.map(|_| ())
})
}
pub fn list_post_media_by_post(conn: &DbConnection, post_id: &str) -> QueryResult<Vec<PostMedia>> {
conn.with(|c| {
post_media::table
.filter(post_media::post_id.eq(post_id))
.order(post_media::sort_order)
.select(PostMediaRecord::as_select())
.load(c)
.map(|rows: Vec<PostMediaRecord>| rows.into_iter().map(Into::into).collect())
})
}
pub fn list_post_media_by_media(
conn: &Connection,
conn: &DbConnection,
media_id: &str,
) -> rusqlite::Result<Vec<PostMedia>> {
let mut stmt = conn.prepare(&format!(
"SELECT {POST_MEDIA_COLUMNS} FROM post_media WHERE media_id = ?1 ORDER BY created_at"
))?;
let rows = stmt.query_map(params![media_id], post_media_from_row)?;
rows.collect()
) -> QueryResult<Vec<PostMedia>> {
conn.with(|c| {
post_media::table
.filter(post_media::media_id.eq(media_id))
.order(post_media::created_at)
.select(PostMediaRecord::as_select())
.load(c)
.map(|rows: Vec<PostMediaRecord>| rows.into_iter().map(Into::into).collect())
})
}
pub fn update_sort_order(
conn: &Connection,
conn: &DbConnection,
post_id: &str,
media_id: &str,
sort_order: i32,
) -> rusqlite::Result<()> {
conn.execute(
"UPDATE post_media SET sort_order = ?1 WHERE post_id = ?2 AND media_id = ?3",
params![sort_order, post_id, media_id],
)?;
Ok(())
) -> QueryResult<()> {
conn.with(|c| {
diesel::update(
post_media::table
.filter(post_media::post_id.eq(post_id))
.filter(post_media::media_id.eq(media_id)),
)
.set(post_media::sort_order.eq(sort_order))
.execute(c)
.map(|_| ())
})
}
#[cfg(test)]
@@ -67,19 +82,14 @@ mod tests {
use super::*;
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::project::{insert_project, make_test_project};
fn setup() -> Database {
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();
db.conn()
.execute(
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
VALUES ('post1', 'p1', 'Hello', 'hello', 'draft', 1000, 1000)",
[],
)
.unwrap();
insert_post(db.conn(), &make_test_post("post1", "p1", "hello")).unwrap();
insert_media(db.conn(), &make_test_media("m1", "p1")).unwrap();
insert_media(db.conn(), &make_test_media("m2", "p1")).unwrap();
db

View File

@@ -1,139 +1,111 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use crate::db::from_row::{
POST_TRANSLATION_COLUMNS, post_status_to_str, post_translation_from_row,
};
use crate::db::DbConnection;
use crate::db::from_row::{PostTranslationRecord, convert, convert_all};
use crate::db::schema::post_translations;
use crate::model::PostTranslation;
pub fn insert_post_translation(conn: &Connection, t: &PostTranslation) -> rusqlite::Result<()> {
pub fn insert_post_translation(conn: &DbConnection, t: &PostTranslation) -> QueryResult<()> {
if !t.status.is_valid_for_translation() {
return Err(rusqlite::Error::InvalidParameterName(
"translation status must be draft or published".to_string(),
return Err(diesel::result::Error::SerializationError(
"translation status must be draft or published".into(),
));
}
conn.execute(
"INSERT INTO post_translations (
id, project_id, translation_for, language, title, excerpt, content,
status, file_path, checksum, created_at, updated_at, published_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
params![
t.id,
t.project_id,
t.translation_for,
t.language,
t.title,
t.excerpt,
t.content,
post_status_to_str(&t.status),
t.file_path,
t.checksum,
t.created_at,
t.updated_at,
t.published_at,
],
)?;
Ok(())
conn.with(|c| {
diesel::insert_into(post_translations::table)
.values(PostTranslationRecord::from(t))
.execute(c)
.map(|_| ())
})
}
pub fn get_post_translation_by_id(
conn: &Connection,
id: &str,
) -> rusqlite::Result<PostTranslation> {
conn.query_row(
&format!("SELECT {POST_TRANSLATION_COLUMNS} FROM post_translations WHERE id = ?1"),
params![id],
post_translation_from_row,
)
pub fn get_post_translation_by_id(conn: &DbConnection, id: &str) -> QueryResult<PostTranslation> {
conn.with(|c| {
post_translations::table
.filter(post_translations::id.eq(id))
.select(PostTranslationRecord::as_select())
.first(c)
.and_then(convert)
})
}
pub fn get_post_translation_by_post_and_language(
conn: &Connection,
conn: &DbConnection,
translation_for: &str,
language: &str,
) -> rusqlite::Result<PostTranslation> {
conn.query_row(
&format!(
"SELECT {POST_TRANSLATION_COLUMNS} FROM post_translations
WHERE translation_for = ?1 AND language = ?2"
),
params![translation_for, language],
post_translation_from_row,
)
) -> QueryResult<PostTranslation> {
conn.with(|c| {
post_translations::table
.filter(post_translations::translation_for.eq(translation_for))
.filter(post_translations::language.eq(language))
.select(PostTranslationRecord::as_select())
.first(c)
.and_then(convert)
})
}
pub fn list_post_translations_by_post(
conn: &Connection,
conn: &DbConnection,
translation_for: &str,
) -> rusqlite::Result<Vec<PostTranslation>> {
let mut stmt = conn.prepare(&format!(
"SELECT {POST_TRANSLATION_COLUMNS} FROM post_translations
WHERE translation_for = ?1 ORDER BY language"
))?;
let rows = stmt.query_map(params![translation_for], post_translation_from_row)?;
rows.collect()
) -> QueryResult<Vec<PostTranslation>> {
conn.with(|c| {
post_translations::table
.filter(post_translations::translation_for.eq(translation_for))
.order(post_translations::language)
.select(PostTranslationRecord::as_select())
.load(c)
.and_then(convert_all)
})
}
pub fn update_post_translation(conn: &Connection, t: &PostTranslation) -> rusqlite::Result<()> {
pub fn update_post_translation(conn: &DbConnection, t: &PostTranslation) -> QueryResult<()> {
if !t.status.is_valid_for_translation() {
return Err(rusqlite::Error::InvalidParameterName(
"translation status must be draft or published".to_string(),
return Err(diesel::result::Error::SerializationError(
"translation status must be draft or published".into(),
));
}
conn.execute(
"UPDATE post_translations SET
title = ?1, excerpt = ?2, content = ?3, status = ?4,
file_path = ?5, checksum = ?6, updated_at = ?7, published_at = ?8
WHERE id = ?9",
params![
t.title,
t.excerpt,
t.content,
post_status_to_str(&t.status),
t.file_path,
t.checksum,
t.updated_at,
t.published_at,
t.id,
],
)?;
Ok(())
conn.with(|c| {
diesel::update(post_translations::table.filter(post_translations::id.eq(&t.id)))
.set(PostTranslationRecord::from(t))
.execute(c)
.map(|_| ())
})
}
pub fn delete_post_translation(conn: &Connection, id: &str) -> rusqlite::Result<()> {
conn.execute("DELETE FROM post_translations WHERE id = ?1", params![id])?;
Ok(())
pub fn delete_post_translation(conn: &DbConnection, id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(post_translations::table.filter(post_translations::id.eq(id)))
.execute(c)
.map(|_| ())
})
}
pub fn delete_all_translations_for_post(
conn: &Connection,
conn: &DbConnection,
translation_for: &str,
) -> rusqlite::Result<()> {
conn.execute(
"DELETE FROM post_translations WHERE translation_for = ?1",
params![translation_for],
)?;
Ok(())
) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(
post_translations::table.filter(post_translations::translation_for.eq(translation_for)),
)
.execute(c)
.map(|_| ())
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::Database;
use crate::db::queries::post::{insert_post, make_test_post};
use crate::db::queries::project::{insert_project, make_test_project};
use crate::model::PostStatus;
fn setup() -> Database {
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();
// insert a parent post
db.conn()
.execute(
"INSERT INTO posts (id, project_id, title, slug, status, created_at, updated_at)
VALUES ('post1', 'p1', 'Hello', 'hello', 'draft', 1000, 1000)",
[],
)
.unwrap();
insert_post(db.conn(), &make_test_post("post1", "p1", "hello")).unwrap();
db
}

View File

@@ -1,90 +1,88 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use crate::db::from_row::{PROJECT_COLUMNS, project_from_row};
use crate::db::DbConnection;
use crate::db::from_row::ProjectRecord;
use crate::db::schema::projects;
use crate::model::Project;
pub fn insert_project(conn: &Connection, project: &Project) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO projects (id, name, slug, description, data_path, is_active, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
project.id,
project.name,
project.slug,
project.description,
project.data_path,
project.is_active as i64,
project.created_at,
project.updated_at,
],
)?;
Ok(())
pub fn insert_project(conn: &DbConnection, project: &Project) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(projects::table)
.values(ProjectRecord::from(project))
.execute(c)
.map(|_| ())
})
}
pub fn get_project_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Project> {
conn.query_row(
&format!("SELECT {PROJECT_COLUMNS} FROM projects WHERE id = ?1"),
params![id],
project_from_row,
)
pub fn get_project_by_id(conn: &DbConnection, id: &str) -> QueryResult<Project> {
conn.with(|c| {
projects::table
.filter(projects::id.eq(id))
.select(ProjectRecord::as_select())
.first(c)
.map(Into::into)
})
}
pub fn get_project_by_slug(conn: &Connection, slug: &str) -> rusqlite::Result<Project> {
conn.query_row(
&format!("SELECT {PROJECT_COLUMNS} FROM projects WHERE slug = ?1"),
params![slug],
project_from_row,
)
pub fn get_project_by_slug(conn: &DbConnection, slug: &str) -> QueryResult<Project> {
conn.with(|c| {
projects::table
.filter(projects::slug.eq(slug))
.select(ProjectRecord::as_select())
.first(c)
.map(Into::into)
})
}
pub fn get_active_project(conn: &Connection) -> rusqlite::Result<Project> {
conn.query_row(
&format!("SELECT {PROJECT_COLUMNS} FROM projects WHERE is_active = 1 LIMIT 1"),
[],
project_from_row,
)
pub fn get_active_project(conn: &DbConnection) -> QueryResult<Project> {
conn.with(|c| {
projects::table
.filter(projects::is_active.eq(1))
.select(ProjectRecord::as_select())
.first(c)
.map(Into::into)
})
}
pub fn set_active_project(conn: &Connection, id: &str) -> rusqlite::Result<()> {
let tx = conn.unchecked_transaction()?;
tx.execute("UPDATE projects SET is_active = 0 WHERE is_active = 1", [])?;
tx.execute(
"UPDATE projects SET is_active = 1 WHERE id = ?1",
params![id],
)?;
tx.commit()?;
Ok(())
pub fn set_active_project(conn: &DbConnection, id: &str) -> QueryResult<()> {
conn.with(|c| {
c.transaction(|c| {
diesel::update(projects::table.filter(projects::is_active.eq(1)))
.set(projects::is_active.eq(0))
.execute(c)?;
diesel::update(projects::table.filter(projects::id.eq(id)))
.set(projects::is_active.eq(1))
.execute(c)?;
Ok(())
})
})
}
pub fn list_projects(conn: &Connection) -> rusqlite::Result<Vec<Project>> {
let mut stmt = conn.prepare(&format!(
"SELECT {PROJECT_COLUMNS} FROM projects ORDER BY name"
))?;
let rows = stmt.query_map([], project_from_row)?;
rows.collect()
pub fn list_projects(conn: &DbConnection) -> QueryResult<Vec<Project>> {
conn.with(|c| {
projects::table
.order(projects::name)
.select(ProjectRecord::as_select())
.load(c)
.map(|rows: Vec<ProjectRecord>| rows.into_iter().map(Into::into).collect())
})
}
pub fn update_project(conn: &Connection, project: &Project) -> rusqlite::Result<()> {
conn.execute(
"UPDATE projects SET name = ?1, slug = ?2, description = ?3, data_path = ?4,
is_active = ?5, updated_at = ?6
WHERE id = ?7",
params![
project.name,
project.slug,
project.description,
project.data_path,
project.is_active as i64,
project.updated_at,
project.id,
],
)?;
Ok(())
pub fn update_project(conn: &DbConnection, project: &Project) -> QueryResult<()> {
conn.with(|c| {
diesel::update(projects::table.filter(projects::id.eq(&project.id)))
.set(ProjectRecord::from(project))
.execute(c)
.map(|_| ())
})
}
pub fn delete_project(conn: &Connection, id: &str) -> rusqlite::Result<()> {
conn.execute("DELETE FROM projects WHERE id = ?1", params![id])?;
Ok(())
pub fn delete_project(conn: &DbConnection, id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(projects::table.filter(projects::id.eq(id)))
.execute(c)
.map(|_| ())
})
}
/// Test helper: create a minimal Project value (available to sibling test modules).
@@ -108,7 +106,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

@@ -1,92 +1,70 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use crate::db::from_row::{
SCRIPT_COLUMNS, script_from_row, script_kind_to_str, script_status_to_str,
};
use crate::db::DbConnection;
use crate::db::from_row::{ScriptRecord, convert, convert_all};
use crate::db::schema::scripts;
use crate::model::Script;
pub fn insert_script(conn: &Connection, s: &Script) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO scripts (
id, project_id, slug, title, kind, entrypoint, enabled, version,
file_path, status, content, created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
params![
s.id,
s.project_id,
s.slug,
s.title,
script_kind_to_str(&s.kind),
s.entrypoint,
s.enabled as i64,
s.version,
s.file_path,
script_status_to_str(&s.status),
s.content,
s.created_at,
s.updated_at,
],
)?;
Ok(())
pub fn insert_script(conn: &DbConnection, s: &Script) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(scripts::table)
.values(ScriptRecord::from(s))
.execute(c)
.map(|_| ())
})
}
pub fn get_script_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Script> {
conn.query_row(
&format!("SELECT {SCRIPT_COLUMNS} FROM scripts WHERE id = ?1"),
params![id],
script_from_row,
)
pub fn get_script_by_id(conn: &DbConnection, id: &str) -> QueryResult<Script> {
conn.with(|c| {
scripts::table
.filter(scripts::id.eq(id))
.select(ScriptRecord::as_select())
.first(c)
.and_then(convert)
})
}
pub fn get_script_by_slug(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
slug: &str,
) -> rusqlite::Result<Script> {
conn.query_row(
&format!("SELECT {SCRIPT_COLUMNS} FROM scripts WHERE project_id = ?1 AND slug = ?2"),
params![project_id, slug],
script_from_row,
)
) -> QueryResult<Script> {
conn.with(|c| {
scripts::table
.filter(scripts::project_id.eq(project_id))
.filter(scripts::slug.eq(slug))
.select(ScriptRecord::as_select())
.first(c)
.and_then(convert)
})
}
pub fn list_scripts_by_project(
conn: &Connection,
project_id: &str,
) -> rusqlite::Result<Vec<Script>> {
let mut stmt = conn.prepare(&format!(
"SELECT {SCRIPT_COLUMNS} FROM scripts WHERE project_id = ?1 ORDER BY title"
))?;
let rows = stmt.query_map(params![project_id], script_from_row)?;
rows.collect()
pub fn list_scripts_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<Script>> {
conn.with(|c| {
scripts::table
.filter(scripts::project_id.eq(project_id))
.order(scripts::title)
.select(ScriptRecord::as_select())
.load(c)
.and_then(convert_all)
})
}
pub fn update_script(conn: &Connection, s: &Script) -> rusqlite::Result<()> {
conn.execute(
"UPDATE scripts SET
slug = ?1, title = ?2, kind = ?3, entrypoint = ?4, enabled = ?5,
version = ?6, file_path = ?7, status = ?8, content = ?9, updated_at = ?10
WHERE id = ?11",
params![
s.slug,
s.title,
script_kind_to_str(&s.kind),
s.entrypoint,
s.enabled as i64,
s.version,
s.file_path,
script_status_to_str(&s.status),
s.content,
s.updated_at,
s.id,
],
)?;
Ok(())
pub fn update_script(conn: &DbConnection, s: &Script) -> QueryResult<()> {
conn.with(|c| {
diesel::update(scripts::table.filter(scripts::id.eq(&s.id)))
.set(ScriptRecord::from(s))
.execute(c)
.map(|_| ())
})
}
pub fn delete_script(conn: &Connection, id: &str) -> rusqlite::Result<()> {
conn.execute("DELETE FROM scripts WHERE id = ?1", params![id])?;
Ok(())
pub fn delete_script(conn: &DbConnection, id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(scripts::table.filter(scripts::id.eq(id)))
.execute(c)
.map(|_| ())
})
}
#[cfg(test)]
@@ -97,7 +75,7 @@ mod tests {
use crate::model::{ScriptKind, ScriptStatus};
fn setup() -> Database {
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();
db

View File

@@ -1,36 +1,52 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use crate::db::from_row::{SETTING_COLUMNS, setting_from_row};
use crate::db::DbConnection;
use crate::db::from_row::SettingRecord;
use crate::db::schema::settings;
use crate::model::Setting;
pub fn get_setting_by_key(conn: &Connection, key: &str) -> rusqlite::Result<Setting> {
conn.query_row(
&format!("SELECT {SETTING_COLUMNS} FROM settings WHERE key = ?1"),
params![key],
setting_from_row,
)
pub fn get_setting_by_key(conn: &DbConnection, key: &str) -> QueryResult<Setting> {
conn.with(|c| {
settings::table
.filter(settings::key.eq(key))
.select(SettingRecord::as_select())
.first(c)
.map(Into::into)
})
}
pub fn set_setting_value(
conn: &Connection,
conn: &DbConnection,
key: &str,
value: &str,
updated_at: i64,
) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO settings (key, value, updated_at) VALUES (?1, ?2, ?3)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
params![key, value, updated_at],
)?;
Ok(())
) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(settings::table)
.values((
settings::key.eq(key),
settings::value.eq(value),
settings::updated_at.eq(updated_at),
))
.on_conflict(settings::key)
.do_update()
.set((
settings::value.eq(value),
settings::updated_at.eq(updated_at),
))
.execute(c)
.map(|_| ())
})
}
pub fn list_all_settings(conn: &Connection) -> rusqlite::Result<Vec<Setting>> {
let mut stmt = conn.prepare(&format!(
"SELECT {SETTING_COLUMNS} FROM settings ORDER BY key"
))?;
let rows = stmt.query_map([], setting_from_row)?;
rows.collect()
pub fn list_all_settings(conn: &DbConnection) -> QueryResult<Vec<Setting>> {
conn.with(|c| {
settings::table
.order(settings::key)
.select(SettingRecord::as_select())
.load(c)
.map(|rows: Vec<SettingRecord>| rows.into_iter().map(Into::into).collect())
})
}
#[cfg(test)]
@@ -39,7 +55,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

@@ -1,73 +1,73 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use diesel::sql_types::Text;
use crate::db::from_row::{TAG_COLUMNS, tag_from_row};
use crate::db::DbConnection;
use crate::db::from_row::TagRecord;
use crate::db::schema::tags;
use crate::model::Tag;
pub fn insert_tag(conn: &Connection, tag: &Tag) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO tags (id, project_id, name, color, post_template_slug, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![
tag.id,
tag.project_id,
tag.name,
tag.color,
tag.post_template_slug,
tag.created_at,
tag.updated_at,
],
)?;
Ok(())
diesel::define_sql_function!(fn lower(value: Text) -> Text);
pub fn insert_tag(conn: &DbConnection, tag: &Tag) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(tags::table)
.values(TagRecord::from(tag))
.execute(c)
.map(|_| ())
})
}
pub fn get_tag_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Tag> {
conn.query_row(
&format!("SELECT {TAG_COLUMNS} FROM tags WHERE id = ?1"),
params![id],
tag_from_row,
)
pub fn get_tag_by_id(conn: &DbConnection, id: &str) -> QueryResult<Tag> {
conn.with(|c| {
tags::table
.filter(tags::id.eq(id))
.select(TagRecord::as_select())
.first(c)
.map(Into::into)
})
}
pub fn get_tag_by_project_and_name(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
name: &str,
) -> rusqlite::Result<Tag> {
conn.query_row(
&format!(
"SELECT {TAG_COLUMNS} FROM tags WHERE project_id = ?1 AND LOWER(name) = LOWER(?2)"
),
params![project_id, name],
tag_from_row,
)
) -> QueryResult<Tag> {
conn.with(|c| {
tags::table
.filter(tags::project_id.eq(project_id))
.filter(lower(tags::name).eq(name.to_lowercase()))
.select(TagRecord::as_select())
.first(c)
.map(Into::into)
})
}
pub fn list_tags_by_project(conn: &Connection, project_id: &str) -> rusqlite::Result<Vec<Tag>> {
let mut stmt = conn.prepare(&format!(
"SELECT {TAG_COLUMNS} FROM tags WHERE project_id = ?1 ORDER BY name"
))?;
let rows = stmt.query_map(params![project_id], tag_from_row)?;
rows.collect()
pub fn list_tags_by_project(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<Tag>> {
conn.with(|c| {
tags::table
.filter(tags::project_id.eq(project_id))
.order(tags::name)
.select(TagRecord::as_select())
.load(c)
.map(|rows: Vec<TagRecord>| rows.into_iter().map(Into::into).collect())
})
}
pub fn update_tag(conn: &Connection, tag: &Tag) -> rusqlite::Result<()> {
conn.execute(
"UPDATE tags SET name = ?1, color = ?2, post_template_slug = ?3, updated_at = ?4
WHERE id = ?5",
params![
tag.name,
tag.color,
tag.post_template_slug,
tag.updated_at,
tag.id,
],
)?;
Ok(())
pub fn update_tag(conn: &DbConnection, tag: &Tag) -> QueryResult<()> {
conn.with(|c| {
diesel::update(tags::table.filter(tags::id.eq(&tag.id)))
.set(TagRecord::from(tag))
.execute(c)
.map(|_| ())
})
}
pub fn delete_tag(conn: &Connection, id: &str) -> rusqlite::Result<()> {
conn.execute("DELETE FROM tags WHERE id = ?1", params![id])?;
Ok(())
pub fn delete_tag(conn: &DbConnection, id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(tags::table.filter(tags::id.eq(id)))
.execute(c)
.map(|_| ())
})
}
#[cfg(test)]
@@ -77,7 +77,7 @@ mod tests {
use crate::db::queries::project::{insert_project, make_test_project};
fn setup() -> Database {
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();
db

View File

@@ -1,90 +1,73 @@
use rusqlite::{Connection, params};
use diesel::prelude::*;
use crate::db::from_row::{
TEMPLATE_COLUMNS, template_from_row, template_kind_to_str, template_status_to_str,
};
use crate::db::DbConnection;
use crate::db::from_row::{TemplateRecord, convert, convert_all};
use crate::db::schema::templates;
use crate::model::Template;
pub fn insert_template(conn: &Connection, t: &Template) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO templates (
id, project_id, slug, title, kind, enabled, version,
file_path, status, content, created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
params![
t.id,
t.project_id,
t.slug,
t.title,
template_kind_to_str(&t.kind),
t.enabled as i64,
t.version,
t.file_path,
template_status_to_str(&t.status),
t.content,
t.created_at,
t.updated_at,
],
)?;
Ok(())
pub fn insert_template(conn: &DbConnection, t: &Template) -> QueryResult<()> {
conn.with(|c| {
diesel::insert_into(templates::table)
.values(TemplateRecord::from(t))
.execute(c)
.map(|_| ())
})
}
pub fn get_template_by_id(conn: &Connection, id: &str) -> rusqlite::Result<Template> {
conn.query_row(
&format!("SELECT {TEMPLATE_COLUMNS} FROM templates WHERE id = ?1"),
params![id],
template_from_row,
)
pub fn get_template_by_id(conn: &DbConnection, id: &str) -> QueryResult<Template> {
conn.with(|c| {
templates::table
.filter(templates::id.eq(id))
.select(TemplateRecord::as_select())
.first(c)
.and_then(convert)
})
}
pub fn get_template_by_slug(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
slug: &str,
) -> rusqlite::Result<Template> {
conn.query_row(
&format!("SELECT {TEMPLATE_COLUMNS} FROM templates WHERE project_id = ?1 AND slug = ?2"),
params![project_id, slug],
template_from_row,
)
) -> QueryResult<Template> {
conn.with(|c| {
templates::table
.filter(templates::project_id.eq(project_id))
.filter(templates::slug.eq(slug))
.select(TemplateRecord::as_select())
.first(c)
.and_then(convert)
})
}
pub fn list_templates_by_project(
conn: &Connection,
conn: &DbConnection,
project_id: &str,
) -> rusqlite::Result<Vec<Template>> {
let mut stmt = conn.prepare(&format!(
"SELECT {TEMPLATE_COLUMNS} FROM templates WHERE project_id = ?1 ORDER BY title"
))?;
let rows = stmt.query_map(params![project_id], template_from_row)?;
rows.collect()
) -> QueryResult<Vec<Template>> {
conn.with(|c| {
templates::table
.filter(templates::project_id.eq(project_id))
.order(templates::title)
.select(TemplateRecord::as_select())
.load(c)
.and_then(convert_all)
})
}
pub fn update_template(conn: &Connection, t: &Template) -> rusqlite::Result<()> {
conn.execute(
"UPDATE templates SET
slug = ?1, title = ?2, kind = ?3, enabled = ?4, version = ?5,
file_path = ?6, status = ?7, content = ?8, updated_at = ?9
WHERE id = ?10",
params![
t.slug,
t.title,
template_kind_to_str(&t.kind),
t.enabled as i64,
t.version,
t.file_path,
template_status_to_str(&t.status),
t.content,
t.updated_at,
t.id,
],
)?;
Ok(())
pub fn update_template(conn: &DbConnection, t: &Template) -> QueryResult<()> {
conn.with(|c| {
diesel::update(templates::table.filter(templates::id.eq(&t.id)))
.set(TemplateRecord::from(t))
.execute(c)
.map(|_| ())
})
}
pub fn delete_template(conn: &Connection, id: &str) -> rusqlite::Result<()> {
conn.execute("DELETE FROM templates WHERE id = ?1", params![id])?;
Ok(())
pub fn delete_template(conn: &DbConnection, id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(templates::table.filter(templates::id.eq(id)))
.execute(c)
.map(|_| ())
})
}
#[cfg(test)]
@@ -95,7 +78,7 @@ mod tests {
use crate::model::{TemplateKind, TemplateStatus};
fn setup() -> Database {
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();
db