use serde_json::Value;
use crate::registry::fulltext_index::SearchMode;
use super::aggregate::AggregateSpec;
use super::planner::{QuerySpec, SortDirection};
use super::predicates::{Combine, ComparisonOp, PredicateNode};
pub struct PredicateGroupBuilder {
nodes: Vec<PredicateNode>,
}
impl PredicateGroupBuilder {
fn new() -> Self {
PredicateGroupBuilder { nodes: Vec::new() }
}
pub fn r#where(&mut self, field: impl Into<String>, op: ComparisonOp, value: Value) -> &mut Self {
self.nodes.push(PredicateNode::leaf(field, op, value));
self
}
pub fn where_any(&mut self, build: impl FnOnce(&mut PredicateGroupBuilder)) -> &mut Self {
self.nest(Combine::Or, build)
}
pub fn where_all(&mut self, build: impl FnOnce(&mut PredicateGroupBuilder)) -> &mut Self {
self.nest(Combine::And, build)
}
fn nest(&mut self, combine: Combine, build: impl FnOnce(&mut PredicateGroupBuilder)) -> &mut Self {
let mut bracket = PredicateGroupBuilder::new();
build(&mut bracket);
if !bracket.nodes.is_empty() {
self.nodes.push(PredicateNode::Group {
combine,
nodes: bracket.nodes,
});
}
self
}
fn into_nodes(self) -> Vec<PredicateNode> {
self.nodes
}
}
pub struct QueryBuilder {
root: PredicateGroupBuilder,
search: Option<(String, SearchMode)>,
order_by: Option<(String, SortDirection)>,
limit: Option<usize>,
offset: Option<usize>,
aggregate: AggregateSpec,
}
impl QueryBuilder {
pub fn new() -> Self {
QueryBuilder {
root: PredicateGroupBuilder::new(),
search: None,
order_by: None,
limit: None,
offset: None,
aggregate: AggregateSpec::default(),
}
}
pub fn r#where(mut self, field: impl Into<String>, op: ComparisonOp, value: Value) -> Self {
self.root.r#where(field, op, value);
self
}
pub fn where_any(mut self, build: impl FnOnce(&mut PredicateGroupBuilder)) -> Self {
self.root.where_any(build);
self
}
pub fn where_all(mut self, build: impl FnOnce(&mut PredicateGroupBuilder)) -> Self {
self.root.where_all(build);
self
}
pub fn search(mut self, text: impl Into<String>, mode: SearchMode) -> Self {
self.search = Some((text.into(), mode));
self
}
pub fn order_by(mut self, field: impl Into<String>, direction: SortDirection) -> Self {
self.order_by = Some((field.into(), direction));
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
pub fn count(mut self) -> Self {
self.aggregate.count = true;
self
}
pub fn sum(mut self, field: impl Into<String>) -> Self {
self.aggregate.sum.push(field.into());
self
}
pub fn avg(mut self, field: impl Into<String>) -> Self {
self.aggregate.avg.push(field.into());
self
}
pub fn min(mut self, field: impl Into<String>) -> Self {
self.aggregate.min.push(field.into());
self
}
pub fn max(mut self, field: impl Into<String>) -> Self {
self.aggregate.max.push(field.into());
self
}
pub fn group_by(mut self, field: impl Into<String>) -> Self {
self.aggregate.group_by = Some(field.into());
self
}
pub fn spec(self) -> QuerySpec {
let mut spec = QuerySpec::new(PredicateNode::Group {
combine: Combine::And,
nodes: self.root.into_nodes(),
});
if let Some((text, mode)) = self.search {
spec = spec.with_search(text, mode);
}
if let Some((field, direction)) = self.order_by {
spec = spec.with_order_by(field, direction);
}
if let Some(limit) = self.limit {
spec = spec.with_limit(limit);
}
if let Some(offset) = self.offset {
spec = spec.with_offset(offset);
}
spec
}
pub fn build_aggregate(mut self) -> (QuerySpec, AggregateSpec) {
let aggregate = std::mem::take(&mut self.aggregate);
(self.spec(), aggregate)
}
}
impl Default for QueryBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn builds_a_flat_conjunction() {
let spec = QueryBuilder::new()
.r#where("role", ComparisonOp::Eq, json!("admin"))
.r#where("age", ComparisonOp::Gt, json!(30))
.spec();
match spec.root {
PredicateNode::Group {
combine: Combine::And,
nodes,
} => assert_eq!(nodes.len(), 2),
_ => panic!("expected a top-level conjunction"),
}
}
#[test]
fn nests_an_and_bracket_inside_an_or_bracket() {
let spec = QueryBuilder::new()
.where_any(|group| {
group
.r#where("role", ComparisonOp::Eq, json!("admin"))
.where_all(|inner| {
inner.r#where("role", ComparisonOp::Eq, json!("user")).r#where(
"age",
ComparisonOp::Gt,
json!(35),
);
});
})
.spec();
let PredicateNode::Group { nodes, .. } = spec.root else {
panic!("expected root group")
};
assert_eq!(nodes.len(), 1);
let PredicateNode::Group {
combine: Combine::Or,
nodes: or_nodes,
} = &nodes[0]
else {
panic!("expected an or bracket")
};
assert_eq!(or_nodes.len(), 2);
assert!(matches!(
or_nodes[1],
PredicateNode::Group {
combine: Combine::And,
..
}
));
}
#[test]
fn drops_an_empty_bracket_instead_of_keeping_a_hollow_node() {
let spec = QueryBuilder::new().where_any(|_group| {}).spec();
let PredicateNode::Group { nodes, .. } = spec.root else {
panic!("expected root group")
};
assert!(nodes.is_empty());
}
}