Align legacy post snapshots with bDS2.

This commit is contained in:
2026-07-21 22:54:06 +02:00
parent 535303e8b5
commit b8014e0a10
7 changed files with 111 additions and 160 deletions

View File

@@ -79,75 +79,6 @@ pub fn update_post_status(
})
}
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: &DbConnection,
id: &str,
file_path: &str,
updated_at: i64,
) -> 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(
clippy::too_many_arguments,
reason = "arguments mirror the published snapshot columns"
)]
pub fn set_published_snapshot(
conn: &DbConnection,
id: &str,
title: &str,
content: &str,
tags: &str,
categories: &str,
excerpt: Option<&str>,
published_at: i64,
updated_at: i64,
) -> 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: &DbConnection, id: &str) -> QueryResult<()> {
conn.with(|c| {
diesel::delete(posts::table.filter(posts::id.eq(id)))
@@ -545,46 +476,6 @@ mod tests {
assert!(get_post_by_id(db.conn(), "x1").is_err());
}
#[test]
fn clear_content_sets_null() {
let db = setup();
insert_post(db.conn(), &make_post("x1", "hello")).unwrap();
clear_post_content(db.conn(), "x1", 5000).unwrap();
let fetched = get_post_by_id(db.conn(), "x1").unwrap();
assert!(fetched.content.is_none());
}
#[test]
fn set_file_path_updates() {
let db = setup();
insert_post(db.conn(), &make_post("x1", "hello")).unwrap();
set_post_file_path(db.conn(), "x1", "posts/2024/01/hello.md", 5000).unwrap();
let fetched = get_post_by_id(db.conn(), "x1").unwrap();
assert_eq!(fetched.file_path, "posts/2024/01/hello.md");
}
#[test]
fn published_snapshot() {
let db = setup();
insert_post(db.conn(), &make_post("x1", "hello")).unwrap();
set_published_snapshot(
db.conn(),
"x1",
"Pub Title",
"Pub Body",
"[\"rust\"]",
"[\"tech\"]",
Some("Pub Excerpt"),
3000,
3000,
)
.unwrap();
let fetched = get_post_by_id(db.conn(), "x1").unwrap();
assert_eq!(fetched.published_title.as_deref(), Some("Pub Title"));
assert_eq!(fetched.published_content.as_deref(), Some("Pub Body"));
assert_eq!(fetched.published_at, Some(3000));
}
#[test]
fn delete_removes_post() {
let db = setup();

View File

@@ -327,37 +327,9 @@ fn publish_post_in_savepoint(
}
}
// Set published snapshot fields
let tags_json = serde_json::to_string(&post.tags).unwrap_or_else(|_| "[]".into());
let cats_json = serde_json::to_string(&post.categories).unwrap_or_else(|_| "[]".into());
post.published_title = Some(post.title.clone());
post.published_content = Some(body.clone());
post.published_tags = Some(tags_json.clone());
post.published_categories = Some(cats_json.clone());
post.published_excerpt = post.excerpt.clone();
qp::set_published_snapshot(
conn,
&post_id,
&post.title,
&body,
&tags_json,
&cats_json,
post.excerpt.as_deref(),
published_at,
now,
)?;
// Set file_path and checksum in DB
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)?;
// Persist the published record without touching the legacy published_* columns.
post.content = None;
// Set status = Published
qp::update_post_status(conn, &post_id, &PostStatus::Published, now)?;
qp::update_post(conn, &post)?;
// Publish all translations
let translations = qt::list_post_translations_by_post(conn, &post_id)?;
@@ -1638,6 +1610,53 @@ mod tests {
assert_eq!(updated.content, None);
}
#[test]
fn published_updates_ignore_legacy_snapshot_values() {
let (db, dir) = setup();
let mut post = create_published_post(&db, &dir, "Published", "canonical body");
post.published_title = Some("Different Legacy Title".into());
post.published_content = Some("replacement body".into());
post.published_tags = Some("[\"legacy\"]".into());
qp::update_post(db.conn(), &post).unwrap();
let identical = update_post(
db.conn(),
dir.path(),
&post.id,
Some("Published"),
None,
None,
Some("canonical body"),
None,
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(identical.status, PostStatus::Published);
let changed = update_post(
db.conn(),
dir.path(),
&post.id,
None,
None,
None,
Some("replacement body"),
None,
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(changed.status, PostStatus::Draft);
assert_eq!(changed.content.as_deref(), Some("replacement body"));
}
#[test]
fn published_title_change_reopens_draft_with_file_body() {
let (db, dir) = setup();
@@ -1809,9 +1828,12 @@ mod tests {
// published_at should be set
assert!(from_db.published_at.is_some());
// Published snapshot fields should be set
assert_eq!(from_db.published_title.as_deref(), Some("Publish Me"));
assert!(from_db.published_content.is_some());
// Legacy published snapshot columns stay empty, matching bDS2.
assert!(from_db.published_title.is_none());
assert!(from_db.published_content.is_none());
assert!(from_db.published_tags.is_none());
assert!(from_db.published_categories.is_none());
assert!(from_db.published_excerpt.is_none());
// File should exist on disk
let abs_path = dir.path().join(&from_db.file_path);
@@ -1823,6 +1845,39 @@ mod tests {
assert!(file_content.contains("Publish Me"));
}
#[test]
fn publish_post_preserves_legacy_snapshot_values_without_using_them() {
let (db, dir) = setup();
let mut post = create_post(
db.conn(),
dir.path(),
"p1",
"Publish Me",
Some("body"),
vec![],
vec![],
None,
None,
None,
)
.unwrap();
post.published_title = Some("Legacy Title".into());
post.published_content = Some("Legacy Body".into());
post.published_tags = Some("[\"legacy\"]".into());
post.published_categories = Some("[\"old\"]".into());
post.published_excerpt = Some("Legacy Excerpt".into());
qp::update_post(db.conn(), &post).unwrap();
publish_post(db.conn(), dir.path(), &post.id).unwrap();
let published = qp::get_post_by_id(db.conn(), &post.id).unwrap();
assert_eq!(published.published_title, post.published_title);
assert_eq!(published.published_content, post.published_content);
assert_eq!(published.published_tags, post.published_tags);
assert_eq!(published.published_categories, post.published_categories);
assert_eq!(published.published_excerpt, post.published_excerpt);
}
#[test]
fn publish_replaces_divergent_post_path_and_ignores_missing_old_file() {
let (db, dir) = setup();
@@ -1988,7 +2043,7 @@ mod tests {
let discarded = discard_post_draft(db.conn(), dir.path(), &post.id).unwrap();
assert_eq!(discarded.status, PostStatus::Published);
assert_eq!(discarded.title, published.title);
assert_eq!(discarded.excerpt, published.published_excerpt);
assert_eq!(discarded.excerpt, published.excerpt);
assert_eq!(discarded.tags, vec!["one"]);
assert_eq!(discarded.categories, vec!["cat"]);
assert_eq!(discarded.content, None);

View File

@@ -598,16 +598,13 @@ pub fn analyze_wxr(
}
fn existing_post_body(post: &Post, data_dir: &Path) -> Option<String> {
post.content
.clone()
.or_else(|| post.published_content.clone())
.or_else(|| {
(!post.file_path.is_empty())
.then(|| fs::read_to_string(data_dir.join(&post.file_path)).ok())
.flatten()
.and_then(|raw| crate::util::frontmatter::read_post_file(&raw).ok())
.map(|(_, body)| body)
})
post.content.clone().or_else(|| {
(!post.file_path.is_empty())
.then(|| fs::read_to_string(data_dir.join(&post.file_path)).ok())
.flatten()
.and_then(|raw| crate::util::frontmatter::read_post_file(&raw).ok())
.map(|(_, body)| body)
})
}
fn analyze_post(
@@ -1363,7 +1360,7 @@ fn import_post_item(
qp::update_post(conn, &imported)?;
if item.source_status.as_deref() == Some("publish") {
imported = post::publish_post(conn, data_dir, &imported.id)?;
let body = imported.published_content.clone().unwrap_or_default();
let body = content.to_string();
imported.created_at = item.created_at.unwrap_or(imported.created_at);
imported.updated_at = item.updated_at.unwrap_or(imported.created_at);
imported.published_at = item.published_at.or(Some(imported.created_at));

View File

@@ -96,7 +96,7 @@ pub struct Post {
serialize_as = crate::db::types::DbStringList
)]
pub categories: Vec<String>,
// Published snapshot fields (used for diff detection)
// Legacy bDS2-compatible columns. Publishing and lifecycle logic leave them untouched.
#[serde(skip_serializing_if = "Option::is_none")]
pub published_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]

View File

@@ -635,8 +635,12 @@ fn overwrite_resolution_preserves_existing_post_and_media_identity() {
bds_core::db::queries::post::get_post_by_id(db.conn(), &existing_post.id).unwrap();
assert_eq!(overwritten_post.id, existing_post.id);
assert_eq!(overwritten_post.status, PostStatus::Published);
assert!(overwritten_post.published_content.is_none());
let (_, overwritten_body) =
read_post_file(&fs::read_to_string(dir.path().join(&overwritten_post.file_path)).unwrap())
.unwrap();
assert_eq!(
overwritten_post.published_content.as_deref(),
Some(overwritten_body.as_str()),
report.posts[0].content.as_deref()
);
let overwritten_media =

View File

@@ -107,9 +107,9 @@ entity Post {
updated_at: Timestamp
published_at: Timestamp?
-- Published snapshot: copy of title/content/tags/categories/excerpt as of
-- the last publish. Used by changes_affect_published_content to decide when
-- an edit reopens a published post to draft (see ReopenPublishedPost).
-- Legacy bDS2-compatible columns. Publishing does not populate them and
-- application behaviour does not depend on them; imported non-null values
-- remain passive database data.
published_title: String?
published_content: String?
published_tags: String?
@@ -200,6 +200,9 @@ rule ReopenPublishedPost {
when: UpdatePostRequested(post, changes)
requires: post.status = published
requires: changes_affect_published_content(changes)
-- Compares supplied metadata with the current post fields and supplied
-- content with the canonical file body. Legacy published_* columns are
-- not consulted because bDS2 does not populate them.
ensures: post.status = draft
}

View File

@@ -62,7 +62,8 @@ entity Post {
language: String? -- ISO 639-1 code
do_not_translate: Boolean
-- Published snapshot columns (written on publish for diff detection)
-- Legacy compatibility columns. bDS2 and RuDS leave them unchanged on
-- publish and do not use them to determine lifecycle behaviour.
published_title: String?
published_content: String?
published_tags: String?