use crate::filter::{FilterExpr, RelationQuantifier};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Orderable;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Unorderable;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RelationHop {
pub parent_table: &'static str,
pub parent_column: &'static str,
pub related_table: &'static str,
pub related_column: &'static str,
pub quantifier: RelationQuantifier,
}
impl RelationHop {
pub const fn new(
parent_table: &'static str,
parent_column: &'static str,
related_table: &'static str,
related_column: &'static str,
quantifier: RelationQuantifier,
) -> Self {
Self {
parent_table,
parent_column,
related_table,
related_column,
quantifier,
}
}
pub const fn with_quantifier(self, quantifier: RelationQuantifier) -> Self {
Self { quantifier, ..self }
}
}
pub fn wrap_filter(hops: &[RelationHop], inner: FilterExpr) -> FilterExpr {
hops.iter()
.rev()
.fold(inner, |acc, hop| match hop.quantifier {
RelationQuantifier::ToOne => FilterExpr::relation(
hop.parent_table,
hop.parent_column,
hop.related_table,
hop.related_column,
acc,
),
RelationQuantifier::Some => FilterExpr::relation_some(
hop.parent_table,
hop.parent_column,
hop.related_table,
hop.related_column,
acc,
),
RelationQuantifier::Every => FilterExpr::relation_every(
hop.parent_table,
hop.parent_column,
hop.related_table,
hop.related_column,
acc,
),
RelationQuantifier::None => FilterExpr::relation_none(
hop.parent_table,
hop.parent_column,
hop.related_table,
hop.related_column,
acc,
),
})
}
pub fn order_value_sql(hops: &[RelationHop], column: &str) -> String {
assert!(
!hops.is_empty(),
"order_value_sql requires at least one relation hop",
);
let mut sql = format!("{}.{}", hops[hops.len() - 1].related_table, column,);
for index in (1..hops.len()).rev() {
let hop = &hops[index];
let current_table = hops[index - 1].related_table;
sql = format!(
"(SELECT {} FROM {} WHERE {}.{} = {}.{} LIMIT 1)",
sql,
hop.related_table,
hop.related_table,
hop.related_column,
current_table,
hop.parent_column,
);
}
sql
}
pub fn is_orderable(hops: &[RelationHop]) -> bool {
hops.iter()
.all(|hop| matches!(hop.quantifier, RelationQuantifier::ToOne))
}
#[cfg(test)]
mod tests {
use super::*;
const fn to_one(
parent_table: &'static str,
parent_column: &'static str,
related_table: &'static str,
related_column: &'static str,
) -> RelationHop {
RelationHop::new(
parent_table,
parent_column,
related_table,
related_column,
RelationQuantifier::ToOne,
)
}
#[test]
fn single_hop_reads_the_related_table_directly() {
let hops = [to_one("posts", "author_id", "users", "id")];
assert_eq!(order_value_sql(&hops, "email"), "users.email");
}
#[test]
fn two_hops_nest_a_correlated_subquery() {
let hops = [
to_one("posts", "author_id", "users", "id"),
to_one("users", "profile_id", "profiles", "id"),
];
assert_eq!(
order_value_sql(&hops, "nickname"),
"(SELECT profiles.nickname FROM profiles \
WHERE profiles.id = users.profile_id LIMIT 1)",
);
}
#[test]
fn a_to_many_hop_makes_the_path_unorderable() {
let hops = [
to_one("posts", "author_id", "users", "id"),
RelationHop::new(
"users",
"id",
"comments",
"user_id",
RelationQuantifier::Some,
),
];
assert!(!is_orderable(&hops));
assert!(is_orderable(&hops[..1]));
}
}