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();