use super::*;
#[derive(Debug)]
pub enum QueryCriteria<'a> {
Text(&'a str),
SourceId {
source: &'a str,
identifier: &'a str,
},
Author(&'a str),
All,
BeforeDate(DateTime<Utc>),
}
#[derive(Debug, Clone, Copy)]
pub enum OrderField {
Title,
PublicationDate,
Source,
}
impl OrderField {
fn as_sql_str(&self) -> &'static str {
match self {
OrderField::Title => "title",
OrderField::PublicationDate => "publication_date",
OrderField::Source => "source, source_identifier",
}
}
}
#[derive(Debug)]
pub struct Query<'a> {
criteria: QueryCriteria<'a>,
order_by: Option<OrderField>,
descending: bool,
}
impl<'a> Query<'a> {
pub fn new(criteria: QueryCriteria<'a>) -> Self {
Self { criteria, order_by: None, descending: false }
}
pub fn text(query: &'a str) -> Self { Self::new(QueryCriteria::Text(query)) }
pub fn by_paper(paper: &'a Paper) -> Self {
Self::new(QueryCriteria::SourceId {
source: &paper.source,
identifier: &paper.source_identifier,
})
}
pub fn by_source(source: &'a str, identifier: &'a str) -> Self {
Self::new(QueryCriteria::SourceId { source, identifier })
}
pub fn by_author(name: &'a str) -> Self { Self::new(QueryCriteria::Author(name)) }
pub fn list_all() -> Self { Self::new(QueryCriteria::All) }
pub fn before_date(date: DateTime<Utc>) -> Self { Self::new(QueryCriteria::BeforeDate(date)) }
pub fn order_by(mut self, field: OrderField) -> Self {
self.order_by = Some(field);
self
}
pub fn descending(mut self) -> Self {
self.descending = true;
self
}
fn build_criteria_sql(&self) -> (String, Vec<impl ToSql>) {
match &self.criteria {
QueryCriteria::Text(query) => (
"SELECT p.id
FROM papers p
JOIN papers_fts f ON p.id = f.rowid
WHERE papers_fts MATCH ?1 || '*'
ORDER BY rank"
.into(),
vec![(*query).to_string()],
),
QueryCriteria::SourceId { source, identifier } => (
"SELECT id FROM papers
WHERE source = ?1 AND source_identifier = ?2"
.into(),
vec![source.to_string(), (*identifier).to_string()],
),
QueryCriteria::Author(name) => (
"SELECT DISTINCT p.id
FROM papers p
JOIN authors a ON p.id = a.paper_id
WHERE a.name LIKE ?1"
.into(),
vec![format!("%{}%", name)],
),
QueryCriteria::All => ("SELECT id FROM papers".into(), Vec::new()),
QueryCriteria::BeforeDate(date) => (
"SELECT id FROM papers
WHERE publication_date < ?1"
.into(),
vec![date.to_rfc3339()],
),
}
}
fn build_paper_sql(&self) -> String {
let base = "SELECT title, abstract_text, publication_date,
source, source_identifier, pdf_url, doi
FROM papers
WHERE id = ?1";
if let Some(order_field) = &self.order_by {
let direction = if self.descending { "DESC" } else { "ASC" };
format!("{} ORDER BY {} {}", base, order_field.as_sql_str(), direction)
} else {
base.to_string()
}
}
}
#[async_trait]
impl DatabaseInstruction for Query<'_> {
type Output = Vec<Paper>;
async fn execute(&self, db: &mut Database) -> Result<Self::Output> {
let (criteria_sql, params) = self.build_criteria_sql();
let paper_sql = self.build_paper_sql();
let order_by = self.order_by;
let descending = self.descending;
let papers = db
.conn
.call(move |conn| {
let mut papers = Vec::new();
let tx = conn.transaction()?;
let paper_ids = {
let mut stmt = tx.prepare_cached(&criteria_sql)?;
let mut rows = stmt.query(params_from_iter(params))?;
let mut ids = Vec::new();
while let Some(row) = rows.next()? {
ids.push(row.get::<_, i64>(0)?);
}
ids
};
for paper_id in paper_ids {
let mut paper_stmt = tx.prepare_cached(&paper_sql)?;
let paper = paper_stmt.query_row([paper_id], |row| {
Ok(Paper {
title: row.get(0)?,
abstract_text: row.get(1)?,
publication_date: DateTime::parse_from_rfc3339(&row.get::<_, String>(2)?)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
2,
rusqlite::types::Type::Text,
Box::new(e),
)
})?,
source: row.get::<_, String>(3)?,
source_identifier: row.get(4)?,
pdf_url: row.get(5)?,
doi: row.get(6)?,
authors: Vec::new(),
})
})?;
let mut author_stmt = tx.prepare_cached(
"SELECT name, affiliation, email
FROM authors
WHERE paper_id = ?",
)?;
let authors = author_stmt
.query_map([paper_id], |row| {
Ok(Author {
name: row.get(0)?,
affiliation: row.get(1)?,
email: row.get(2)?,
})
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
let mut paper = paper;
paper.authors = authors;
papers.push(paper);
}
if let Some(order_field) = order_by {
papers.sort_by(|a, b| {
let cmp = match order_field {
OrderField::Title => a.title.cmp(&b.title),
OrderField::PublicationDate => a.publication_date.cmp(&b.publication_date),
OrderField::Source => (a.source.to_string(), &a.source_identifier)
.cmp(&(b.source.to_string(), &b.source_identifier)),
};
if descending {
cmp.reverse()
} else {
cmp
}
});
}
Ok(papers)
})
.await?;
Ok(papers)
}
}