use anyhow::{Context, Result};
use rusqlite::named_params;
use super::filter_sql::{
app_like_pattern, can_use_snapshot_event_cache, can_use_snapshot_stats_since_filter,
effective_since_param, event_filter_clause, requires_matching_events, snapshot_filter_clause,
};
use super::queries::{recent_query, timeline_query};
use super::row_mapping::{map_search_hit_row, map_timeline_event_row};
use super::search_results::{paginate_rows, paginate_timeline_rows};
use super::stats::{stats_params, StatsReadSource};
use crate::db::core::clamp_result_limit;
use crate::db::sqlite_helpers::{collect_rows, usize_to_i64};
use crate::db::types::{
Database, Page, RecentCursorState, RecentResults, RetrievalFilters, StatsReport,
TimelineCursorState, TimelineResults, TimelineSort,
};
use crate::model::{SearchHit, TimelineEvent};
impl Database {
pub fn recent(&self, limit: usize, filters: &RetrievalFilters) -> Result<RecentResults> {
let page = self.recent_page(limit, filters, None)?;
let has_more = page.has_more();
Ok(RecentResults::new(page.into_items(), has_more))
}
pub fn recent_hits(&self, limit: usize, filters: &RetrievalFilters) -> Result<Vec<SearchHit>> {
self.recent(limit, filters).map(RecentResults::into_hits)
}
pub fn timeline(
&self,
limit: usize,
filters: &RetrievalFilters,
sort: TimelineSort,
) -> Result<TimelineResults> {
let page = self.timeline_page(limit, filters, sort, None)?;
let has_more = page.has_more();
Ok(TimelineResults::new(page.into_items(), has_more))
}
pub(crate) fn recent_page(
&self,
limit: usize,
filters: &RetrievalFilters,
cursor: Option<&RecentCursorState>,
) -> Result<Page<SearchHit>> {
let limit = clamp_result_limit(limit);
let fetch_limit = usize_to_i64(limit.saturating_add(1))?;
let use_snapshot_stats_since_filter = can_use_snapshot_stats_since_filter(filters);
let include_matching_events =
requires_matching_events(filters) && !use_snapshot_stats_since_filter;
let use_snapshot_event_cache = can_use_snapshot_event_cache(filters);
let sql = recent_query(
include_matching_events,
use_snapshot_event_cache,
use_snapshot_stats_since_filter,
);
let app_like = app_like_pattern(filters);
let bundle_id = filters.bundle_id().map(|value| value.to_ascii_lowercase());
let kind = filters.kind().map(|value| value.as_str());
let since = effective_since_param(filters)?;
let mut stmt = self.conn.prepare(&sql).context("prepare recent query")?;
let rows = stmt
.query_map(
named_params! {
":since" : since.as_deref(),
":until" : filters.until(),
":app_like" : app_like.as_deref(),
":bundle_id" : bundle_id.as_deref(),
":kind" : kind,
":has_text" : filters.has_text(),
":has_url" : filters.has_url(),
":has_file_url" : filters.has_file_url(),
":has_image" : filters.has_image(),
":has_pdf" : filters.has_pdf(),
":min_bytes" : filters.min_bytes().map(usize_to_i64).transpose()?,
":max_bytes" : filters.max_bytes().map(usize_to_i64).transpose()?,
":cursor_last_seen_at" : cursor.map(RecentCursorState::last_seen_at),
":cursor_snapshot_id" : cursor.map(RecentCursorState::snapshot_id),
":limit" : fetch_limit,
},
|row| map_search_hit_row(row, false),
)
.context("execute recent query")?;
collect_rows(rows)
.map(|hits| paginate_rows(hits, limit))
.context("collect recent query rows")
}
pub(crate) fn timeline_page(
&self,
limit: usize,
filters: &RetrievalFilters,
sort: TimelineSort,
cursor: Option<&TimelineCursorState>,
) -> Result<Page<TimelineEvent>> {
let limit = clamp_result_limit(limit);
let fetch_limit = usize_to_i64(limit.saturating_add(1))?;
let sql = timeline_query(sort);
let app_like = app_like_pattern(filters);
let bundle_id = filters.bundle_id().map(|value| value.to_ascii_lowercase());
let kind = filters.kind().map(|value| value.as_str());
let since = effective_since_param(filters)?;
let mut stmt = self.conn.prepare(&sql).context("prepare timeline query")?;
let rows = stmt
.query_map(
named_params! {
":since" : since.as_deref(),
":until" : filters.until(),
":app_like" : app_like.as_deref(),
":bundle_id" : bundle_id.as_deref(),
":kind" : kind,
":has_text" : filters.has_text(),
":has_url" : filters.has_url(),
":has_file_url" : filters.has_file_url(),
":has_image" : filters.has_image(),
":has_pdf" : filters.has_pdf(),
":min_bytes" : filters.min_bytes().map(usize_to_i64).transpose()?,
":max_bytes" : filters.max_bytes().map(usize_to_i64).transpose()?,
":cursor_observed_at" : cursor.map(TimelineCursorState::observed_at),
":cursor_event_id" : cursor.map(TimelineCursorState::event_id),
":limit" : fetch_limit,
},
map_timeline_event_row,
)
.context("execute timeline query")?;
collect_rows(rows)
.map(|events| paginate_timeline_rows(events, limit))
.context("collect timeline query rows")
}
pub(crate) fn snapshot_matches_filters(
&self,
snapshot_id: i64,
filters: &RetrievalFilters,
) -> Result<bool> {
let sql = format!(
"SELECT EXISTS(
SELECT 1
FROM snapshots s
WHERE s.id = :snapshot_id
AND EXISTS (
SELECT 1
FROM capture_events ce
WHERE ce.snapshot_id = s.id
AND {event_filter_clause}
)
AND {snapshot_filter_clause}
)",
event_filter_clause = event_filter_clause("ce"),
snapshot_filter_clause = snapshot_filter_clause("s", "s.id"),
);
let app_like = app_like_pattern(filters);
let bundle_id = filters.bundle_id().map(|value| value.to_ascii_lowercase());
let kind = filters.kind().map(|value| value.as_str());
let since = effective_since_param(filters)?;
let exists: i64 = self
.conn
.query_row(
&sql,
named_params! {
":snapshot_id" : snapshot_id,
":since" : since.as_deref(),
":until" : filters.until(),
":app_like" : app_like.as_deref(),
":bundle_id" : bundle_id.as_deref(),
":kind" : kind,
":has_text" : filters.has_text(),
":has_url" : filters.has_url(),
":has_file_url" : filters.has_file_url(),
":has_image" : filters.has_image(),
":has_pdf" : filters.has_pdf(),
":min_bytes" : filters.min_bytes().map(usize_to_i64).transpose()?,
":max_bytes" : filters.max_bytes().map(usize_to_i64).transpose()?,
},
|row| row.get(0),
)
.context("evaluate snapshot filters")?;
Ok(exists != 0)
}
pub fn stats(&self, filters: &RetrievalFilters) -> Result<StatsReport> {
if *filters == RetrievalFilters::default() {
return self.load_stats_report(StatsReadSource::Unfiltered);
}
let params = stats_params(filters)?;
self.reset_stats_temp_tables()?;
self.materialize_stats_matching_events(¶ms)?;
self.index_stats_matching_events()?;
self.materialize_stats_matching_snapshots(filters, ¶ms)?;
self.index_stats_matching_snapshots()?;
self.load_stats_report(StatsReadSource::Filtered)
}
}