feat(db): require explicit SQLite backend declarations

Signed-off-by: npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
This commit is contained in:
npub13n66s06epmqf2kc3v373ez8hj65cuzyvxzjf93vwpervxqn2u7jq2qd9je
2026-08-10 12:43:27 -04:00
committed by Brother Darryl
parent 336787a368
commit fe440a6afd
17 changed files with 551 additions and 0 deletions
Generated
+47
View File
@@ -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"
+1
View File
@@ -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",
+19
View File
@@ -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"
+104
View File
@@ -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<Option<LitStr>> {
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::<LitStr>()?;
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\"`",
)
})
}
@@ -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");
}
@@ -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() {}
@@ -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) {}
| ^^^^^^^^^^
@@ -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() {}
@@ -0,0 +1,5 @@
error: unsupported reason must not be empty
--> tests/ui/empty_reason.rs:5:36
|
5 | #[sqlite_backend(unsupported = " ")]
| ^^^^
@@ -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() {}
@@ -0,0 +1,5 @@
error: expected `implemented` or `unsupported = "reason"`
--> tests/ui/malformed_marker.rs:5:22
|
5 | #[sqlite_backend(sometimes)]
| ^^^^^^^^^
@@ -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() {}
@@ -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) {}
| ^^^^^^^
@@ -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() {}
+1
View File
@@ -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 }
@@ -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 |
File diff suppressed because it is too large Load Diff