nostr-rs-relay

My dev fork of nostr-rs-relay
git clone git://jb55.com/nostr-rs-relay
Log | Files | Refs | README | LICENSE

commit 1c14adc766e8be09f27534d5af64fa56c3e60ee9
parent e894a86566fea951cab0df3828d7a6a40627dbbb
Author: Greg Heartsfield <scsibug@imap.cc>
Date:   Mon,  4 Jul 2022 17:25:32 -0500

fix(NIP-01): allow limits on a per-filter basis

The original implementation of subscription limit applied to the
entire query, instead of the specific filter.  Now, each filter gets
its own query limit.  When a limit is applied, the most recent N
events will be returned, otherwise the default is to return the
earliest events (in order), for all matching events.

Diffstat:
Msrc/db.rs | 257++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/subscription.rs | 2+-
2 files changed, 132 insertions(+), 127 deletions(-)

diff --git a/src/db.rs b/src/db.rs @@ -7,6 +7,7 @@ use crate::hexrange::hex_range; use crate::hexrange::HexSearch; use crate::nip05; use crate::schema::{upgrade_db, STARTUP_SQL}; +use crate::subscription::ReqFilter; use crate::subscription::Subscription; use crate::utils::is_hex; use governor::clock::Clock; @@ -377,152 +378,156 @@ fn repeat_vars(count: usize) -> String { s } -/// Create a dynamic SQL query string and params from a subscription. -fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) { +/// Create a dynamic SQL subquery and params from a subscription filter. +fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) { // build a dynamic SQL query. all user-input is either an integer // (sqli-safe), or a string that is filtered to only contain // hexadecimal characters. Strings that require escaping (tag // names/values) use parameters. - let mut limit: Option<u32> = None; let mut query = - "SELECT DISTINCT(e.content) FROM event e LEFT JOIN tag t ON e.id=t.event_id ".to_owned(); - // parameters + "SELECT DISTINCT(e.content), e.created_at FROM event e LEFT JOIN tag t ON e.id=t.event_id " + .to_owned(); + // query parameters for SQLite let mut params: Vec<Box<dyn ToSql>> = vec![]; - - // for every filter in the subscription, generate a where clause - let mut filter_clauses: Vec<String> = Vec::new(); - for f in sub.filters.iter() { - // individual filter components - let mut filter_components: Vec<String> = Vec::new(); - // Query for "authors", allowing prefix matches - if let Some(authvec) = &f.authors { - // take each author and convert to a hexsearch - let mut auth_searches: Vec<String> = vec![]; - for auth in authvec { - match hex_range(auth) { - Some(HexSearch::Exact(ex)) => { - auth_searches.push("author=?".to_owned()); - params.push(Box::new(ex)); - } - Some(HexSearch::Range(lower, upper)) => { - auth_searches.push("(author>? AND author<?)".to_owned()); - params.push(Box::new(lower)); - params.push(Box::new(upper)); - } - Some(HexSearch::LowerOnly(lower)) => { - auth_searches.push("author>?".to_owned()); - params.push(Box::new(lower)); - } - None => { - info!("Could not parse hex range from author {:?}", auth); - } + // individual filter components (single conditions such as an author or event ID) + let mut filter_components: Vec<String> = Vec::new(); + // Query for "authors", allowing prefix matches + if let Some(authvec) = &f.authors { + // take each author and convert to a hexsearch + let mut auth_searches: Vec<String> = vec![]; + for auth in authvec { + match hex_range(auth) { + Some(HexSearch::Exact(ex)) => { + auth_searches.push("author=?".to_owned()); + params.push(Box::new(ex)); + } + Some(HexSearch::Range(lower, upper)) => { + auth_searches.push("(author>? AND author<?)".to_owned()); + params.push(Box::new(lower)); + params.push(Box::new(upper)); + } + Some(HexSearch::LowerOnly(lower)) => { + auth_searches.push("author>?".to_owned()); + params.push(Box::new(lower)); + } + None => { + info!("Could not parse hex range from author {:?}", auth); } } - let authors_clause = format!("({})", auth_searches.join(" OR ")); - filter_components.push(authors_clause); - } - if let Some(lim) = f.limit { - limit = Some(lim) - } - // Query for Kind - if let Some(ks) = &f.kinds { - // kind is number, no escaping needed - let str_kinds: Vec<String> = ks.iter().map(|x| x.to_string()).collect(); - let kind_clause = format!("kind IN ({})", str_kinds.join(", ")); - filter_components.push(kind_clause); } - // Query for event, allowing prefix matches - if let Some(idvec) = &f.ids { - // take each author and convert to a hexsearch - let mut id_searches: Vec<String> = vec![]; - for id in idvec { - match hex_range(id) { - Some(HexSearch::Exact(ex)) => { - id_searches.push("event_hash=?".to_owned()); - params.push(Box::new(ex)); - } - Some(HexSearch::Range(lower, upper)) => { - id_searches.push("(event_hash>? AND event_hash<?)".to_owned()); - params.push(Box::new(lower)); - params.push(Box::new(upper)); - } - Some(HexSearch::LowerOnly(lower)) => { - id_searches.push("event_hash>?".to_owned()); - params.push(Box::new(lower)); - } - None => { - info!("Could not parse hex range from id {:?}", id); - } + let authors_clause = format!("({})", auth_searches.join(" OR ")); + filter_components.push(authors_clause); + } + // Query for Kind + if let Some(ks) = &f.kinds { + // kind is number, no escaping needed + let str_kinds: Vec<String> = ks.iter().map(|x| x.to_string()).collect(); + let kind_clause = format!("kind IN ({})", str_kinds.join(", ")); + filter_components.push(kind_clause); + } + // Query for event, allowing prefix matches + if let Some(idvec) = &f.ids { + // take each author and convert to a hexsearch + let mut id_searches: Vec<String> = vec![]; + for id in idvec { + match hex_range(id) { + Some(HexSearch::Exact(ex)) => { + id_searches.push("event_hash=?".to_owned()); + params.push(Box::new(ex)); + } + Some(HexSearch::Range(lower, upper)) => { + id_searches.push("(event_hash>? AND event_hash<?)".to_owned()); + params.push(Box::new(lower)); + params.push(Box::new(upper)); + } + Some(HexSearch::LowerOnly(lower)) => { + id_searches.push("event_hash>?".to_owned()); + params.push(Box::new(lower)); + } + None => { + info!("Could not parse hex range from id {:?}", id); } } - let id_clause = format!("({})", id_searches.join(" OR ")); - filter_components.push(id_clause); } - // Query for tags - if let Some(map) = &f.tags { - for (key, val) in map.iter() { - let mut str_vals: Vec<Box<dyn ToSql>> = vec![]; - let mut blob_vals: Vec<Box<dyn ToSql>> = vec![]; - for v in val { - if is_hex(v) { - if let Ok(h) = hex::decode(&v) { - blob_vals.push(Box::new(h)); - } - } else { - str_vals.push(Box::new(v.to_owned())); + let id_clause = format!("({})", id_searches.join(" OR ")); + filter_components.push(id_clause); + } + // Query for tags + if let Some(map) = &f.tags { + for (key, val) in map.iter() { + let mut str_vals: Vec<Box<dyn ToSql>> = vec![]; + let mut blob_vals: Vec<Box<dyn ToSql>> = vec![]; + for v in val { + if is_hex(v) { + if let Ok(h) = hex::decode(&v) { + blob_vals.push(Box::new(h)); } + } else { + str_vals.push(Box::new(v.to_owned())); } - // create clauses with "?" params for each tag value being searched - let str_clause = format!("value IN ({})", repeat_vars(str_vals.len())); - let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len())); - let tag_clause = format!("(name=? AND ({} OR {}))", str_clause, blob_clause); - // add the tag name as the first parameter - params.push(Box::new(key.to_owned())); - // add all tag values that are plain strings as params - params.append(&mut str_vals); - // add all tag values that are blobs as params - params.append(&mut blob_vals); - filter_components.push(tag_clause); } - } - // Query for timestamp - if f.since.is_some() { - let created_clause = format!("created_at > {}", f.since.unwrap()); - filter_components.push(created_clause); - } - // Query for timestamp - if f.until.is_some() { - let until_clause = format!("created_at < {}", f.until.unwrap()); - filter_components.push(until_clause); - } - - // combine all clauses, and add to filter_clauses - if !filter_components.is_empty() { - let mut fc = "( ".to_owned(); - fc.push_str(&filter_components.join(" AND ")); - fc.push_str(" )"); - filter_clauses.push(fc); + // create clauses with "?" params for each tag value being searched + let str_clause = format!("value IN ({})", repeat_vars(str_vals.len())); + let blob_clause = format!("value_hex IN ({})", repeat_vars(blob_vals.len())); + let tag_clause = format!("(name=? AND ({} OR {}))", str_clause, blob_clause); + // add the tag name as the first parameter + params.push(Box::new(key.to_owned())); + // add all tag values that are plain strings as params + params.append(&mut str_vals); + // add all tag values that are blobs as params + params.append(&mut blob_vals); + filter_components.push(tag_clause); } } - + // Query for timestamp + if f.since.is_some() { + let created_clause = format!("created_at > {}", f.since.unwrap()); + filter_components.push(created_clause); + } + // Query for timestamp + if f.until.is_some() { + let until_clause = format!("created_at < {}", f.until.unwrap()); + filter_components.push(until_clause); + } // never display hidden events - query.push_str(" WHERE hidden!=TRUE "); - - // combine all filters with OR clauses, if any exist - if !filter_clauses.is_empty() { - query.push_str(" AND ("); - query.push_str(&filter_clauses.join(" OR ")); - query.push_str(") "); + query.push_str(" WHERE hidden!=TRUE"); + // build filter component conditions + if !filter_components.is_empty() { + query.push_str(" AND "); + query.push_str(&filter_components.join(" AND ")); + } + // Apply per-filter limit to this subquery. + // The use of a LIMIT implies a DESC order, to capture only the most recent events. + if let Some(lim) = f.limit { + query.push_str(&format!(" ORDER BY e.created_at DESC LIMIT {}", lim)) + } else { + query.push_str(" ORDER BY e.created_at ASC") } - // add order clause - query.push_str(&format!( - " ORDER BY created_at {}", - limit.map_or("ASC", |_| "DESC") - )); - if let Some(lim) = limit { - query.push_str(&format!(" LIMIT {}", lim)) + (query, params) +} + +/// Create a dynamic SQL query string and params from a subscription. +fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) { + // build a dynamic SQL query for an entire subscription, based on + // SQL subqueries for filters. + let mut subqueries: Vec<String> = Vec::new(); + // subquery params + let mut params: Vec<Box<dyn ToSql>> = vec![]; + // for every filter in the subscription, generate a subquery + for f in sub.filters.iter() { + let (f_subquery, mut f_params) = query_from_filter(&f); + subqueries.push(f_subquery); + params.append(&mut f_params); } - debug!("query string: {}", query); + // encapsulate subqueries into select statements + let subqueries_selects: Vec<String> = subqueries + .iter() + .map(|s| { + return format!("SELECT content, created_at FROM ({})", s); + }) + .collect(); + let query: String = subqueries_selects.join(" UNION "); + info!("final query string: {}", query); (query, params) } diff --git a/src/subscription.rs b/src/subscription.rs @@ -32,7 +32,7 @@ pub struct ReqFilter { /// List of author public keys pub authors: Option<Vec<String>>, /// Limit number of results - pub limit: Option<u32>, + pub limit: Option<u64>, /// Set of tags #[serde(skip)] pub tags: Option<HashMap<String, HashSet<String>>>,