diff --git a/crates/bds-core/src/db/queries/post.rs b/crates/bds-core/src/db/queries/post.rs index cac53a7..d7e7739 100644 --- a/crates/bds-core/src/db/queries/post.rs +++ b/crates/bds-core/src/db/queries/post.rs @@ -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::), - 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(); diff --git a/crates/bds-core/src/engine/post.rs b/crates/bds-core/src/engine/post.rs index 0e4be53..3464d71 100644 --- a/crates/bds-core/src/engine/post.rs +++ b/crates/bds-core/src/engine/post.rs @@ -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); diff --git a/crates/bds-core/src/engine/wordpress_import.rs b/crates/bds-core/src/engine/wordpress_import.rs index f1776e4..2a035f8 100644 --- a/crates/bds-core/src/engine/wordpress_import.rs +++ b/crates/bds-core/src/engine/wordpress_import.rs @@ -598,16 +598,13 @@ pub fn analyze_wxr( } fn existing_post_body(post: &Post, data_dir: &Path) -> Option { - 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)); diff --git a/crates/bds-core/src/model/post.rs b/crates/bds-core/src/model/post.rs index 42b4fce..5204866 100644 --- a/crates/bds-core/src/model/post.rs +++ b/crates/bds-core/src/model/post.rs @@ -96,7 +96,7 @@ pub struct Post { serialize_as = crate::db::types::DbStringList )] pub categories: Vec, - // 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, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/bds-core/tests/wordpress_import.rs b/crates/bds-core/tests/wordpress_import.rs index 11e9575..b509c62 100644 --- a/crates/bds-core/tests/wordpress_import.rs +++ b/crates/bds-core/tests/wordpress_import.rs @@ -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 = diff --git a/specs/post.allium b/specs/post.allium index 0828f5e..6f568cb 100644 --- a/specs/post.allium +++ b/specs/post.allium @@ -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 } diff --git a/specs/schema.allium b/specs/schema.allium index d27f9ea..f907b4e 100644 --- a/specs/schema.allium +++ b/specs/schema.allium @@ -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?