diff --git a/src/db.rs b/src/db.rs index 94dabce..8ee6a39 100644 --- a/src/db.rs +++ b/src/db.rs @@ -230,6 +230,12 @@ pub struct TimelineMultiPoint { pub cached: i64, } +#[derive(Debug, Clone)] +pub struct DomainStats { + pub unique: i64, + pub top: Vec, +} + #[derive(Debug, Clone, Serialize)] pub struct HeatmapCell { pub weekday: i64, // 0 = Sunday, 6 = Saturday (matches strftime('%w')) @@ -418,7 +424,8 @@ impl Database { upstream TEXT, doh_token TEXT, result TEXT, - authenticated_data INTEGER NOT NULL DEFAULT 0 + authenticated_data INTEGER NOT NULL DEFAULT 0, + has_result INTEGER GENERATED ALWAYS AS (result IS NOT NULL AND result != '') VIRTUAL ); CREATE INDEX IF NOT EXISTS idx_query_logs_timestamp ON query_logs(timestamp); CREATE INDEX IF NOT EXISTS idx_query_logs_domain_ts ON query_logs(domain, timestamp); @@ -642,7 +649,22 @@ impl Database { )?; } - const LATEST_VERSION: i64 = 11; + if version < 12 { + add_column_if_missing( + conn, + "query_logs", + "has_result", + "INTEGER GENERATED ALWAYS AS (result IS NOT NULL AND result != '') VIRTUAL", + )?; + conn.execute_batch( + "DROP INDEX IF EXISTS idx_query_logs_ts_metrics; + CREATE INDEX idx_query_logs_ts_metrics \ + ON query_logs(timestamp, blocked, cached, response_ms, query_type, has_result); + ANALYZE;", + )?; + } + + const LATEST_VERSION: i64 = 12; if version < LATEST_VERSION { conn.pragma_update(None, "user_version", LATEST_VERSION)?; } @@ -1891,25 +1913,43 @@ impl Database { since: i64, limit: i64, ) -> Result, DbError> { + Ok(self.domain_stats_since(since, limit).await?.top) + } + + pub async fn domain_stats_since(&self, since: i64, limit: i64) -> Result { let since_ms = since * 1000; - let rows = self + let stats = self .reader() .call(move |conn| { let mut stmt = conn.prepare_cached( - "SELECT domain, COUNT(*) as cnt FROM query_logs WHERE timestamp >= ?1 GROUP BY domain ORDER BY cnt DESC LIMIT ?2", + "WITH d AS ( \ + SELECT domain, COUNT(*) AS cnt \ + FROM query_logs \ + WHERE timestamp >= ?1 \ + GROUP BY domain \ + ) \ + SELECT (SELECT COUNT(*) FROM d), domain, cnt \ + FROM d ORDER BY cnt DESC LIMIT ?2", )?; let rows = stmt .query_map(params![since_ms, limit], |row| { - Ok(TopDomain { - domain: row.get(0)?, - count: row.get(1)?, - }) + Ok(( + row.get::<_, i64>(0)?, + TopDomain { + domain: row.get(1)?, + count: row.get(2)?, + }, + )) })? .collect::, _>>()?; - Ok(rows) + let unique = rows.first().map_or(0, |(n, _)| *n); + Ok(DomainStats { + unique, + top: rows.into_iter().map(|(_, d)| d).collect(), + }) }) .await?; - Ok(rows) + Ok(stats) } pub async fn top_clients_since( @@ -2119,19 +2159,7 @@ impl Database { } pub async fn unique_domains_since(&self, since: i64) -> Result { - let since_ms = since * 1000; - let count = self - .reader() - .call(move |conn| { - let n: i64 = conn.query_row( - "SELECT COUNT(DISTINCT domain) FROM query_logs WHERE timestamp >= ?1", - params![since_ms], - |row| row.get(0), - )?; - Ok(n) - }) - .await?; - Ok(count) + Ok(self.domain_stats_since(since, 1).await?.unique) } pub async fn latency_summary_since(&self, since: i64) -> Result { @@ -2297,7 +2325,7 @@ fn add_column_if_missing( ) -> Result<(), rusqlite::Error> { let exists: bool = conn .query_row( - &format!("SELECT COUNT(*) FROM pragma_table_info('{table}') WHERE name = ?1"), + &format!("SELECT COUNT(*) FROM pragma_table_xinfo('{table}') WHERE name = ?1"), params![column], |row| row.get::<_, i64>(0), )