use crate::dialect::{CurrentDialect, Dialect};
use chrono::{DateTime, Utc};
#[derive(Debug, Clone, PartialEq)]
pub enum ImageQueryExpr {
Tag(String),
And(Box<ImageQueryExpr>, Box<ImageQueryExpr>),
Or(Box<ImageQueryExpr>, Box<ImageQueryExpr>),
Not(Box<ImageQueryExpr>),
DateUntil(DateTime<Utc>),
DateSince(DateTime<Utc>),
}
impl ImageQueryExpr {
pub fn tag<T: Into<String>>(tag: T) -> Self {
ImageQueryExpr::Tag(tag.into())
}
pub fn and(self, other: impl Into<ImageQueryExpr>) -> Self {
ImageQueryExpr::And(Box::new(self), Box::new(other.into()))
}
pub fn or(self, other: impl Into<ImageQueryExpr>) -> Self {
ImageQueryExpr::Or(Box::new(self), Box::new(other.into()))
}
pub fn not(expr: impl Into<ImageQueryExpr>) -> Self {
ImageQueryExpr::Not(Box::new(expr.into()))
}
pub fn date_until(date: impl AsRef<str>) -> Self {
ImageQueryExpr::DateUntil(
DateTime::parse_from_rfc3339(date.as_ref())
.unwrap()
.with_timezone(&Utc),
)
}
pub fn date_since(date: impl AsRef<str>) -> Self {
ImageQueryExpr::DateSince(
DateTime::parse_from_rfc3339(date.as_ref())
.unwrap()
.with_timezone(&Utc),
)
}
pub fn to_sql(&self) -> (String, Vec<String>) {
let mut params = Vec::new();
let sql = self.build_sql(&mut params);
(sql, params)
}
fn build_sql(&self, params: &mut Vec<String>) -> String {
match self {
ImageQueryExpr::Tag(tag) => {
params.push(tag.clone());
CurrentDialect::exists_tag_query(params.len())
}
ImageQueryExpr::And(lhs, rhs) => {
format!("({} AND {})", lhs.build_sql(params), rhs.build_sql(params))
}
ImageQueryExpr::Or(lhs, rhs) => {
format!("({} OR {})", lhs.build_sql(params), rhs.build_sql(params))
}
ImageQueryExpr::Not(expr) => {
format!("NOT {}", expr.build_sql(params))
}
ImageQueryExpr::DateUntil(date_time) => {
params.push(date_time.to_rfc3339());
CurrentDialect::exists_date_until_query(params.len())
}
ImageQueryExpr::DateSince(date_time) => {
params.push(date_time.to_rfc3339());
CurrentDialect::exists_date_since_query(params.len())
}
}
}
}
pub fn tag(tag: impl Into<String>) -> ImageQueryExpr {
ImageQueryExpr::tag(tag)
}
pub fn date_until(date: impl AsRef<str>) -> ImageQueryExpr {
ImageQueryExpr::date_until(date)
}
pub fn date_since(date: impl AsRef<str>) -> ImageQueryExpr {
ImageQueryExpr::date_since(date)
}
pub fn not(expr: impl Into<ImageQueryExpr>) -> ImageQueryExpr {
ImageQueryExpr::not(expr.into())
}
#[derive(Debug, Clone, PartialEq)]
pub enum ImageQueryKind {
All,
Where(ImageQueryExpr),
}
impl ImageQueryKind {
pub fn to_sql(&self) -> (String, Vec<String>) {
match self {
ImageQueryKind::All => ("".to_string(), vec![]),
ImageQueryKind::Where(query_expr) => {
let (sql, params) = query_expr.to_sql();
(format!("WHERE {}", sql), params)
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum OrderBy {
CreatedAtAsc,
CreatedAtDesc,
FileSizeAsc,
FileSizeDesc,
Random,
}
impl OrderBy {
fn to_sql(&self) -> &'static str {
match self {
OrderBy::CreatedAtAsc => " ORDER BY created_at ASC",
OrderBy::CreatedAtDesc => " ORDER BY created_at DESC",
OrderBy::FileSizeAsc => " ORDER BY file_size ASC",
OrderBy::FileSizeDesc => " ORDER BY file_size DESC",
OrderBy::Random => " ORDER BY RANDOM()",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImageQuery {
pub expr: ImageQueryKind,
pub limit: Option<u32>,
pub offset: Option<u32>,
pub order: Option<OrderBy>,
}
impl ImageQuery {
pub fn new(expr: ImageQueryKind) -> Self {
Self {
expr,
limit: None,
offset: None,
order: None,
}
}
pub fn filter(expr: impl Into<ImageQueryExpr>) -> Self {
Self::new(ImageQueryKind::Where(expr.into()))
}
pub fn all() -> Self {
Self::new(ImageQueryKind::All)
}
pub fn with_limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn with_offset(mut self, offset: u32) -> Self {
self.offset = Some(offset);
self
}
pub fn with_order(mut self, order: OrderBy) -> Self {
self.order = Some(order);
self
}
pub fn to_sql(&self) -> (String, Vec<String>) {
let (mut where_sql, mut params) = self.expr.to_sql();
if let Some(order) = &self.order {
where_sql.push_str(order.to_sql());
}
if let Some(limit) = self.limit {
params.push(limit.to_string());
where_sql.push_str(
format!(
" LIMIT CAST({} AS INTEGER)",
CurrentDialect::placeholder(params.len())
)
.as_str(),
);
}
if let Some(offset) = self.offset {
params.push(offset.to_string());
where_sql.push_str(
format!(
" OFFSET CAST({} AS INTEGER)",
CurrentDialect::placeholder(params.len())
)
.as_str(),
);
}
(where_sql, params)
}
}
#[cfg(test)]
mod tests {
use super::{CurrentDialect, Dialect, ImageQuery, date_until, not, tag};
use crate::query::OrderBy;
#[test]
fn test_build_query() {
let query = ImageQuery::filter(
tag("cat")
.and(tag("cute"))
.or(not(tag("dog")))
.and(date_until("2024-12-01T00:00:00Z")),
)
.with_limit(10)
.with_offset(20)
.with_order(OrderBy::CreatedAtDesc);
let (sql, params) = query.to_sql();
assert_eq!(
format!(
"WHERE ((({} AND {}) OR NOT {}) AND {}) ORDER BY created_at DESC LIMIT CAST({} AS INTEGER) OFFSET CAST({} AS INTEGER)",
CurrentDialect::exists_tag_query(1),
CurrentDialect::exists_tag_query(2),
CurrentDialect::exists_tag_query(3),
CurrentDialect::exists_date_until_query(4),
CurrentDialect::placeholder(5),
CurrentDialect::placeholder(6),
),
sql
);
assert_eq!(
vec![
"cat",
"cute",
"dog",
"2024-12-01T00:00:00+00:00",
"10",
"20",
],
params
);
}
}