use sea_orm::sea_query::{Alias, Expr, ExprTrait, SimpleExpr};
use crate::db::models::constants::LOOKUP_SEP;
const KNOWN_LOOKUPS: &[&str] = &[
"exact",
"iexact",
"contains",
"icontains",
"gt",
"gte",
"lt",
"lte",
"startswith",
"endswith",
"in",
"isnull",
"range",
"regex",
"iregex",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LookupPath {
pub relations: Vec<String>,
pub field: String,
pub lookup: String,
}
#[must_use]
pub fn related_lookup(relation_path: &str, field: &str, value: SimpleExpr) -> SimpleExpr {
let column = if relation_path.is_empty() {
field.to_string()
} else {
format!("{relation_path}.{field}")
};
Expr::col(Alias::new(&column)).eq(value)
}
#[must_use]
pub fn parse_lookup_path(path: &str) -> LookupPath {
if path.is_empty() {
return LookupPath {
relations: Vec::new(),
field: String::new(),
lookup: "exact".to_string(),
};
}
let parts: Vec<&str> = path.split(LOOKUP_SEP).collect();
match parts.len() {
0 => LookupPath {
relations: Vec::new(),
field: String::new(),
lookup: "exact".to_string(),
},
1 => LookupPath {
relations: Vec::new(),
field: parts[0].to_string(),
lookup: "exact".to_string(),
},
_ => {
let last = parts[parts.len() - 1];
if KNOWN_LOOKUPS.contains(&last) {
LookupPath {
relations: parts[..parts.len() - 2]
.iter()
.map(|part| (*part).to_string())
.collect(),
field: parts[parts.len() - 2].to_string(),
lookup: last.to_string(),
}
} else {
LookupPath {
relations: parts[..parts.len() - 1]
.iter()
.map(|part| (*part).to_string())
.collect(),
field: last.to_string(),
lookup: "exact".to_string(),
}
}
}
}
}
#[cfg(test)]
mod tests {
use sea_orm::Condition;
use sea_orm::sea_query::{Alias, Expr, Query, SimpleExpr, SqliteQueryBuilder};
use super::{LookupPath, parse_lookup_path, related_lookup};
fn render_where(expr: SimpleExpr) -> String {
Query::select()
.column(Alias::new("id"))
.from(Alias::new("widgets"))
.cond_where(Condition::all().add(expr))
.to_owned()
.to_string(SqliteQueryBuilder)
}
#[test]
fn parse_lookup_path_handles_empty_path() {
assert_eq!(
parse_lookup_path(""),
LookupPath {
relations: vec![],
field: String::new(),
lookup: "exact".to_string(),
}
);
}
#[test]
fn parse_lookup_path_simple_field_defaults_to_exact() {
assert_eq!(
parse_lookup_path("title"),
LookupPath {
relations: vec![],
field: "title".to_string(),
lookup: "exact".to_string(),
}
);
}
#[test]
fn parse_lookup_path_field_and_lookup() {
assert_eq!(
parse_lookup_path("title__icontains"),
LookupPath {
relations: vec![],
field: "title".to_string(),
lookup: "icontains".to_string(),
}
);
}
#[test]
fn parse_lookup_path_relation_and_field() {
assert_eq!(
parse_lookup_path("author__name"),
LookupPath {
relations: vec!["author".to_string()],
field: "name".to_string(),
lookup: "exact".to_string(),
}
);
}
#[test]
fn parse_lookup_path_relation_field_and_lookup() {
assert_eq!(
parse_lookup_path("author__name__contains"),
LookupPath {
relations: vec!["author".to_string()],
field: "name".to_string(),
lookup: "contains".to_string(),
}
);
}
#[test]
fn parse_lookup_path_handles_deep_relation_chain() {
assert_eq!(
parse_lookup_path("publisher__country__region__name__iexact"),
LookupPath {
relations: vec![
"publisher".to_string(),
"country".to_string(),
"region".to_string(),
],
field: "name".to_string(),
lookup: "iexact".to_string(),
}
);
}
#[test]
fn parse_lookup_path_unknown_suffix_defaults_to_exact_lookup() {
assert_eq!(
parse_lookup_path("author__name__customlookup"),
LookupPath {
relations: vec!["author".to_string(), "name".to_string()],
field: "customlookup".to_string(),
lookup: "exact".to_string(),
}
);
}
#[test]
fn parse_lookup_path_recognizes_isnull_lookup() {
assert_eq!(
parse_lookup_path("author__deleted_at__isnull"),
LookupPath {
relations: vec!["author".to_string()],
field: "deleted_at".to_string(),
lookup: "isnull".to_string(),
}
);
}
#[test]
fn parse_lookup_path_recognizes_in_lookup() {
assert_eq!(
parse_lookup_path("author__id__in"),
LookupPath {
relations: vec!["author".to_string()],
field: "id".to_string(),
lookup: "in".to_string(),
}
);
}
#[test]
fn related_lookup_produces_expression_with_relation_alias() {
let sql = render_where(related_lookup("author", "name", Expr::val("Alice")));
assert!(sql.contains("author.name"), "unexpected SQL: {sql}");
assert!(sql.contains("Alice"), "unexpected SQL: {sql}");
}
}