From 4ae8bf88f2a788737b25f1e58646ccb9bc963ce1 Mon Sep 17 00:00:00 2001 From: Brother Darryl <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz> Date: Mon, 10 Aug 2026 15:33:36 -0400 Subject: [PATCH] feat(db): enable SQLite community persistence Signed-off-by: Brother Darryl <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz> --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/buzz-db/src/sqlite.rs | 79 ++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f2c5f0050d..13c9a631eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4549,6 +4549,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] diff --git a/Cargo.toml b/Cargo.toml index be9cb5aa7b..69b6e1aad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ tower-http = { version = "0.6", features = ["trace", "cors", "compression-gzip" # Database sqlx = { version = "0.9", features = [ - "runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json" + "runtime-tokio", "tls-rustls", "postgres", "sqlite", "uuid", "chrono", "json" ] } # Redis diff --git a/crates/buzz-db/src/sqlite.rs b/crates/buzz-db/src/sqlite.rs index 922657ad11..c3e4dfdc8f 100644 --- a/crates/buzz-db/src/sqlite.rs +++ b/crates/buzz-db/src/sqlite.rs @@ -265,6 +265,85 @@ pub(crate) async fn is_community_active( Ok(count != 0) } +pub(crate) async fn rebind_single_node_community_host( + pool: &SqlitePool, + normalized_host: &str, + owner_pubkey: &str, +) -> Result> { + let mut tx = pool.begin().await?; + + // Defect-era databases may contain several loopback communities, one per + // ephemeral port. Prefer a community owned by this desktop identity, then + // the one carrying the most events. This preserves the most useful UUID and + // its channels/messages while making the current Host header resolvable. + let row = sqlx::query( + r#"SELECT c.id, c.host + FROM communities c + LEFT JOIN relay_members rm + ON rm.community_id = c.id + AND lower(rm.pubkey) = lower(?1) + AND rm.role = 'owner' + LEFT JOIN events e ON e.community_id = c.id + WHERE c.archived_at IS NULL + AND c.host LIKE '127.0.0.1:%' + GROUP BY c.id, c.host, c.created_at + ORDER BY (rm.pubkey IS NOT NULL) DESC, count(e.id) DESC, c.created_at ASC, c.id ASC + LIMIT 1"#, + ) + .bind(owner_pubkey) + .fetch_optional(&mut *tx) + .await?; + let Some(row) = row else { + tx.commit().await?; + return Ok(None); + }; + let record = community_record(row)?; + if record.host.eq_ignore_ascii_case(normalized_host) { + tx.commit().await?; + return Ok(Some(record)); + } + + // The OS may reuse a port that belongs to another stale defect-era row. + // Swap that collision to the selected row's old authority transactionally, + // using a temporary unique host to satisfy the case-insensitive constraint. + if let Some(collision_id) = sqlx::query_scalar::<_, String>( + "SELECT id FROM communities WHERE host = ?1 COLLATE NOCASE AND id <> ?2", + ) + .bind(normalized_host) + .bind(record.id.as_uuid().to_string()) + .fetch_optional(&mut *tx) + .await? + { + let temporary_host = format!("rebind-{}.invalid", Uuid::new_v4()); + sqlx::query("UPDATE communities SET host = ?2 WHERE id = ?1") + .bind(&collision_id) + .bind(&temporary_host) + .execute(&mut *tx) + .await?; + sqlx::query("UPDATE communities SET host = ?2 WHERE id = ?1") + .bind(record.id.as_uuid().to_string()) + .bind(normalized_host) + .execute(&mut *tx) + .await?; + sqlx::query("UPDATE communities SET host = ?2 WHERE id = ?1") + .bind(collision_id) + .bind(&record.host) + .execute(&mut *tx) + .await?; + } else { + sqlx::query("UPDATE communities SET host = ?2 WHERE id = ?1") + .bind(record.id.as_uuid().to_string()) + .bind(normalized_host) + .execute(&mut *tx) + .await?; + } + tx.commit().await?; + Ok(Some(CommunityRecord { + id: record.id, + host: normalized_host.to_string(), + })) +} + pub(crate) async fn ensure_configured_community( pool: &SqlitePool, normalized_host: &str,