mirror of
https://github.com/block/buzz.git
synced 2026-11-11 20:01:09 +00:00
perf(db): split explicit kinds for author-only reads
The author-only mask `kind IN (..) AND (kind NOT IN (private) OR pubkey = reader)` looked broad to the Postgres planner. When foreign private rows dominated the requested kinds, it walked the community's primary key backward, so a mixed read such as kinds [30300, 30078] scanned every row in the community: 1.4-1.8 s on a 2.45M-event community, against about 6 ms before the pushdown. With explicit kinds, emit `kind IN (public) OR (kind IN (private) AND pubkey = reader)` instead. Postgres can estimate and index each side, and the same reads now take 4-9 ms. Kindless reads keep the mask. Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -125,8 +125,9 @@ pub struct EventQuery {
|
||||
/// defense-in-depth catches any residual mismatch.
|
||||
pub shared_gated_reader: Option<Vec<u8>>,
|
||||
/// Author-only visibility reader for [`query_events`]: exclude foreign
|
||||
/// [`AUTHOR_ONLY_KINDS`] before ordering, offset and limit. This does not
|
||||
/// affect `count_events`; callers must retain its existing fallback gate.
|
||||
/// [`AUTHOR_ONLY_KINDS`] before ordering, offset and limit. The SQL
|
||||
/// `count_events` path ignores it, so callers must keep their existing
|
||||
/// fallback gate; COUNT fallbacks built on `query_events` do apply it.
|
||||
pub author_only_reader: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
@@ -679,13 +680,62 @@ fn build_query_events_sql(q: &EventQuery) -> QueryBuilder<sqlx::Postgres> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ks) = q.kinds.as_deref().filter(|k| !k.is_empty()) {
|
||||
qb.push(format!(" AND {col_prefix}kind IN ("));
|
||||
let mut sep = qb.separated(", ");
|
||||
for k in ks {
|
||||
sep.push_bind(*k);
|
||||
let kinds = q.kinds.as_deref().filter(|k| !k.is_empty());
|
||||
match (kinds, q.author_only_reader.as_ref()) {
|
||||
// Author-only visibility belongs before pagination, like shared-gated
|
||||
// visibility; relay result checks remain defense in depth. With
|
||||
// explicit kinds, split them so Postgres sees `public kinds OR the
|
||||
// reader's own private rows` and can estimate and index each side.
|
||||
// The equivalent `kind IN (..) AND (kind NOT IN (private) OR pubkey =
|
||||
// reader)` looks broad to the planner: when foreign private rows
|
||||
// dominate the kinds, it walks the community's primary key backward.
|
||||
(Some(ks), Some(reader_bytes)) => {
|
||||
let (private, public): (Vec<i32>, Vec<i32>) = ks
|
||||
.iter()
|
||||
.partition(|k| AUTHOR_ONLY_KINDS.iter().any(|a| *a as i32 == **k));
|
||||
qb.push(" AND (");
|
||||
if !public.is_empty() {
|
||||
qb.push(format!("{col_prefix}kind IN ("));
|
||||
let mut sep = qb.separated(", ");
|
||||
for k in &public {
|
||||
sep.push_bind(*k);
|
||||
}
|
||||
qb.push(")");
|
||||
}
|
||||
if !private.is_empty() {
|
||||
if !public.is_empty() {
|
||||
qb.push(" OR ");
|
||||
}
|
||||
qb.push(format!("({col_prefix}kind IN ("));
|
||||
let mut sep = qb.separated(", ");
|
||||
for k in &private {
|
||||
sep.push_bind(*k);
|
||||
}
|
||||
qb.push(format!(") AND {col_prefix}pubkey = "));
|
||||
qb.push_bind(reader_bytes.clone());
|
||||
qb.push(")");
|
||||
}
|
||||
qb.push(")");
|
||||
}
|
||||
qb.push(")");
|
||||
(Some(ks), None) => {
|
||||
qb.push(format!(" AND {col_prefix}kind IN ("));
|
||||
let mut sep = qb.separated(", ");
|
||||
for k in ks {
|
||||
sep.push_bind(*k);
|
||||
}
|
||||
qb.push(")");
|
||||
}
|
||||
(None, Some(reader_bytes)) => {
|
||||
qb.push(format!(" AND ({col_prefix}kind NOT IN ("));
|
||||
let mut sep = qb.separated(", ");
|
||||
for kind in AUTHOR_ONLY_KINDS {
|
||||
sep.push_bind(*kind as i32);
|
||||
}
|
||||
qb.push(format!(") OR {col_prefix}pubkey = "));
|
||||
qb.push_bind(reader_bytes.clone());
|
||||
qb.push(")");
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
|
||||
if let Some(ref pk) = q.pubkey {
|
||||
@@ -802,19 +852,6 @@ fn build_query_events_sql(q: &EventQuery) -> QueryBuilder<sqlx::Postgres> {
|
||||
qb.push(")");
|
||||
}
|
||||
|
||||
// Author-only visibility belongs before pagination, just like shared-gated
|
||||
// visibility. Keep relay result checks as defense in depth.
|
||||
if let Some(ref reader_bytes) = q.author_only_reader {
|
||||
qb.push(format!(" AND ({col_prefix}kind NOT IN ("));
|
||||
let mut sep = qb.separated(", ");
|
||||
for kind in AUTHOR_ONLY_KINDS {
|
||||
sep.push_bind(*kind as i32);
|
||||
}
|
||||
qb.push(format!(") OR {col_prefix}pubkey = "));
|
||||
qb.push_bind(reader_bytes.clone());
|
||||
qb.push(")");
|
||||
}
|
||||
|
||||
// Composite ordering for deterministic pagination across ALL callers of
|
||||
// query_events (WebSocket REQ, REST endpoints, canvas, notes, etc.).
|
||||
// The `id ASC` tiebreaker ensures stable results when events share the
|
||||
@@ -2597,6 +2634,38 @@ mod e_tag_filter_shape_tests {
|
||||
assert!(sql[fence_end..].contains("ORDER BY created_at DESC, id ASC LIMIT "));
|
||||
}
|
||||
|
||||
/// Explicit kinds must reach SQL as `public OR own private`, not as a
|
||||
/// `NOT IN .. OR pubkey` mask the planner cannot estimate: on a large
|
||||
/// community that mask made a sparse mixed-kind read walk every row.
|
||||
#[test]
|
||||
fn author_only_reader_splits_explicit_kinds() {
|
||||
let shape = |kinds: Option<Vec<i32>>| {
|
||||
let mut q = EventQuery::for_community(CommunityId::from_uuid(uuid::Uuid::nil()));
|
||||
q.kinds = kinds;
|
||||
q.author_only_reader = Some(vec![1; 32]);
|
||||
build_query_events_sql(&q).sql().as_str().to_owned()
|
||||
};
|
||||
|
||||
let mixed = shape(Some(vec![30300, 1, 30350]));
|
||||
assert!(
|
||||
mixed.contains("AND (kind IN ($2) OR (kind IN ($3, $4) AND pubkey = $5))"),
|
||||
"{mixed}"
|
||||
);
|
||||
assert!(!mixed.contains("NOT IN"), "{mixed}");
|
||||
|
||||
let private = shape(Some(vec![30300]));
|
||||
assert!(
|
||||
private.contains("AND ((kind IN ($2) AND pubkey = $3))"),
|
||||
"{private}"
|
||||
);
|
||||
|
||||
let kindless = shape(None);
|
||||
assert!(
|
||||
kindless.contains("AND (kind NOT IN ($2, $3, $4) OR pubkey = $5)"),
|
||||
"{kindless}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_e_tag_query_is_not_fenced() {
|
||||
let q = EventQuery::for_community(CommunityId::from_uuid(uuid::Uuid::nil()));
|
||||
|
||||
Reference in New Issue
Block a user