mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
test(relay): cover exact #buzz-channel matching against tag decoys
Containment-only matching let a newer event carrying ["x","buzz-channel",<id>] take a limit-1 project or repository lookup. Also reattach the e-tag planner comment and describe the positional check in the tag field docs. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
@@ -89,11 +89,13 @@ pub struct EventQuery {
|
||||
/// column is NULL (not NIP-33), so this lets identity lookups match before
|
||||
/// SQL `LIMIT`. Rows of other kinds are left to the caller's post-filter.
|
||||
pub d_tag_values: Option<Vec<String>>,
|
||||
/// Restrict results to events with an exact custom tag pair.
|
||||
/// Uses JSONB containment against `tags` before SQL `LIMIT`.
|
||||
/// Restrict results to events with an exact custom tag pair: a tag whose
|
||||
/// first element is the name and second the value, checked positionally
|
||||
/// (JSONB containment is only the index prefilter) before SQL `LIMIT`.
|
||||
pub custom_tag: Option<(String, String)>,
|
||||
/// Restrict results to events with a `t` tag of this value. Uses JSONB
|
||||
/// containment against `tags` before SQL `LIMIT`, so a per-subject read
|
||||
/// Restrict results to events with a `t` tag of this value, checked
|
||||
/// positionally (JSONB containment is only the index prefilter) before
|
||||
/// SQL `LIMIT`, so a per-subject read
|
||||
/// (e.g. a kind:44300 team's current version) is not starved by newer
|
||||
/// events with other `t` values.
|
||||
pub t_tag: Option<String>,
|
||||
@@ -860,9 +862,6 @@ async fn fetch_with_e_tag_deadline(
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// e-tag pushdown as one array-bound containment test instead of an N-way
|
||||
/// `OR` chain, so planner cost does not scale with the number of referenced
|
||||
/// ids (the thread aux hop sends one id per reply).
|
||||
/// Match a tag whose first element is `name` and second is `value` (NIP-01).
|
||||
///
|
||||
/// JSONB containment alone ignores element position and multiplicity:
|
||||
@@ -887,6 +886,9 @@ fn push_exact_tag_filter(
|
||||
.push(")");
|
||||
}
|
||||
|
||||
/// e-tag pushdown as one array-bound containment test instead of an N-way
|
||||
/// `OR` chain, so planner cost does not scale with the number of referenced
|
||||
/// ids (the thread aux hop sends one id per reply).
|
||||
fn push_e_tag_filter(qb: &mut QueryBuilder<sqlx::Postgres>, col_prefix: &str, e_tags: &[String]) {
|
||||
let containments: Vec<serde_json::Value> = e_tags
|
||||
.iter()
|
||||
|
||||
@@ -144,25 +144,34 @@ impl Fixture {
|
||||
#[ignore = "requires Postgres"]
|
||||
async fn project_filter_survives_artifact_query_hook() {
|
||||
let f = Fixture::new().await;
|
||||
let project = EventBuilder::new(Kind::Custom(30621), "")
|
||||
.tags([
|
||||
Tag::parse(["d", "review-project"]).unwrap(),
|
||||
Tag::parse(["buzz-channel", &f.home.to_string()]).unwrap(),
|
||||
])
|
||||
.sign_with_keys(&f.owner)
|
||||
.unwrap();
|
||||
f.state
|
||||
.db
|
||||
.insert_event(f.community, &project, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let filter = json!([{"kinds":[30621],"#buzz-channel":[f.home]}]);
|
||||
let (status, body) = f.request("/query", filter.clone()).await;
|
||||
assert!(status.is_success(), "{status}: {body}");
|
||||
assert_eq!(body.as_array().unwrap().len(), 1);
|
||||
assert_eq!(body[0]["id"], project.id.to_hex());
|
||||
let (status, body) = f.request("/count", filter).await;
|
||||
assert!(status.is_success(), "{status}: {body}");
|
||||
let home = f.home.to_string();
|
||||
// 30621 projects and 30617 repositories bind a channel via `buzz-channel`.
|
||||
for kind in [30621u16, 30617] {
|
||||
let event = |tag: &[&str], at: u64| {
|
||||
EventBuilder::new(Kind::Custom(kind), "")
|
||||
.tags([
|
||||
Tag::parse(["d", &Uuid::new_v4().to_string()]).unwrap(),
|
||||
Tag::parse(tag.iter().copied()).unwrap(),
|
||||
])
|
||||
.custom_created_at(nostr::Timestamp::from(at))
|
||||
.sign_with_keys(&f.owner)
|
||||
.unwrap()
|
||||
};
|
||||
let now = nostr::Timestamp::now().as_secs();
|
||||
let bound = event(&["buzz-channel", &home], now - 10);
|
||||
// Newer, unbound: `buzz-channel` and the wanted id sit in an unrelated tag.
|
||||
let decoy = event(&["x", "buzz-channel", &home], now);
|
||||
for e in [&bound, &decoy] {
|
||||
f.state.db.insert_event(f.community, e, None).await.unwrap();
|
||||
}
|
||||
let filter = json!([{"kinds":[kind],"#buzz-channel":[f.home],"limit":1}]);
|
||||
let (status, body) = f.request("/query", filter.clone()).await;
|
||||
assert!(status.is_success(), "{kind} {status}: {body}");
|
||||
assert_eq!(body.as_array().unwrap().len(), 1, "{kind}: {body}");
|
||||
assert_eq!(body[0]["id"], bound.id.to_hex(), "{kind}: {body}");
|
||||
let (status, body) = f.request("/count", filter).await;
|
||||
assert!(status.is_success(), "{kind} {status}: {body}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1323,7 +1323,7 @@ async fn author_only_kinds_are_unsearchable() {
|
||||
assert!(
|
||||
!kinds.contains(&(kind as i32)),
|
||||
"AUTHOR_ONLY kind:{kind} MUST NOT be searchable — \
|
||||
schema skip-set is missing this kind. AUTHOR_ONLY_KINDS={AUTHOR_ONLY_KINDS:?}, \
|
||||
schema exclusion or query fence is missing this kind. AUTHOR_ONLY_KINDS={AUTHOR_ONLY_KINDS:?}, \
|
||||
hits={kinds:?}",
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user