diff --git a/Cargo.lock b/Cargo.lock index da4ce3312..2d305eabe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1005,6 +1005,7 @@ name = "buzz-db" version = "0.1.0" dependencies = [ "buzz-core", + "buzz-db-backend-macro", "chrono", "hex", "metrics", @@ -1021,6 +1022,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "buzz-db-backend-macro" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "trybuild", +] + [[package]] name = "buzz-dev-mcp" version = "0.1.0" @@ -3260,6 +3271,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "globset" version = "0.4.18" @@ -9490,6 +9507,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-triple" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" + [[package]] name = "tempfile" version = "3.27.0" @@ -9503,6 +9526,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termina" version = "0.3.3" @@ -10180,6 +10212,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "trybuild" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml 1.1.2+spec-1.1.0", +] + [[package]] name = "tungstenite" version = "0.26.2" diff --git a/Cargo.toml b/Cargo.toml index 9879a1666..8b6d6b02a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/buzz-conformance", "crates/buzz-push-gateway", "crates/buzz-db", + "crates/buzz-db-backend-macro", "crates/buzz-pubsub", "crates/buzz-auth", "crates/buzz-search", diff --git a/crates/buzz-db-backend-macro/Cargo.toml b/crates/buzz-db-backend-macro/Cargo.toml new file mode 100644 index 000000000..31c4718ab --- /dev/null +++ b/crates/buzz-db-backend-macro/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "buzz-db-backend-macro" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } + +[dev-dependencies] +trybuild = "1" diff --git a/crates/buzz-db-backend-macro/src/lib.rs b/crates/buzz-db-backend-macro/src/lib.rs new file mode 100644 index 000000000..8ba800378 --- /dev/null +++ b/crates/buzz-db-backend-macro/src/lib.rs @@ -0,0 +1,104 @@ +//! Compile-time enforcement for explicit SQLite decisions on public `Db` methods. +//! +//! The impl-level attribute inspects only public associated methods. Private and +//! `pub(crate)` plumbing is intentionally outside this policy boundary. + +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{parse_macro_input, Attribute, ImplItem, ItemImpl, LitStr, Visibility}; + +#[proc_macro_attribute] +pub fn enforce_sqlite_backend_declarations(_args: TokenStream, input: TokenStream) -> TokenStream { + let mut item = parse_macro_input!(input as ItemImpl); + let mut errors = Vec::new(); + let mut inventory = Vec::new(); + + for impl_item in &mut item.items { + let ImplItem::Fn(method) = impl_item else { + continue; + }; + if !matches!(method.vis, Visibility::Public(_)) { + continue; + } + + let mut markers = Vec::new(); + let mut retained = Vec::new(); + for attr in method.attrs.drain(..) { + if attr.path().is_ident("sqlite_backend") { + markers.push(attr); + } else { + retained.push(attr); + } + } + method.attrs = retained; + + let name = method.sig.ident.to_string(); + match markers.as_slice() { + [] => errors.push(syn::Error::new( + method.sig.ident.span(), + format!("public Db method `{name}` is missing #[sqlite_backend(...)]"), + )), + [marker] => match parse_marker(marker) { + Ok(reason) => inventory.push((name, reason)), + Err(error) => errors.push(error), + }, + _ => errors.push(syn::Error::new( + method.sig.ident.span(), + format!("public Db method `{name}` has duplicate #[sqlite_backend] markers"), + )), + } + } + + inventory.sort_by(|a, b| a.0.cmp(&b.0)); + let names = inventory + .iter() + .map(|(name, _)| LitStr::new(name, proc_macro2::Span::call_site())); + let reasons = inventory.iter().map(|(_, reason)| match reason { + Some(reason) => quote! { Some(#reason) }, + None => quote! { None }, + }); + let errors = errors.into_iter().map(|e| e.to_compile_error()); + + quote! { + #(#errors)* + #item + #[doc(hidden)] + pub const SQLITE_BACKEND_INVENTORY: &[(&str, Option<&str>)] = &[#((#names, #reasons)),*]; + } + .into() +} + +fn parse_marker(attr: &Attribute) -> syn::Result> { + let mut result = None; + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("implemented") { + if result.is_some() { + return Err(meta.error("duplicate backend decision")); + } + result = Some(None); + return Ok(()); + } + if meta.path.is_ident("unsupported") { + if result.is_some() { + return Err(meta.error("duplicate backend decision")); + } + let value = meta.value()?.parse::()?; + if value.value().trim().is_empty() { + return Err(syn::Error::new( + value.span(), + "unsupported reason must not be empty", + )); + } + result = Some(Some(value)); + return Ok(()); + } + Err(meta.error("expected `implemented` or `unsupported = \"reason\"`")) + })?; + result.ok_or_else(|| { + syn::Error::new( + attr.span(), + "backend decision must be `implemented` or `unsupported = \"reason\"`", + ) + }) +} diff --git a/crates/buzz-db-backend-macro/tests/compile.rs b/crates/buzz-db-backend-macro/tests/compile.rs new file mode 100644 index 000000000..f7fc2773f --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/compile.rs @@ -0,0 +1,9 @@ +#[test] +fn ui() { + let tests = trybuild::TestCases::new(); + tests.compile_fail("tests/ui/missing_marker.rs"); + tests.compile_fail("tests/ui/duplicate_marker.rs"); + tests.compile_fail("tests/ui/empty_reason.rs"); + tests.compile_fail("tests/ui/malformed_marker.rs"); + tests.pass("tests/ui/valid_markers.rs"); +} diff --git a/crates/buzz-db-backend-macro/tests/ui/duplicate_marker.rs b/crates/buzz-db-backend-macro/tests/ui/duplicate_marker.rs new file mode 100644 index 000000000..1059d2ae9 --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/ui/duplicate_marker.rs @@ -0,0 +1,9 @@ +use buzz_db_backend_macro::enforce_sqlite_backend_declarations; +struct Db; +#[enforce_sqlite_backend_declarations] +impl Db { + #[sqlite_backend(implemented)] + #[sqlite_backend(implemented)] + pub fn duplicated(&self) {} +} +fn main() {} diff --git a/crates/buzz-db-backend-macro/tests/ui/duplicate_marker.stderr b/crates/buzz-db-backend-macro/tests/ui/duplicate_marker.stderr new file mode 100644 index 000000000..5784cfc9c --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/ui/duplicate_marker.stderr @@ -0,0 +1,5 @@ +error: public Db method `duplicated` has duplicate #[sqlite_backend] markers + --> tests/ui/duplicate_marker.rs:7:12 + | +7 | pub fn duplicated(&self) {} + | ^^^^^^^^^^ diff --git a/crates/buzz-db-backend-macro/tests/ui/empty_reason.rs b/crates/buzz-db-backend-macro/tests/ui/empty_reason.rs new file mode 100644 index 000000000..4454aa8d4 --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/ui/empty_reason.rs @@ -0,0 +1,8 @@ +use buzz_db_backend_macro::enforce_sqlite_backend_declarations; +struct Db; +#[enforce_sqlite_backend_declarations] +impl Db { + #[sqlite_backend(unsupported = " ")] + pub fn empty(&self) {} +} +fn main() {} diff --git a/crates/buzz-db-backend-macro/tests/ui/empty_reason.stderr b/crates/buzz-db-backend-macro/tests/ui/empty_reason.stderr new file mode 100644 index 000000000..4fb735d11 --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/ui/empty_reason.stderr @@ -0,0 +1,5 @@ +error: unsupported reason must not be empty + --> tests/ui/empty_reason.rs:5:36 + | +5 | #[sqlite_backend(unsupported = " ")] + | ^^^^ diff --git a/crates/buzz-db-backend-macro/tests/ui/malformed_marker.rs b/crates/buzz-db-backend-macro/tests/ui/malformed_marker.rs new file mode 100644 index 000000000..69b2b934d --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/ui/malformed_marker.rs @@ -0,0 +1,8 @@ +use buzz_db_backend_macro::enforce_sqlite_backend_declarations; +struct Db; +#[enforce_sqlite_backend_declarations] +impl Db { + #[sqlite_backend(sometimes)] + pub fn malformed(&self) {} +} +fn main() {} diff --git a/crates/buzz-db-backend-macro/tests/ui/malformed_marker.stderr b/crates/buzz-db-backend-macro/tests/ui/malformed_marker.stderr new file mode 100644 index 000000000..c6e3f8443 --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/ui/malformed_marker.stderr @@ -0,0 +1,5 @@ +error: expected `implemented` or `unsupported = "reason"` + --> tests/ui/malformed_marker.rs:5:22 + | +5 | #[sqlite_backend(sometimes)] + | ^^^^^^^^^ diff --git a/crates/buzz-db-backend-macro/tests/ui/missing_marker.rs b/crates/buzz-db-backend-macro/tests/ui/missing_marker.rs new file mode 100644 index 000000000..f8f735e8e --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/ui/missing_marker.rs @@ -0,0 +1,10 @@ +use buzz_db_backend_macro::enforce_sqlite_backend_declarations; + +struct Db; + +#[enforce_sqlite_backend_declarations] +impl Db { + pub fn omitted(&self) {} +} + +fn main() {} diff --git a/crates/buzz-db-backend-macro/tests/ui/missing_marker.stderr b/crates/buzz-db-backend-macro/tests/ui/missing_marker.stderr new file mode 100644 index 000000000..41ba31944 --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/ui/missing_marker.stderr @@ -0,0 +1,5 @@ +error: public Db method `omitted` is missing #[sqlite_backend(...)] + --> tests/ui/missing_marker.rs:7:12 + | +7 | pub fn omitted(&self) {} + | ^^^^^^^ diff --git a/crates/buzz-db-backend-macro/tests/ui/valid_markers.rs b/crates/buzz-db-backend-macro/tests/ui/valid_markers.rs new file mode 100644 index 000000000..7bb0258c8 --- /dev/null +++ b/crates/buzz-db-backend-macro/tests/ui/valid_markers.rs @@ -0,0 +1,18 @@ +use buzz_db_backend_macro::enforce_sqlite_backend_declarations; + +struct Db; + +#[enforce_sqlite_backend_declarations] +impl Db { + #[sqlite_backend(implemented)] + #[allow(dead_code)] + pub fn implemented(&self) {} + + /// Deliberately unsupported operation. + #[sqlite_backend(unsupported = "requires postgres-only advisory locks")] + pub async fn unsupported(&self) {} + + fn private(&self) {} +} + +fn main() {} diff --git a/crates/buzz-db/Cargo.toml b/crates/buzz-db/Cargo.toml index 6f76a11bc..0aa5be29f 100644 --- a/crates/buzz-db/Cargo.toml +++ b/crates/buzz-db/Cargo.toml @@ -9,6 +9,7 @@ description = "Postgres event store and data access layer for Buzz" [dependencies] buzz-core = { workspace = true } +buzz-db-backend-macro = { path = "../buzz-db-backend-macro" } sqlx = { workspace = true } tokio = { workspace = true } serde = { workspace = true } diff --git a/crates/buzz-db/SQLITE_BACKEND_INVENTORY.md b/crates/buzz-db/SQLITE_BACKEND_INVENTORY.md new file mode 100644 index 000000000..b6e1209a5 --- /dev/null +++ b/crates/buzz-db/SQLITE_BACKEND_INVENTORY.md @@ -0,0 +1,15 @@ +# SQLite backend inventory + +Generated from `#[sqlite_backend(...)]` declarations in `crates/buzz-db/src/lib.rs`. + +> ⚠️ **PROVISIONAL — classification in progress.** Most declarations are mechanical placeholders and this table is not yet valid S6 input. + +Regenerate after changing declarations with: + +```sh +cargo test -p buzz-db backend_inventory_is_current -- --nocapture +``` + +| Method | SQLite status | Reason | +| --- | --- | --- | +| `usage_community_count` | unsupported | usage analytics is PostgreSQL-only | diff --git a/crates/buzz-db/src/lib.rs b/crates/buzz-db/src/lib.rs index 9894b2815..569ae9dba 100644 --- a/crates/buzz-db/src/lib.rs +++ b/crates/buzz-db/src/lib.rs @@ -65,6 +65,7 @@ use std::time::Duration; use uuid::Uuid; use buzz_core::{CommunityId, StoredEvent}; +use buzz_db_backend_macro::enforce_sqlite_backend_declarations; fn event_replacement_lock_key( community_id: CommunityId, @@ -651,6 +652,7 @@ pub struct TokenSummary { pub expires_at: Option>, } +#[enforce_sqlite_backend_declarations] impl Db { /// Returns the PostgreSQL pool or a typed error for local SQLite handles. fn pg_pool(&self) -> Result<&PgPool> { @@ -674,6 +676,7 @@ impl Db { /// Return a clone of the SQLite pool for process-local services that share /// the same database, such as FTS5 search. + #[sqlite_backend(implemented)] pub fn sqlite_pool_clone(&self) -> Result { Ok(self.sqlite_pool()?.clone()) } @@ -681,6 +684,7 @@ impl Db { /// Creates a local single-node database backed by SQLite. /// /// The embedded local schema is applied before this constructor returns. + #[sqlite_backend(implemented)] pub async fn new_sqlite(database_url: &str) -> Result { let sqlite = sqlite::connect(database_url).await?; let pool = PgPoolOptions::new() @@ -707,6 +711,7 @@ impl Db { /// (migration 0021) on every connection by setting the /// `buzz.created_at_floor` GUC — this is what makes the replica fence /// proof hold for every insert path that goes through this pool. + #[sqlite_backend(implemented)] pub async fn new(config: &DbConfig) -> Result { let pool = Self::connect_pool(config, &config.database_url, true).await?; let read_max_connections = config @@ -805,6 +810,7 @@ impl Db { /// [`Db::reader_aurora_capability_on`]. Prime failure is fine: the routed /// path re-probes on the connection it already holds, so a failed prime /// costs a round trip rather than a second acquire budget. + #[sqlite_backend(implemented)] pub fn spawn_read_pool_boot_ping(&self) { let Some(read_pool) = self.read_pool.clone() else { return; @@ -832,6 +838,7 @@ impl Db { } /// Creates a `Db` from an existing `PgPool` (useful in tests). + #[sqlite_backend(implemented)] pub fn from_pool(pool: PgPool) -> Self { Self { backend: DbBackend::Postgres, @@ -852,6 +859,7 @@ impl Db { /// fake replica must open it via /// [`replica_fence::ReplicaFence::force_open_for_tests`] (see /// [`Db::fence`]). + #[sqlite_backend(implemented)] pub fn from_pools(pool: PgPool, read_pool: PgPool) -> Self { Self { backend: DbBackend::Postgres, @@ -867,11 +875,13 @@ impl Db { /// Test hook: set the head-fetch routing budget (Predicate A), which /// [`Db::from_pools`] leaves disabled. + #[sqlite_backend(implemented)] pub fn set_replica_read_max_age_for_tests(&mut self, budget: Option) { self.replica_read_max_age = budget; } /// The freshness fence gating replica routing (see [`replica_fence`]). + #[sqlite_backend(implemented)] pub fn fence(&self) -> &std::sync::Arc { &self.fence } @@ -892,6 +902,7 @@ impl Db { /// On any verification failure the probe is never spawned and the fence /// stays closed: every cursor page routes to the writer. The relay keeps /// serving — degraded capacity, never holes. + #[sqlite_backend(implemented)] pub async fn spawn_fence_probe(&self) -> Result { if self.read_pool.is_none() { return Ok(false); @@ -919,6 +930,7 @@ impl Db { } /// Whether a distinct read-replica pool is configured. + #[sqlite_backend(implemented)] pub fn has_read_pool(&self) -> bool { self.read_pool.is_some() } @@ -1071,6 +1083,7 @@ impl Db { } /// Run pending database migrations. + #[sqlite_backend(implemented)] pub async fn migrate(&self) -> Result<()> { match &self.backend { DbBackend::Postgres => migration::run_migrations(self.pg_pool()?).await, @@ -1079,6 +1092,7 @@ impl Db { } /// Returns `true` if the database is reachable (used by readiness probes). + #[sqlite_backend(implemented)] pub async fn ping(&self) -> bool { match &self.backend { DbBackend::Postgres => sqlx::query("SELECT 1").execute(&self.pool).await.is_ok(), @@ -1091,6 +1105,7 @@ impl Db { /// `size` — total connections (idle + active) /// `idle` — connections available for immediate reuse /// `max` — pool ceiling set at construction + #[sqlite_backend(implemented)] pub fn pool_stats(&self) -> DbPoolStats { let Some(pool) = (match &self.backend { DbBackend::Postgres => Some(&self.pool), @@ -1117,6 +1132,7 @@ impl Db { /// and deriving it from the writer's max would misreport saturation by /// exactly the ratio of the two pool sizes — in the direction that hides /// the problem. + #[sqlite_backend(implemented)] pub fn read_pool_stats(&self) -> Option { self.read_pool.as_ref().map(|p| DbPoolStats { size: p.size(), @@ -1131,6 +1147,7 @@ impl Db { /// detached from the shared pool so a stable leader neither returns a locked /// session to other callers nor permanently consumes a pool slot. Dropping the /// guard closes the connection and releases the session-scoped lock. + #[sqlite_backend(implemented)] pub async fn try_lock_usage_metrics( &self, lock_key: i64, @@ -1151,6 +1168,7 @@ impl Db { /// List reports for the deployment-global read-only admin plane. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn admin_list_reports( &self, community_id: Option, @@ -1195,6 +1213,7 @@ impl Db { } /// Fetch one report for the deployment-global read-only admin plane. + #[sqlite_backend(implemented)] pub async fn admin_get_report( &self, id: Uuid, @@ -1206,6 +1225,7 @@ impl Db { } /// List feedback for the deployment-global read-only admin plane. + #[sqlite_backend(implemented)] pub async fn admin_list_feedback( &self, limit: i64, @@ -1217,6 +1237,7 @@ impl Db { } /// Fetch one feedback submission for the deployment-global admin plane. + #[sqlite_backend(implemented)] pub async fn admin_get_feedback( &self, id: Uuid, @@ -1228,36 +1249,43 @@ impl Db { } /// Return total number of communities on this relay. + #[sqlite_backend(unsupported = "usage analytics is PostgreSQL-only")] pub async fn usage_community_count(&self) -> Result { usage::community_count(self.pg_pool()?).await } /// Return per-community user counts split by human/agent. + #[sqlite_backend(implemented)] pub async fn usage_user_counts(&self) -> Result> { usage::user_counts(self.pg_pool()?).await } /// Return per-community channel counts by type. + #[sqlite_backend(implemented)] pub async fn usage_channel_counts(&self) -> Result> { usage::channel_counts(self.pg_pool()?).await } /// Return per-community kind=9 message counts. + #[sqlite_backend(implemented)] pub async fn usage_message_counts(&self) -> Result> { usage::message_counts(self.pg_pool()?).await } /// Return per-community relay-member counts by role. + #[sqlite_backend(implemented)] pub async fn usage_relay_member_counts(&self) -> Result> { usage::relay_member_counts(self.pg_pool()?).await } /// Return per-community workflow counts by status. + #[sqlite_backend(implemented)] pub async fn usage_workflow_counts(&self) -> Result> { usage::workflow_counts(self.pg_pool()?).await } /// Return per-community git-repo counts. + #[sqlite_backend(implemented)] pub async fn usage_git_repo_counts(&self) -> Result> { usage::git_repo_counts(self.pg_pool()?).await } @@ -1265,6 +1293,7 @@ impl Db { /// Return per-community distinct active-user counts for a given SQL interval. /// /// `interval_sql` must be a trusted literal such as `"1 day"` or `"7 days"`. + #[sqlite_backend(implemented)] pub async fn usage_active_user_counts( &self, interval_sql: &'static str, @@ -1273,6 +1302,7 @@ impl Db { } /// Return per-community active-channel counts for a given SQL interval. + #[sqlite_backend(implemented)] pub async fn usage_active_channel_counts( &self, interval_sql: &'static str, @@ -1281,6 +1311,7 @@ impl Db { } /// Return all community id → host mappings. + #[sqlite_backend(implemented)] pub async fn usage_community_hosts(&self) -> Result> { usage::community_hosts(self.pg_pool()?).await } @@ -1289,6 +1320,7 @@ impl Db { /// /// Returns a `'static` transaction because `PgPool` is `Arc`-backed internally. /// The transaction holds an owned pool handle, not a borrow. + #[sqlite_backend(implemented)] pub async fn begin_transaction(&self) -> Result> { self.pg_pool()?.begin().await.map_err(Into::into) } @@ -1297,6 +1329,7 @@ impl Db { /// /// The caller owns host normalization and turns `None` into the fail-closed /// request/connection error. buzz-db only reads the durable host map. + #[sqlite_backend(implemented)] pub async fn lookup_community_by_host( &self, normalized_host: &str, @@ -1329,6 +1362,7 @@ impl Db { } /// Returns whether a community id still exists in the active lifecycle state. + #[sqlite_backend(implemented)] pub async fn is_community_active(&self, community_id: CommunityId) -> Result { if matches!(&self.backend, DbBackend::SQLite(_)) { return sqlite::is_community_active(self.sqlite_pool()?, community_id).await; @@ -1343,6 +1377,7 @@ impl Db { } /// Returns a community by host regardless of lifecycle state. Operator-plane only. + #[sqlite_backend(implemented)] pub async fn lookup_community_by_host_for_management( &self, normalized_host: &str, @@ -1364,6 +1399,7 @@ impl Db { /// /// This is an operator-plane helper, not a tenant-scoped data-plane read: /// callers must gate it on deployment-level operator auth before exposing it. + #[sqlite_backend(implemented)] pub async fn list_communities_owned_by( &self, owner_pubkey: &str, @@ -1409,6 +1445,7 @@ impl Db { /// fan out under *that* community rather than the deployment default. The /// community is authoritative; the host is read back for labelling only and /// is never used to re-derive the community. + #[sqlite_backend(implemented)] pub async fn lookup_community_host(&self, community_id: CommunityId) -> Result> { if matches!(&self.backend, DbBackend::SQLite(_)) { return sqlite::lookup_community_host(self.sqlite_pool()?, community_id).await; @@ -1436,6 +1473,7 @@ impl Db { /// /// Set by relay admins/owners via the kind:9033 command; the value is /// validated and size-capped at that write path. + #[sqlite_backend(implemented)] 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; @@ -1459,6 +1497,7 @@ impl Db { } /// Sets or clears (`None`) the community's workspace icon. + #[sqlite_backend(implemented)] pub async fn set_community_icon( &self, community_id: CommunityId, @@ -1484,6 +1523,7 @@ impl Db { /// Rebind the durable loopback community to this single-node process's /// current ephemeral authority while preserving its stable UUID and data. /// SQLite-only: production deployments never rewrite tenant host mappings. + #[sqlite_backend(implemented)] pub async fn rebind_single_node_community_host( &self, normalized_host: &str, @@ -1507,6 +1547,7 @@ impl Db { /// This is the startup/config seeding path for N=1 deployments. Migrations /// create the schema only; deployment-specific hosts are not hardcoded into /// schema history. + #[sqlite_backend(implemented)] pub async fn ensure_configured_community( &self, normalized_host: &str, @@ -1542,6 +1583,7 @@ impl Db { /// Holds a per-owner advisory lock while enforcing the ownership limit. /// Identical create retries return the original record; host collisions and /// limit failures remain distinguishable to the operator API. + #[sqlite_backend(implemented)] pub async fn create_community_with_owner( &self, normalized_host: &str, @@ -1627,6 +1669,7 @@ impl Db { } /// Idempotently archives a community when the asserted pubkey is its current owner. + #[sqlite_backend(implemented)] pub async fn archive_community_owned_by( &self, normalized_host: &str, @@ -1660,6 +1703,7 @@ impl Db { } /// Idempotently restores a community when the asserted pubkey is its current owner. + #[sqlite_backend(implemented)] pub async fn unarchive_community_owned_by( &self, normalized_host: &str, @@ -1692,6 +1736,7 @@ 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. + #[sqlite_backend(implemented)] 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; @@ -1733,6 +1778,7 @@ impl Db { /// are intentionally not present rather than mapped to a default — /// callers MUST treat "channel-id not in map" as a coverage breach, /// never as "use the resolved community". + #[sqlite_backend(implemented)] pub async fn communities_of_channels( &self, channel_ids: &[Uuid], @@ -1767,6 +1813,7 @@ impl Db { } /// Inserts an event. Returns `(StoredEvent, was_inserted)` — `false` on duplicate. + #[sqlite_backend(implemented)] pub async fn insert_event( &self, community_id: CommunityId, @@ -1793,6 +1840,7 @@ impl Db { /// callers that tolerate bounded staleness should use /// [`Db::query_events_routed`] instead — converting a caller is an /// explicit, per-callsite decision, never a change to this method. + #[sqlite_backend(implemented)] pub async fn query_events(&self, q: &EventQuery) -> Result> { match &self.backend { DbBackend::SQLite(pool) => sqlite::query_events(pool, q).await, @@ -1816,6 +1864,7 @@ impl Db { /// unset, even covered-eligible queries stay on the writer, so merging /// this seam is a true no-op until the budget is configured. Every /// failure fails closed to the writer. + #[sqlite_backend(implemented)] pub async fn query_events_routed( &self, path: &'static str, @@ -1853,6 +1902,7 @@ impl Db { /// display page absorbs that per-row; a number derived from the rows /// does not. Same classification-table requirement as /// [`Db::query_events_routed`]. + #[sqlite_backend(implemented)] pub async fn query_events_routed_bounded( &self, path: &'static str, @@ -1883,6 +1933,7 @@ impl Db { /// /// Always reads from the WRITER pool — see [`Db::query_events`] for the /// writer-vs-routed rule. + #[sqlite_backend(implemented)] pub async fn count_events(&self, q: &EventQuery) -> Result { match &self.backend { DbBackend::SQLite(pool) => sqlite::count_events(pool, q).await, @@ -1900,6 +1951,7 @@ impl Db { /// inflated number for up to `FENCE_STALENESS` is a different product /// statement than a page briefly showing a deleted row. `Bounded` ties /// the error to the accepted budget `B`. + #[sqlite_backend(implemented)] pub async fn count_events_routed(&self, path: &'static str, q: &EventQuery) -> Result { if let DbBackend::SQLite(pool) = &self.backend { return sqlite::count_events(pool, q).await; @@ -1924,6 +1976,7 @@ impl Db { /// Return whether a creator-signed huddle-start event links a parent /// channel to an ephemeral huddle channel. + #[sqlite_backend(implemented)] pub async fn huddle_started_link_exists( &self, community_id: CommunityId, @@ -1960,6 +2013,7 @@ impl Db { /// Uses canonical NIP-16 ordering: `created_at DESC, id ASC`. /// This matches the write path in [`replace_addressable_event`] and handles /// historical duplicate survivors correctly. + #[sqlite_backend(implemented)] pub async fn get_latest_global_replaceable( &self, community_id: CommunityId, @@ -1985,6 +2039,7 @@ impl Db { /// Fetches a single non-deleted event by its raw ID bytes. /// /// Returns `None` if the event does not exist or has been soft-deleted. + #[sqlite_backend(implemented)] pub async fn get_event_by_id( &self, community_id: CommunityId, @@ -1999,6 +2054,7 @@ impl Db { } /// Fetches a single event by its raw ID bytes, **including soft-deleted rows**. + #[sqlite_backend(implemented)] pub async fn get_event_by_id_including_deleted( &self, community_id: CommunityId, @@ -2014,6 +2070,7 @@ impl Db { } /// Soft-deletes an event. Returns `Ok(true)` if deleted, `Ok(false)` if already deleted. + #[sqlite_backend(implemented)] pub async fn soft_delete_event( &self, community_id: CommunityId, @@ -2033,6 +2090,7 @@ impl Db { /// when it is not newer than the deletion request. /// Used by NIP-09 a-tag deletion for parameterized-replaceable kinds; /// `deletion_created_at_secs` is the deletion event's `created_at`. + #[sqlite_backend(implemented)] pub async fn soft_delete_by_coordinate( &self, community_id: CommunityId, @@ -2060,6 +2118,7 @@ impl Db { } /// Atomically soft-delete an event and decrement thread reply counters. + #[sqlite_backend(implemented)] pub async fn soft_delete_event_and_update_thread( &self, community_id: CommunityId, @@ -2092,6 +2151,7 @@ impl Db { } /// Returns the most recent `created_at` for a channel. + #[sqlite_backend(implemented)] pub async fn get_last_message_at( &self, community_id: CommunityId, @@ -2108,6 +2168,7 @@ impl Db { } /// Bulk-fetch the most recent `created_at` for a set of channel IDs. + #[sqlite_backend(implemented)] pub async fn get_last_message_at_bulk( &self, community_id: CommunityId, @@ -2124,6 +2185,7 @@ impl Db { } /// Batch-fetch non-deleted events by their raw IDs. + #[sqlite_backend(implemented)] pub async fn get_events_by_ids( &self, community_id: CommunityId, @@ -2144,6 +2206,7 @@ impl Db { /// channel pin, so no fence floor can prove insert-completeness — the /// covered arm is structurally unavailable. Used for FTS hit hydration, /// where a missing row degrades to a skipped search hit downstream. + #[sqlite_backend(implemented)] pub async fn get_events_by_ids_routed( &self, path: &'static str, @@ -2174,6 +2237,7 @@ impl Db { } /// Exclusively claim a batch of due matcher jobs from one community. + #[sqlite_backend(implemented)] pub async fn claim_due_push_match_batch( &self, limit: i64, @@ -2183,6 +2247,7 @@ impl Db { } /// Load active endpoint-enabled leases eligible for push matching. + #[sqlite_backend(implemented)] pub async fn active_push_match_leases( &self, community: CommunityId, @@ -2191,6 +2256,7 @@ impl Db { } /// Complete matcher jobs from one claimed batch while the fence holds. + #[sqlite_backend(implemented)] pub async fn complete_push_match_batch( &self, community: CommunityId, @@ -2201,6 +2267,7 @@ impl Db { } /// Release fenced matcher claims from one batch for retry. + #[sqlite_backend(implemented)] pub async fn retry_push_match_batch( &self, community: CommunityId, @@ -2212,11 +2279,13 @@ impl Db { } /// Delete exhausted matcher jobs (periodic sweep, off the claim path). + #[sqlite_backend(implemented)] pub async fn reap_exhausted_push_matches(&self) -> Result { push::reap_exhausted_matches(self.pg_pool()?).await } /// Idempotently enqueue a wake for a matched lease and event. + #[sqlite_backend(implemented)] pub async fn enqueue_push_wake( &self, community: CommunityId, @@ -2228,6 +2297,7 @@ impl Db { } /// Set-wise [`Self::enqueue_push_wake`]: one transaction per batch. + #[sqlite_backend(implemented)] pub async fn enqueue_push_wakes( &self, community: CommunityId, @@ -2237,6 +2307,7 @@ impl Db { } /// Exclusively claim due wake jobs for one community. + #[sqlite_backend(implemented)] pub async fn claim_due_push_wakes( &self, community: CommunityId, @@ -2247,6 +2318,7 @@ impl Db { } /// Revalidate a wake's claim, source event, and current lease before send. + #[sqlite_backend(implemented)] pub async fn revalidate_push_wake( &self, community: CommunityId, @@ -2257,6 +2329,7 @@ impl Db { } /// Mark a fenced wake claim delivered. + #[sqlite_backend(implemented)] pub async fn complete_push_wake( &self, community: CommunityId, @@ -2267,6 +2340,7 @@ impl Db { } /// Release a fenced wake claim for retry at the supplied time. + #[sqlite_backend(implemented)] pub async fn retry_push_wake( &self, community: CommunityId, @@ -2278,6 +2352,7 @@ impl Db { } /// Mark a fenced wake claim terminally failed. + #[sqlite_backend(implemented)] pub async fn fail_push_wake( &self, community: CommunityId, @@ -2288,6 +2363,7 @@ impl Db { } /// Disable an endpoint only if the specified lease generation is current. + #[sqlite_backend(implemented)] pub async fn disable_push_endpoint( &self, community: CommunityId, @@ -2307,6 +2383,7 @@ impl Db { /// Atomically persist a validated kind:30350 event and its effective lease. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn accept_push_lease_event( &self, community: CommunityId, @@ -2329,6 +2406,7 @@ impl Db { } /// Atomically insert an event AND its thread metadata in a single transaction. + #[sqlite_backend(implemented)] pub async fn insert_event_with_thread_metadata( &self, community_id: CommunityId, @@ -2365,6 +2443,7 @@ impl Db { /// Atomically insert a kind:7 reaction event and its reaction row. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn insert_reaction_event_with_thread_metadata( &self, community_id: CommunityId, @@ -2413,6 +2492,7 @@ impl Db { /// Creates a new channel, bootstraps the creator as owner, and returns the record. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn create_channel( &self, community_id: CommunityId, @@ -2457,6 +2537,7 @@ impl Db { /// /// Returns `(record, true)` if newly created, `(record, false)` if already exists. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn create_channel_with_id( &self, community_id: CommunityId, @@ -2497,6 +2578,7 @@ impl Db { } /// Fetches a channel record by ID. + #[sqlite_backend(implemented)] pub async fn get_channel( &self, community_id: CommunityId, @@ -2511,6 +2593,7 @@ impl Db { } /// Returns the canvas content for a channel, if any. + #[sqlite_backend(implemented)] pub async fn get_canvas( &self, community_id: CommunityId, @@ -2525,6 +2608,7 @@ impl Db { } /// Sets or clears the canvas content for a channel. + #[sqlite_backend(implemented)] pub async fn set_canvas( &self, community_id: CommunityId, @@ -2542,6 +2626,7 @@ impl Db { } /// Adds a member to a channel. + #[sqlite_backend(implemented)] pub async fn add_member( &self, community_id: CommunityId, @@ -2566,6 +2651,7 @@ impl Db { } /// Removes a member from a channel. + #[sqlite_backend(implemented)] pub async fn remove_member( &self, community_id: CommunityId, @@ -2591,6 +2677,7 @@ impl Db { } /// Returns `true` if the pubkey is an active member. + #[sqlite_backend(implemented)] pub async fn is_member( &self, community_id: CommunityId, @@ -2609,6 +2696,7 @@ impl Db { /// Return the active (channel, pubkey) membership pairs among the given /// sets, in one statement. + #[sqlite_backend(implemented)] pub async fn membership_pairs( &self, community_id: CommunityId, @@ -2626,6 +2714,7 @@ impl Db { } /// Returns all active members of a channel. + #[sqlite_backend(implemented)] pub async fn get_members( &self, community_id: CommunityId, @@ -2640,6 +2729,7 @@ impl Db { } /// Returns active members for multiple channels in a single query. + #[sqlite_backend(implemented)] pub async fn get_members_bulk( &self, community_id: CommunityId, @@ -2656,6 +2746,7 @@ impl Db { } /// Get all channel IDs accessible to a pubkey. + #[sqlite_backend(implemented)] pub async fn get_accessible_channel_ids( &self, community_id: CommunityId, @@ -2672,6 +2763,7 @@ impl Db { } /// Lists channels, optionally filtered by visibility. + #[sqlite_backend(implemented)] pub async fn list_channels( &self, community_id: CommunityId, @@ -2686,6 +2778,7 @@ impl Db { } /// Returns full channel records for all channels a user can access. + #[sqlite_backend(implemented)] pub async fn get_accessible_channels( &self, community_id: CommunityId, @@ -2718,6 +2811,7 @@ impl Db { } /// Returns all bot-role members with their aggregated channel names in one community. + #[sqlite_backend(implemented)] pub async fn get_bot_members( &self, community_id: CommunityId, @@ -2729,6 +2823,7 @@ impl Db { } /// Bulk-fetch user records by pubkey. + #[sqlite_backend(implemented)] pub async fn get_users_bulk( &self, community_id: CommunityId, @@ -2743,6 +2838,7 @@ impl Db { } /// Updates a channel's name and/or description. + #[sqlite_backend(implemented)] pub async fn update_channel( &self, community_id: CommunityId, @@ -2760,6 +2856,7 @@ impl Db { } /// Sets the topic for a channel. + #[sqlite_backend(implemented)] pub async fn set_topic( &self, community_id: CommunityId, @@ -2778,6 +2875,7 @@ impl Db { } /// Sets the purpose for a channel. + #[sqlite_backend(implemented)] pub async fn set_purpose( &self, community_id: CommunityId, @@ -2797,6 +2895,7 @@ impl Db { } /// Archives a channel. + #[sqlite_backend(implemented)] pub async fn archive_channel(&self, community_id: CommunityId, channel_id: Uuid) -> Result<()> { match &self.backend { DbBackend::SQLite(pool) => { @@ -2809,6 +2908,7 @@ impl Db { } /// Unarchives a channel. + #[sqlite_backend(implemented)] pub async fn unarchive_channel( &self, community_id: CommunityId, @@ -2825,6 +2925,7 @@ impl Db { } /// Soft-delete a channel. + #[sqlite_backend(implemented)] pub async fn soft_delete_channel( &self, community_id: CommunityId, @@ -2841,6 +2942,7 @@ impl Db { } /// Returns the count of active members in a channel. + #[sqlite_backend(implemented)] pub async fn get_member_count( &self, community_id: CommunityId, @@ -2857,6 +2959,7 @@ impl Db { } /// Bulk-fetch member counts for a set of channel IDs. + #[sqlite_backend(implemented)] pub async fn get_member_counts_bulk( &self, community_id: CommunityId, @@ -2873,6 +2976,7 @@ impl Db { } /// Get the active role of a pubkey in a channel. + #[sqlite_backend(implemented)] pub async fn get_member_role( &self, community_id: CommunityId, @@ -2890,6 +2994,7 @@ impl Db { } /// Archive ephemeral channels whose TTL deadline has passed. + #[sqlite_backend(implemented)] pub async fn reap_expired_ephemeral_channels( &self, ) -> Result> { @@ -2900,6 +3005,7 @@ impl Db { } /// Query due reminders ready for delivery. + #[sqlite_backend(implemented)] pub async fn query_due_reminders( &self, now_secs: i64, @@ -2916,6 +3022,7 @@ impl Db { } /// Atomically claim a due reminder for delivery (cross-pod dedup). + #[sqlite_backend(implemented)] pub async fn claim_due_reminder( &self, community_id: CommunityId, @@ -2941,6 +3048,7 @@ impl Db { } /// Atomically claim a due reminder using a caller-supplied delivery stamp. + #[sqlite_backend(implemented)] pub async fn claim_due_reminder_with_stamp( &self, community_id: CommunityId, @@ -2973,6 +3081,7 @@ impl Db { } /// Release a claimed due reminder after a publish failure. + #[sqlite_backend(implemented)] pub async fn release_due_reminder( &self, community_id: CommunityId, @@ -3009,6 +3118,7 @@ impl Db { /// Returns `true` if a new row was inserted (first time), `false` if it /// already existed. Callers use the `true` return to increment /// `buzz_users_created_total`. + #[sqlite_backend(implemented)] pub async fn ensure_user(&self, community_id: CommunityId, pubkey: &[u8]) -> Result { match &self.backend { DbBackend::SQLite(pool) => sqlite::ensure_user(pool, community_id, pubkey).await, @@ -3017,6 +3127,7 @@ impl Db { } /// Get a single user record by pubkey. + #[sqlite_backend(implemented)] pub async fn get_user( &self, community_id: CommunityId, @@ -3029,6 +3140,7 @@ impl Db { } /// Update a user's profile fields. + #[sqlite_backend(implemented)] pub async fn update_user_profile( &self, community_id: CommunityId, @@ -3067,6 +3179,7 @@ impl Db { } /// Look up a user by NIP-05 handle. + #[sqlite_backend(implemented)] pub async fn get_user_by_nip05( &self, community_id: CommunityId, @@ -3084,6 +3197,7 @@ impl Db { } /// Search users by display name, NIP-05 handle, or pubkey prefix. + #[sqlite_backend(implemented)] pub async fn search_users( &self, community_id: CommunityId, @@ -3100,6 +3214,7 @@ impl Db { /// Atomically set agent owner — only if no owner is currently assigned. /// Returns Ok(true) if set, Ok(false) if an owner already exists. + #[sqlite_backend(implemented)] pub async fn set_agent_owner( &self, community_id: CommunityId, @@ -3118,6 +3233,7 @@ impl Db { } /// Get the channel_add_policy and agent_owner_pubkey for a user. + #[sqlite_backend(implemented)] pub async fn get_agent_channel_policy( &self, community_id: CommunityId, @@ -3134,6 +3250,7 @@ impl Db { } /// Check whether `actor_pubkey` is the agent owner of `target_pubkey`. + #[sqlite_backend(implemented)] pub async fn is_agent_owner( &self, community_id: CommunityId, @@ -3152,6 +3269,7 @@ impl Db { } /// Set the channel_add_policy for a user. + #[sqlite_backend(implemented)] pub async fn set_channel_add_policy( &self, community_id: CommunityId, @@ -3169,6 +3287,7 @@ impl Db { } /// Find an existing DM by its participant hash. + #[sqlite_backend(implemented)] pub async fn find_dm_by_participants( &self, community_id: CommunityId, @@ -3185,6 +3304,7 @@ impl Db { } /// Create or return an existing DM channel. + #[sqlite_backend(implemented)] pub async fn create_dm( &self, community_id: CommunityId, @@ -3202,6 +3322,7 @@ impl Db { } /// List all DMs for a user. + #[sqlite_backend(implemented)] pub async fn list_dms_for_user( &self, community_id: CommunityId, @@ -3220,6 +3341,7 @@ impl Db { } /// Open or retrieve a DM for the given participants. + #[sqlite_backend(implemented)] pub async fn open_dm( &self, community_id: CommunityId, @@ -3240,6 +3362,7 @@ impl Db { /// Atomically persist a SQLite DM-open command and its channel mutation. /// `None` means the command event was already processed. + #[sqlite_backend(implemented)] pub async fn open_dm_sqlite_command( &self, community_id: CommunityId, @@ -3257,6 +3380,7 @@ impl Db { /// Atomically persist a SQLite DM-hide command and its visibility mutation. /// `None` means either PostgreSQL or a command event already processed. + #[sqlite_backend(implemented)] pub async fn hide_dm_sqlite_command( &self, community_id: CommunityId, @@ -3276,6 +3400,7 @@ impl Db { /// /// The DM is not deleted — it can be restored by opening a new DM with /// the same participants. + #[sqlite_backend(implemented)] pub async fn hide_dm( &self, community_id: CommunityId, @@ -3293,6 +3418,7 @@ impl Db { } /// Unhide a DM channel for a specific user. + #[sqlite_backend(implemented)] pub async fn unhide_dm( &self, community_id: CommunityId, @@ -3310,6 +3436,7 @@ impl Db { } /// List the channel IDs of all DMs the given user currently has hidden. + #[sqlite_backend(implemented)] pub async fn list_hidden_dms( &self, community_id: CommunityId, @@ -3323,6 +3450,7 @@ impl Db { /// Insert thread metadata. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn insert_thread_metadata( &self, community_id: CommunityId, @@ -3389,6 +3517,7 @@ impl Db { /// A head fetch routed under Predicate A skips the re-run: bounded /// staleness (missing at most the freshest budget-window of replies) is /// exactly the semantic the head gate accepts. + #[sqlite_backend(implemented)] pub async fn get_thread_replies( &self, community_id: CommunityId, @@ -3474,6 +3603,7 @@ impl Db { } /// Fetch aggregated thread stats. + #[sqlite_backend(implemented)] pub async fn get_thread_summary( &self, community_id: CommunityId, @@ -3493,6 +3623,7 @@ impl Db { /// /// Convenience wrapper over [`Db::get_channel_window_with_session`] for /// callers with no follow-up queries; the serving session is released. + #[sqlite_backend(implemented)] pub async fn get_channel_window( &self, community_id: CommunityId, @@ -3532,6 +3663,7 @@ impl Db { /// /// Every failure fails closed to the writer and is recorded in /// `buzz_db_route_decision`. + #[sqlite_backend(implemented)] pub async fn get_channel_window_with_session( &self, community_id: CommunityId, @@ -3714,6 +3846,7 @@ impl Db { } /// Look up a single thread_metadata row by event_id. + #[sqlite_backend(implemented)] pub async fn get_thread_metadata_by_event( &self, community_id: CommunityId, @@ -3730,6 +3863,7 @@ impl Db { } /// Decrement reply counts. + #[sqlite_backend(implemented)] pub async fn decrement_reply_count( &self, community_id: CommunityId, @@ -3754,6 +3888,7 @@ impl Db { } /// Add (or re-activate) a reaction. + #[sqlite_backend(implemented)] pub async fn add_reaction( &self, community: CommunityId, @@ -3792,6 +3927,7 @@ impl Db { } /// Soft-delete a reaction. + #[sqlite_backend(implemented)] pub async fn remove_reaction( &self, community: CommunityId, @@ -3823,6 +3959,7 @@ impl Db { } /// Soft-delete a reaction by its source event ID. + #[sqlite_backend(implemented)] pub async fn remove_reaction_by_source_event_id( &self, community: CommunityId, @@ -3837,6 +3974,7 @@ impl Db { } /// Look up the active reaction row for one actor + emoji + target tuple. + #[sqlite_backend(implemented)] pub async fn get_active_reaction_record( &self, community: CommunityId, @@ -3872,6 +4010,7 @@ impl Db { } /// Backfill the source event ID on an active reaction row. + #[sqlite_backend(implemented)] pub async fn set_reaction_event_id( &self, community: CommunityId, @@ -3910,6 +4049,7 @@ impl Db { } /// Get all active reactions for an event, grouped by emoji. + #[sqlite_backend(implemented)] pub async fn get_reactions( &self, community: CommunityId, @@ -3938,6 +4078,7 @@ impl Db { } /// Batch-fetch emoji counts for a set of (event_id, event_created_at) pairs. + #[sqlite_backend(implemented)] pub async fn get_reactions_bulk( &self, community: CommunityId, @@ -3952,6 +4093,7 @@ impl Db { } /// Find events that @mention the given pubkey. + #[sqlite_backend(implemented)] pub async fn query_feed_mentions( &self, community: CommunityId, @@ -3993,6 +4135,7 @@ impl Db { /// parameter admits community-global rows alongside channel rows, so no /// single channel's fence floor can prove completeness — the covered arm /// is structurally unavailable, not merely unchosen. + #[sqlite_backend(implemented)] pub async fn query_feed_mentions_routed( &self, path: &'static str, @@ -4059,6 +4202,7 @@ impl Db { } /// Find events that require action from the given pubkey. + #[sqlite_backend(implemented)] pub async fn query_feed_needs_action( &self, community: CommunityId, @@ -4096,6 +4240,7 @@ impl Db { /// [`Db::query_feed_needs_action`] with replica routing — BOUNDED arm /// only; see [`Db::query_feed_mentions_routed`] for why the covered arm /// is structurally unavailable to feed queries. + #[sqlite_backend(implemented)] pub async fn query_feed_needs_action_routed( &self, path: &'static str, @@ -4162,6 +4307,7 @@ impl Db { } /// Find recent activity across accessible channels. + #[sqlite_backend(implemented)] pub async fn query_feed_activity( &self, community: CommunityId, @@ -4190,6 +4336,7 @@ impl Db { /// [`Db::query_feed_activity`] with replica routing — BOUNDED arm only; /// see [`Db::query_feed_mentions_routed`] for why the covered arm is /// structurally unavailable to feed queries. + #[sqlite_backend(implemented)] pub async fn query_feed_activity_routed( &self, path: &'static str, @@ -4252,6 +4399,7 @@ impl Db { /// Create a new API token record. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn create_api_token( &self, community_id: CommunityId, @@ -4294,6 +4442,7 @@ impl Db { /// Atomic conditional INSERT with 10-token limit (per (community, owner)). #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn create_api_token_if_under_limit( &self, community_id: CommunityId, @@ -4340,6 +4489,7 @@ impl Db { /// See [`api_token::get_api_token_by_hash_including_revoked`] for the /// row-44 conformance rationale — the `(community_id, token_hash)` key /// is enforced both by the storage UNIQUE index and by this WHERE clause. + #[sqlite_backend(implemented)] pub async fn get_api_token_by_hash( &self, community_id: CommunityId, @@ -4368,6 +4518,7 @@ impl Db { } /// Look up an API token by hash, including revoked, scoped to community. + #[sqlite_backend(implemented)] pub async fn get_api_token_by_hash_including_revoked( &self, community_id: CommunityId, @@ -4389,6 +4540,7 @@ impl Db { } /// Record a token usage (update `last_used_at`), scoped to community. + #[sqlite_backend(implemented)] pub async fn touch_api_token(&self, community_id: CommunityId, hash: &[u8]) -> Result<()> { match &self.backend { DbBackend::SQLite(pool) => sqlite::touch_api_token(pool, community_id, hash).await, @@ -4400,6 +4552,7 @@ impl Db { } /// Alias for [`Self::touch_api_token`]. + #[sqlite_backend(implemented)] pub async fn update_token_last_used( &self, community_id: CommunityId, @@ -4409,6 +4562,7 @@ impl Db { } /// List all active (non-revoked) tokens in a community, newest first. + #[sqlite_backend(implemented)] pub async fn list_active_tokens(&self, community_id: CommunityId) -> Result> { match &self.backend { DbBackend::SQLite(pool) => sqlite::list_active_tokens(pool, community_id).await, @@ -4448,6 +4602,7 @@ impl Db { } /// List all tokens for a (community, owner) pair (including revoked). + #[sqlite_backend(implemented)] pub async fn list_tokens_by_owner( &self, community_id: CommunityId, @@ -4465,6 +4620,7 @@ impl Db { } /// Revoke a single token by ID, scoped to (community, owner). + #[sqlite_backend(implemented)] pub async fn revoke_token( &self, community_id: CommunityId, @@ -4490,6 +4646,7 @@ impl Db { } /// Revoke all active tokens for a (community, owner) pair. + #[sqlite_backend(implemented)] pub async fn revoke_all_tokens( &self, community_id: CommunityId, @@ -4513,6 +4670,7 @@ impl Db { } /// Create a new workflow. + #[sqlite_backend(implemented)] pub async fn create_workflow( &self, community_id: CommunityId, @@ -4553,6 +4711,7 @@ impl Db { /// Atomically persist a SQLite workflow-definition command and its upsert. /// `None` means either PostgreSQL or a stale/duplicate command event. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn upsert_workflow_sqlite_command( &self, community_id: CommunityId, @@ -4587,6 +4746,7 @@ impl Db { /// Insert or update a workflow using its NIP-33 `d`-tag UUID. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn upsert_workflow( &self, community_id: CommunityId, @@ -4628,6 +4788,7 @@ impl Db { } /// Fetch a single workflow by ID, scoped to its community. + #[sqlite_backend(implemented)] pub async fn get_workflow( &self, community_id: CommunityId, @@ -4640,6 +4801,7 @@ impl Db { } /// List workflows for a channel. + #[sqlite_backend(implemented)] pub async fn list_channel_workflows( &self, community_id: CommunityId, @@ -4665,6 +4827,7 @@ impl Db { } /// List active, enabled workflows for a channel. + #[sqlite_backend(implemented)] pub async fn list_enabled_channel_workflows( &self, community_id: CommunityId, @@ -4682,6 +4845,7 @@ impl Db { } /// List all active, enabled schedule-triggered workflows. + #[sqlite_backend(implemented)] pub async fn list_all_enabled_workflows(&self) -> Result> { match &self.backend { DbBackend::SQLite(pool) => sqlite::list_all_enabled_workflows(pool).await, @@ -4697,6 +4861,7 @@ impl Db { /// from the scheduler scan), never client-supplied — `workflows` is keyed /// `(community_id, id)`, so the claim must bind both to avoid fanning /// across communities that share the workflow UUID. + #[sqlite_backend(implemented)] pub async fn claim_scheduled_workflow_fire( &self, community_id: CommunityId, @@ -4726,6 +4891,7 @@ impl Db { } /// Fetch the latest claimed schedule instant for interval trigger anchoring. + #[sqlite_backend(implemented)] pub async fn latest_scheduled_workflow_fire( &self, community_id: CommunityId, @@ -4743,6 +4909,7 @@ impl Db { } /// Attach the workflow run id created from a won scheduled-fire claim. + #[sqlite_backend(implemented)] pub async fn attach_scheduled_workflow_run( &self, community_id: CommunityId, @@ -4775,6 +4942,7 @@ impl Db { } /// Delete old scheduled workflow fire claims before a retention cutoff. + #[sqlite_backend(implemented)] pub async fn prune_scheduled_workflow_fires_before( &self, older_than: chrono::DateTime, @@ -4790,6 +4958,7 @@ impl Db { } /// Update a workflow's name, definition, and hash. + #[sqlite_backend(implemented)] pub async fn update_workflow( &self, community_id: CommunityId, @@ -4825,6 +4994,7 @@ impl Db { } /// Update a workflow's status. + #[sqlite_backend(implemented)] pub async fn update_workflow_status( &self, community_id: CommunityId, @@ -4842,6 +5012,7 @@ impl Db { } /// Enable or disable a workflow. + #[sqlite_backend(implemented)] pub async fn set_workflow_enabled( &self, community_id: CommunityId, @@ -4860,6 +5031,7 @@ impl Db { /// Disable all of an owner's workflows in a channel (SEC-006, on /// membership loss). Returns the number of workflows disabled. + #[sqlite_backend(implemented)] pub async fn disable_workflows_for_owner_in_channel( &self, community_id: CommunityId, @@ -4889,6 +5061,7 @@ impl Db { } /// Delete a workflow and all its runs/approvals. + #[sqlite_backend(implemented)] pub async fn delete_workflow(&self, community_id: CommunityId, id: Uuid) -> Result<()> { match &self.backend { DbBackend::SQLite(pool) => sqlite::delete_workflow(pool, community_id, id).await, @@ -4900,6 +5073,7 @@ impl Db { /// Delete a workflow only when it belongs to the provided owner. /// Returns the deleted workflow's `channel_id`. + #[sqlite_backend(implemented)] pub async fn delete_workflow_for_owner( &self, community_id: CommunityId, @@ -4919,6 +5093,7 @@ impl Db { /// Find a workflow by owner pubkey and name within a community. Used for /// NIP-09 a-tag deletion where the d-tag is the workflow name (not UUID). + #[sqlite_backend(implemented)] pub async fn find_workflow_by_owner_and_name( &self, community_id: CommunityId, @@ -4939,6 +5114,7 @@ impl Db { /// Atomically persist a SQLite workflow-trigger command and create its run. /// `None` means either PostgreSQL or a command event already processed. + #[sqlite_backend(implemented)] pub async fn create_workflow_run_sqlite_command( &self, community_id: CommunityId, @@ -4966,6 +5142,7 @@ impl Db { } /// Create a new workflow run. + #[sqlite_backend(implemented)] pub async fn create_workflow_run( &self, community_id: CommunityId, @@ -4998,6 +5175,7 @@ impl Db { } /// Fetch a single workflow run, scoped to its community. + #[sqlite_backend(implemented)] pub async fn get_workflow_run( &self, community_id: CommunityId, @@ -5012,6 +5190,7 @@ impl Db { } /// List runs for a workflow. + #[sqlite_backend(implemented)] pub async fn list_workflow_runs( &self, community_id: CommunityId, @@ -5030,6 +5209,7 @@ impl Db { } /// Update a workflow run's status. + #[sqlite_backend(implemented)] pub async fn update_workflow_run( &self, community_id: CommunityId, @@ -5068,6 +5248,7 @@ impl Db { } /// Create an approval request. + #[sqlite_backend(implemented)] pub async fn create_approval(&self, params: workflow::CreateApprovalParams<'_>) -> Result<()> { match &self.backend { DbBackend::SQLite(pool) => sqlite::create_approval(pool, params).await, @@ -5076,6 +5257,7 @@ impl Db { } /// Fetch an approval by raw token. + #[sqlite_backend(implemented)] pub async fn get_approval( &self, community_id: CommunityId, @@ -5090,6 +5272,7 @@ impl Db { } /// Fetch an approval by its already-hashed token (no re-hashing). + #[sqlite_backend(implemented)] pub async fn get_approval_by_stored_hash( &self, community_id: CommunityId, @@ -5107,6 +5290,7 @@ impl Db { } /// Fetch all approvals for a workflow run. + #[sqlite_backend(implemented)] pub async fn get_run_approvals( &self, community_id: CommunityId, @@ -5125,6 +5309,7 @@ impl Db { } /// Update an approval's status. + #[sqlite_backend(implemented)] pub async fn update_approval( &self, community_id: CommunityId, @@ -5154,6 +5339,7 @@ impl Db { /// Atomically persist a SQLite approval command and update its approval. /// `None` means either PostgreSQL or a command event already processed. + #[sqlite_backend(implemented)] pub async fn update_approval_sqlite_command( &self, community_id: CommunityId, @@ -5181,6 +5367,7 @@ impl Db { } /// Update an approval by its already-hashed token (no re-hashing). + #[sqlite_backend(implemented)] pub async fn update_approval_by_stored_hash( &self, community_id: CommunityId, @@ -5219,6 +5406,7 @@ impl Db { /// /// SQLite stores events in an unpartitioned local table, so this is an /// intentional single-node no-op. + #[sqlite_backend(implemented)] pub async fn ensure_future_partitions(&self, months_ahead: u32) -> Result<()> { match &self.backend { DbBackend::SQLite(_) => Ok(()), @@ -5234,6 +5422,7 @@ impl Db { /// Runs a single UPDATE touching only NIP-33 rows with NULL d_tag. SQLite /// derives d-tags from the persisted event JSON and has no denormalized /// `d_tag` column, so its profile deliberately returns zero. + #[sqlite_backend(implemented)] pub async fn backfill_d_tags(&self) -> Result { if matches!(&self.backend, DbBackend::SQLite(_)) { return Ok(0); @@ -5253,6 +5442,7 @@ impl Db { } /// Check if a pubkey is in the allowlist for `community`. + #[sqlite_backend(implemented)] pub async fn is_pubkey_allowed(&self, community: CommunityId, pubkey: &[u8]) -> Result { if let DbBackend::SQLite(pool) = &self.backend { return sqlite::is_pubkey_allowed(pool, community, pubkey).await; @@ -5269,6 +5459,7 @@ impl Db { } /// Check if the community allowlist has any entries (i.e. is enforcement active). + #[sqlite_backend(implemented)] pub async fn has_allowlist_entries(&self, community: CommunityId) -> Result { if let DbBackend::SQLite(pool) = &self.backend { return sqlite::has_allowlist_entries(pool, community).await; @@ -5283,6 +5474,7 @@ impl Db { } /// Add a pubkey to the community allowlist. + #[sqlite_backend(implemented)] pub async fn add_to_allowlist( &self, community: CommunityId, @@ -5307,6 +5499,7 @@ impl Db { } /// Remove a pubkey from the community allowlist. + #[sqlite_backend(implemented)] pub async fn remove_from_allowlist( &self, community: CommunityId, @@ -5325,6 +5518,7 @@ impl Db { } /// List all pubkeys in the community allowlist. + #[sqlite_backend(implemented)] pub async fn list_allowlist(&self, community: CommunityId) -> Result> { if let DbBackend::SQLite(pool) = &self.backend { return sqlite::list_allowlist(pool, community).await; @@ -5356,6 +5550,7 @@ impl Db { /// `B`; everything else fails closed to the writer, exactly like /// [`Db::query_events_routed_bounded`]. Not precedent for routing other /// permission reads. + #[sqlite_backend(implemented)] pub async fn is_relay_member(&self, community: CommunityId, pubkey: &str) -> Result { match &self.backend { DbBackend::SQLite(pool) => sqlite::is_relay_member(pool, community, pubkey).await, @@ -5388,6 +5583,7 @@ impl Db { } /// Returns the relay member record for `pubkey` in `community`, or `None` if not found. + #[sqlite_backend(implemented)] pub async fn get_relay_member( &self, community: CommunityId, @@ -5402,6 +5598,7 @@ impl Db { } /// Returns all relay members of `community` ordered by `created_at` ascending. + #[sqlite_backend(implemented)] pub async fn list_relay_members( &self, community: CommunityId, @@ -5418,6 +5615,7 @@ impl Db { /// /// Returns `true` if the row was actually inserted, `false` if the pubkey /// already existed in `community` (idempotent — `ON CONFLICT DO NOTHING`). + #[sqlite_backend(implemented)] pub async fn add_relay_member( &self, community: CommunityId, @@ -5438,6 +5636,7 @@ impl Db { /// Claims relay membership via an invite and atomically persists the /// accepted policy version when a policy is configured. + #[sqlite_backend(implemented)] pub async fn claim_relay_membership( &self, community: CommunityId, @@ -5463,6 +5662,7 @@ impl Db { } /// Returns whether a member has persisted acceptance evidence for a policy version. + #[sqlite_backend(implemented)] pub async fn has_join_policy_acceptance( &self, community: CommunityId, @@ -5486,6 +5686,7 @@ impl Db { } /// Removes a relay member from `community` atomically, refusing to delete the owner. + #[sqlite_backend(implemented)] pub async fn remove_relay_member( &self, community: CommunityId, @@ -5505,6 +5706,7 @@ impl Db { /// /// Atomic conditional delete — eliminates the TOCTOU race between a /// prior role read and the delete. See [`relay_members::remove_relay_member_if_role`]. + #[sqlite_backend(implemented)] pub async fn remove_relay_member_if_role( &self, community: CommunityId, @@ -5528,6 +5730,7 @@ impl Db { } /// Updates the role of an existing relay member in `community`. Returns `true` if updated. + #[sqlite_backend(implemented)] pub async fn update_relay_member_role( &self, community: CommunityId, @@ -5551,6 +5754,7 @@ impl Db { } /// Ensures the owner pubkey exists with role `"owner"` in `community`. Called at startup. + #[sqlite_backend(implemented)] pub async fn bootstrap_owner(&self, community: CommunityId, owner_pubkey: &str) -> Result<()> { match &self.backend { DbBackend::SQLite(pool) => sqlite::bootstrap_owner(pool, community, owner_pubkey).await, @@ -5570,6 +5774,7 @@ impl Db { /// demoting the previous owner(s) to `member`. Verifies /// `expected_owner_pubkey` matches the current owner inside the same /// transaction to prevent stale-owner races. + #[sqlite_backend(implemented)] pub async fn transfer_ownership( &self, community: CommunityId, @@ -5589,6 +5794,7 @@ impl Db { /// /// Idempotent — uses `ON CONFLICT DO NOTHING`. Returns the number of rows /// inserted, or 0 if the `pubkey_allowlist` table doesn't exist. + #[sqlite_backend(implemented)] pub async fn backfill_from_allowlist(&self, community: CommunityId) -> Result { relay_members::backfill_from_allowlist(self.pg_pool()?, community).await } @@ -5598,6 +5804,7 @@ impl Db { /// /// `max_uses` is `None` for unlimited or `Some(1..=10000)`. /// `ttl_secs` must be in the shared invite lifetime range. + #[sqlite_backend(implemented)] pub async fn mint_relay_invite( &self, community: CommunityId, @@ -5623,6 +5830,7 @@ impl Db { } /// Delete one bounded batch of invites expired before `cutoff`. + #[sqlite_backend(implemented)] pub async fn reap_expired_relay_invites( &self, cutoff: chrono::DateTime, @@ -5640,6 +5848,7 @@ impl Db { /// transaction with `FOR UPDATE` on the invite row. /// /// `token_hash` is the SHA-256 of the presented v2 code (32 bytes). + #[sqlite_backend(implemented)] pub async fn claim_relay_invite( &self, community: CommunityId, @@ -5672,6 +5881,7 @@ impl Db { } /// Sidecar an accepted product-feedback event, idempotent by event id. + #[sqlite_backend(implemented)] pub async fn insert_product_feedback( &self, community: CommunityId, @@ -5688,6 +5898,7 @@ impl Db { } /// List product feedback across the deployment, newest first. + #[sqlite_backend(implemented)] pub async fn list_product_feedback( &self, limit: i64, @@ -5699,6 +5910,7 @@ impl Db { } /// Insert a tenant-scoped NIP-56 report row, idempotent by report event id. + #[sqlite_backend(implemented)] pub async fn insert_moderation_report( &self, community: CommunityId, @@ -5715,6 +5927,7 @@ impl Db { } /// List moderation reports for a community, newest first. + #[sqlite_backend(implemented)] pub async fn list_moderation_reports( &self, community: CommunityId, @@ -5732,6 +5945,7 @@ impl Db { } /// Fetch one moderation report by row id. + #[sqlite_backend(implemented)] pub async fn get_moderation_report( &self, community: CommunityId, @@ -5748,6 +5962,7 @@ impl Db { } /// Fetch one moderation report by signed NIP-56 report event id. + #[sqlite_backend(implemented)] pub async fn get_moderation_report_by_event( &self, community: CommunityId, @@ -5764,6 +5979,7 @@ impl Db { } /// Resolve, dismiss, or escalate an open moderation report. + #[sqlite_backend(implemented)] pub async fn resolve_moderation_report( &self, community: CommunityId, @@ -5799,6 +6015,7 @@ impl Db { } /// Upsert a community ban for a member pubkey. + #[sqlite_backend(implemented)] pub async fn ban_community_member( &self, community: CommunityId, @@ -5826,6 +6043,7 @@ impl Db { } /// Lift a community ban for a member pubkey. + #[sqlite_backend(implemented)] pub async fn unban_community_member( &self, community: CommunityId, @@ -5841,6 +6059,7 @@ impl Db { } /// Upsert a community timeout/write-block for a member pubkey. + #[sqlite_backend(implemented)] pub async fn timeout_community_member( &self, community: CommunityId, @@ -5868,6 +6087,7 @@ impl Db { } /// Clear a community timeout/write-block for a member pubkey. + #[sqlite_backend(implemented)] pub async fn untimeout_community_member( &self, community: CommunityId, @@ -5885,6 +6105,7 @@ impl Db { } /// Fetch the active ban/timeout restriction state for enforcement hot paths. + #[sqlite_backend(implemented)] pub async fn moderation_restriction_state( &self, community: CommunityId, @@ -5899,6 +6120,7 @@ impl Db { } /// Fetch the full ban/timeout row for a member pubkey. + #[sqlite_backend(implemented)] pub async fn get_community_ban( &self, community: CommunityId, @@ -5911,6 +6133,7 @@ impl Db { } /// List currently restricted members in a community. + #[sqlite_backend(implemented)] pub async fn list_community_restrictions( &self, community: CommunityId, @@ -5922,6 +6145,7 @@ impl Db { } /// Insert a moderation audit action row. + #[sqlite_backend(implemented)] pub async fn insert_moderation_action( &self, community: CommunityId, @@ -5938,6 +6162,7 @@ impl Db { } /// List moderation audit action rows, newest first. + #[sqlite_backend(implemented)] pub async fn list_moderation_actions( &self, community: CommunityId, @@ -5955,6 +6180,7 @@ impl Db { /// Return the current owner of git repo name `repo_id` in `community`, or /// `None` if unreserved. See [`git_repo::repo_name_owner`]. + #[sqlite_backend(implemented)] pub async fn repo_name_owner( &self, community: CommunityId, @@ -5972,6 +6198,7 @@ impl Db { /// /// See [`git_repo::reserve_repo_name`] for the outcome semantics. The /// per-pubkey quota is enforced by the caller against `count_repos_for_owner`. + #[sqlite_backend(implemented)] pub async fn reserve_repo_name( &self, community: CommunityId, @@ -5989,6 +6216,7 @@ impl Db { } /// Count git repos reserved by `owner_pubkey` in `community` (quota check). + #[sqlite_backend(implemented)] pub async fn count_repos_for_owner( &self, community: CommunityId, @@ -6007,6 +6235,7 @@ impl Db { /// Release a git repo name reservation held by `owner_pubkey` (rollback). /// /// Returns the number of rows removed (0 or 1). See [`git_repo::release_repo_name`]. + #[sqlite_backend(implemented)] pub async fn release_repo_name( &self, community: CommunityId, @@ -6024,6 +6253,7 @@ impl Db { } /// Returns `true` if `pubkey` (64-char hex) is archived in `community_id`. + #[sqlite_backend(implemented)] pub async fn is_archived(&self, community_id: CommunityId, pubkey: &str) -> Result { match &self.backend { DbBackend::SQLite(pool) => sqlite::is_archived(pool, community_id, pubkey).await, @@ -6035,6 +6265,7 @@ impl Db { /// Archives an identity in `community_id`. Returns `true` if inserted, `false` if already archived. #[allow(clippy::too_many_arguments)] + #[sqlite_backend(implemented)] pub async fn archive( &self, community_id: CommunityId, @@ -6076,6 +6307,7 @@ impl Db { } /// Unarchives an identity from `community_id`. Returns `true` if deleted, `false` if absent. + #[sqlite_backend(implemented)] pub async fn unarchive(&self, community_id: CommunityId, pubkey: &str) -> Result { match &self.backend { DbBackend::SQLite(pool) => sqlite::unarchive(pool, community_id, pubkey).await, @@ -6086,6 +6318,7 @@ impl Db { } /// Returns all identities archived in `community_id`, ordered by archive time ascending. + #[sqlite_backend(implemented)] pub async fn list_archived( &self, community_id: CommunityId, @@ -6099,6 +6332,7 @@ impl Db { } /// Soft-delete NIP-29 discovery events for a channel created by a specific relay pubkey. + #[sqlite_backend(implemented)] pub async fn soft_delete_discovery_events( &self, community_id: CommunityId, @@ -6133,6 +6367,7 @@ impl Db { /// Same-second ties are broken by lowest event `id` (NIP-16 deterministic ordering). /// Returns `(event, false)` for stale writes and duplicate IDs — callers should /// skip fan-out/dispatch when `was_inserted` is false. + #[sqlite_backend(implemented)] pub async fn replace_addressable_event( &self, community_id: CommunityId, @@ -6270,6 +6505,7 @@ impl Db { /// Snapshot and canonical rows are compared directly rather than by /// timestamp: relay membership events use whole-second Nostr timestamps, /// and multiple mutations within one second must still be repaired. + #[sqlite_backend(implemented)] pub async fn nip43_membership_snapshot_needs_reconciliation( &self, community_id: CommunityId, @@ -6320,6 +6556,7 @@ impl Db { /// prevents the stale-snapshot race where a concurrent publication reads /// older state and overwrites a newer snapshot by arrival order. /// + #[sqlite_backend(implemented)] pub async fn publish_nip43_membership_locked( &self, community_id: CommunityId, @@ -6460,6 +6697,7 @@ impl Db { /// relay-signed NIP-29 group metadata (kind 39000–39002) where the relay is the /// author and channel_id distinguishes groups. User-submitted NIP-33 events use /// this function instead, where the author's pubkey + d-tag is the natural key. + #[sqlite_backend(implemented)] pub async fn replace_parameterized_event( &self, community_id: CommunityId, @@ -10252,3 +10490,47 @@ mod tests { drop_scratch_db(&admin, pool, &name).await; } } + +#[cfg(test)] +mod backend_declaration_tests { + use super::SQLITE_BACKEND_INVENTORY; + + #[test] + fn backend_inventory_is_current() { + let rendered = SQLITE_BACKEND_INVENTORY + .iter() + .filter_map(|(method, reason)| { + reason.map(|reason| format!("| `{method}` | unsupported | {reason} |\n")) + }) + .collect::(); + let expected = include_str!("../SQLITE_BACKEND_INVENTORY.md"); + let table = expected + .split("| --- | --- | --- |\n") + .nth(1) + .and_then(|body| body.split("\n\n").next()) + .unwrap_or_default(); + assert_eq!( + table.trim(), + rendered.trim(), + "regenerate SQLITE_BACKEND_INVENTORY.md" + ); + } + + #[test] + fn every_db_impl_is_guarded() { + let source = include_str!("lib.rs"); + let impl_lines = source + .lines() + .enumerate() + .filter(|(_, line)| line.trim() == "impl Db {") + .collect::>(); + assert_eq!( + impl_lines.len(), + 1, + "every impl Db must be reviewed for the enforcement attribute" + ); + let (line_index, _) = impl_lines[0]; + let preceding = source.lines().nth(line_index - 1).unwrap_or_default(); + assert_eq!(preceding, "#[enforce_sqlite_backend_declarations]"); + } +}