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.
This commit is contained in:
Greg Heartsfield 2022-07-04 17:25:32 -05:00
parent e894a86566
commit 1c14adc766
2 changed files with 132 additions and 127 deletions

View File

@ -7,6 +7,7 @@ use crate::hexrange::hex_range;
use crate::hexrange::HexSearch; use crate::hexrange::HexSearch;
use crate::nip05; use crate::nip05;
use crate::schema::{upgrade_db, STARTUP_SQL}; use crate::schema::{upgrade_db, STARTUP_SQL};
use crate::subscription::ReqFilter;
use crate::subscription::Subscription; use crate::subscription::Subscription;
use crate::utils::is_hex; use crate::utils::is_hex;
use governor::clock::Clock; use governor::clock::Clock;
@ -377,22 +378,18 @@ fn repeat_vars(count: usize) -> String {
s s
} }
/// Create a dynamic SQL query string and params from a subscription. /// Create a dynamic SQL subquery and params from a subscription filter.
fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) { fn query_from_filter(f: &ReqFilter) -> (String, Vec<Box<dyn ToSql>>) {
// build a dynamic SQL query. all user-input is either an integer // build a dynamic SQL query. all user-input is either an integer
// (sqli-safe), or a string that is filtered to only contain // (sqli-safe), or a string that is filtered to only contain
// hexadecimal characters. Strings that require escaping (tag // hexadecimal characters. Strings that require escaping (tag
// names/values) use parameters. // names/values) use parameters.
let mut limit: Option<u32> = None;
let mut query = let mut query =
"SELECT DISTINCT(e.content) FROM event e LEFT JOIN tag t ON e.id=t.event_id ".to_owned(); "SELECT DISTINCT(e.content), e.created_at FROM event e LEFT JOIN tag t ON e.id=t.event_id "
// parameters .to_owned();
// query parameters for SQLite
let mut params: Vec<Box<dyn ToSql>> = vec![]; let mut params: Vec<Box<dyn ToSql>> = vec![];
// individual filter components (single conditions such as an author or event ID)
// 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(); let mut filter_components: Vec<String> = Vec::new();
// Query for "authors", allowing prefix matches // Query for "authors", allowing prefix matches
if let Some(authvec) = &f.authors { if let Some(authvec) = &f.authors {
@ -421,9 +418,6 @@ fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
let authors_clause = format!("({})", auth_searches.join(" OR ")); let authors_clause = format!("({})", auth_searches.join(" OR "));
filter_components.push(authors_clause); filter_components.push(authors_clause);
} }
if let Some(lim) = f.limit {
limit = Some(lim)
}
// Query for Kind // Query for Kind
if let Some(ks) = &f.kinds { if let Some(ks) = &f.kinds {
// kind is number, no escaping needed // kind is number, no escaping needed
@ -495,34 +489,45 @@ fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
let until_clause = format!("created_at < {}", f.until.unwrap()); let until_clause = format!("created_at < {}", f.until.unwrap());
filter_components.push(until_clause); 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);
}
}
// never display hidden events // never display hidden events
query.push_str(" WHERE hidden!=TRUE"); 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")
}
(query, params)
}
// combine all filters with OR clauses, if any exist /// Create a dynamic SQL query string and params from a subscription.
if !filter_clauses.is_empty() { fn query_from_sub(sub: &Subscription) -> (String, Vec<Box<dyn ToSql>>) {
query.push_str(" AND ("); // build a dynamic SQL query for an entire subscription, based on
query.push_str(&filter_clauses.join(" OR ")); // SQL subqueries for filters.
query.push_str(") "); 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);
} }
// add order clause // encapsulate subqueries into select statements
query.push_str(&format!( let subqueries_selects: Vec<String> = subqueries
" ORDER BY created_at {}", .iter()
limit.map_or("ASC", |_| "DESC") .map(|s| {
)); return format!("SELECT content, created_at FROM ({})", s);
if let Some(lim) = limit { })
query.push_str(&format!(" LIMIT {}", lim)) .collect();
} let query: String = subqueries_selects.join(" UNION ");
debug!("query string: {}", query); info!("final query string: {}", query);
(query, params) (query, params)
} }

View File

@ -32,7 +32,7 @@ pub struct ReqFilter {
/// List of author public keys /// List of author public keys
pub authors: Option<Vec<String>>, pub authors: Option<Vec<String>>,
/// Limit number of results /// Limit number of results
pub limit: Option<u32>, pub limit: Option<u64>,
/// Set of tags /// Set of tags
#[serde(skip)] #[serde(skip)]
pub tags: Option<HashMap<String, HashSet<String>>>, pub tags: Option<HashMap<String, HashSet<String>>>,