From 3cf000f8655d531d0e13c8248b85a464bbfbba20 Mon Sep 17 00:00:00 2001 From: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> Date: Sat, 1 Aug 2026 15:22:13 -0400 Subject: [PATCH] feat(db): port SQLite community helpers Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz> --- crates/buzz-db/src/lib.rs | 9 ++++ crates/buzz-db/src/sqlite.rs | 94 ++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/crates/buzz-db/src/lib.rs b/crates/buzz-db/src/lib.rs index 984e4164c..8c96f22a2 100644 --- a/crates/buzz-db/src/lib.rs +++ b/crates/buzz-db/src/lib.rs @@ -1437,6 +1437,9 @@ impl Db { /// Set by relay admins/owners via the kind:9033 command; the value is /// validated and size-capped at that write path. pub async fn get_community_icon(&self, community_id: CommunityId) -> Result> { + if let DbBackend::SQLite(pool) = &self.backend { + return sqlite::lookup_community_icon(pool, community_id).await; + } let row = sqlx::query( r#" SELECT icon @@ -1461,6 +1464,9 @@ impl Db { community_id: CommunityId, icon: Option<&str>, ) -> Result<()> { + if let DbBackend::SQLite(pool) = &self.backend { + return sqlite::set_community_icon(pool, community_id, icon).await; + } sqlx::query( r#" UPDATE communities @@ -1666,6 +1672,9 @@ impl Db { /// Internal relay producers use this to derive tenant context from the row /// they are acting on, rather than falling back to an implicit default. pub async fn community_of_channel(&self, channel_id: Uuid) -> Result> { + if let DbBackend::SQLite(pool) = &self.backend { + return sqlite::community_of_channel(pool, channel_id).await; + } let row = sqlx::query( r#" SELECT community_id diff --git a/crates/buzz-db/src/sqlite.rs b/crates/buzz-db/src/sqlite.rs index 13faa98ca..52602adeb 100644 --- a/crates/buzz-db/src/sqlite.rs +++ b/crates/buzz-db/src/sqlite.rs @@ -479,6 +479,52 @@ pub(crate) async fn is_community_active( Ok(count != 0) } +pub(crate) async fn lookup_community_icon( + pool: &SqlitePool, + community: CommunityId, +) -> Result> { + Ok( + sqlx::query_scalar::<_, Option>("SELECT icon FROM communities WHERE id = ?1") + .bind(community.as_uuid().to_string()) + .fetch_optional(pool) + .await? + .flatten() + .filter(|icon| !icon.is_empty()), + ) +} + +pub(crate) async fn set_community_icon( + pool: &SqlitePool, + community: CommunityId, + icon: Option<&str>, +) -> Result<()> { + sqlx::query("UPDATE communities SET icon = ?2 WHERE id = ?1") + .bind(community.as_uuid().to_string()) + .bind(icon) + .execute(pool) + .await?; + Ok(()) +} + +pub(crate) async fn community_of_channel( + pool: &SqlitePool, + channel_id: Uuid, +) -> Result> { + let community: Option = sqlx::query_scalar( + "SELECT community_id FROM channels WHERE id = ?1 AND deleted_at IS NULL", + ) + .bind(channel_id.to_string()) + .fetch_optional(pool) + .await?; + community + .map(|id| { + Uuid::parse_str(&id) + .map(CommunityId::from_uuid) + .map_err(|e| crate::DbError::InvalidData(e.to_string())) + }) + .transpose() +} + pub(crate) async fn ensure_configured_community( pool: &SqlitePool, normalized_host: &str, @@ -4582,6 +4628,54 @@ mod tests { std::fs::remove_file(path).unwrap(); } + #[tokio::test] + async fn community_icon_and_channel_owner_helpers_match_lifecycle_contracts() { + let pool = connect("sqlite::memory:").await.unwrap(); + let community = ensure_configured_community(&pool, "community-helpers.example") + .await + .unwrap() + .id; + assert_eq!(lookup_community_icon(&pool, community).await.unwrap(), None); + set_community_icon(&pool, community, Some("https://example.test/icon.png")) + .await + .unwrap(); + assert_eq!( + lookup_community_icon(&pool, community) + .await + .unwrap() + .as_deref(), + Some("https://example.test/icon.png") + ); + set_community_icon(&pool, community, Some("")) + .await + .unwrap(); + assert_eq!(lookup_community_icon(&pool, community).await.unwrap(), None); + set_community_icon(&pool, community, None).await.unwrap(); + + let channel = Uuid::new_v4(); + sqlx::query("INSERT INTO channels (id,community_id,name,channel_type,visibility,created_by) VALUES (?1,?2,'helper','public','public',?3)") + .bind(channel.to_string()) + .bind(community.as_uuid().to_string()) + .bind(vec![1_u8; 32]) + .execute(&pool) + .await + .unwrap(); + assert_eq!( + community_of_channel(&pool, channel).await.unwrap(), + Some(community) + ); + sqlx::query("UPDATE channels SET deleted_at = unixepoch() WHERE id = ?1") + .bind(channel.to_string()) + .execute(&pool) + .await + .unwrap(); + assert_eq!(community_of_channel(&pool, channel).await.unwrap(), None); + assert_eq!( + community_of_channel(&pool, Uuid::new_v4()).await.unwrap(), + None + ); + } + #[tokio::test] async fn allowlist_is_idempotent_and_community_scoped() { let pool = connect("sqlite::memory:").await.unwrap();