use serde_json::Value;
use tokio_stream::Stream;
#[allow(
clippy::missing_docs_in_private_items,
reason = "fields are documented with ///"
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Query {
pub filters: Vec<Filter>,
pub sort: Option<(String, SortOrder)>,
pub limit: Option<usize>,
pub offset: Option<usize>,
pub projection: Option<Vec<String>>,
}
pub struct QueryResult {
pub documents: std::pin::Pin<Box<dyn Stream<Item = crate::Result<crate::Document>> + Send>>,
pub total_count: Option<usize>,
pub execution_time: std::time::Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortOrder {
Ascending,
Descending,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Filter {
Equals(String, Value),
GreaterThan(String, Value),
LessThan(String, Value),
GreaterOrEqual(String, Value),
LessOrEqual(String, Value),
Contains(String, String),
StartsWith(String, String),
EndsWith(String, String),
In(String, Vec<Value>),
Exists(String, bool),
And(Box<Self>, Box<Self>),
Or(Box<Self>, Box<Self>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Operator {
Equals,
GreaterThan,
LessThan,
GreaterOrEqual,
LessOrEqual,
Contains,
StartsWith,
EndsWith,
In,
Exists,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryBuilder {
filters: Vec<Filter>,
sort: Option<(String, SortOrder)>,
limit: Option<usize>,
offset: Option<usize>,
projection: Option<Vec<String>>,
}
impl Default for QueryBuilder {
fn default() -> Self { Self::new() }
}
impl QueryBuilder {
pub const fn new() -> Self {
Self {
filters: Vec::new(),
sort: None,
limit: None,
offset: None,
projection: None,
}
}
pub fn filter(mut self, field: &str, op: Operator, value: Value) -> Self {
let filter = match op {
Operator::Equals => Filter::Equals(field.to_owned(), value),
Operator::GreaterThan => Filter::GreaterThan(field.to_owned(), value),
Operator::LessThan => Filter::LessThan(field.to_owned(), value),
Operator::GreaterOrEqual => Filter::GreaterOrEqual(field.to_owned(), value),
Operator::LessOrEqual => Filter::LessOrEqual(field.to_owned(), value),
Operator::Contains => {
if let Value::String(s) = value {
Filter::Contains(field.to_owned(), s)
}
else {
return self;
}
},
Operator::StartsWith => {
if let Value::String(s) = value {
Filter::StartsWith(field.to_owned(), s)
}
else {
return self;
}
},
Operator::EndsWith => {
if let Value::String(s) = value {
Filter::EndsWith(field.to_owned(), s)
}
else {
return self;
}
},
Operator::In => {
if let Value::Array(arr) = value {
Filter::In(field.to_owned(), arr)
}
else {
return self;
}
},
Operator::Exists => {
let exists = match value {
Value::Bool(b) => b,
Value::Number(n) if n.as_i64() == Some(1) => true,
Value::Number(n) if n.as_i64() == Some(0) => false,
Value::Null | Value::Number(_) | Value::String(_) | Value::Array(_) | Value::Object(_) => true,
};
Filter::Exists(field.to_owned(), exists)
},
};
self.filters.push(filter);
self
}
pub fn and(mut self, other: Filter) -> Self {
if let Some(last) = self.filters.pop() {
let combined = Filter::And(Box::new(last), Box::new(other));
self.filters.push(combined);
}
else {
self.filters.push(other);
}
self
}
pub fn or(mut self, other: Filter) -> Self {
if let Some(last) = self.filters.pop() {
let combined = Filter::Or(Box::new(last), Box::new(other));
self.filters.push(combined);
}
else {
self.filters.push(other);
}
self
}
pub fn sort(mut self, field: &str, order: SortOrder) -> Self {
self.sort = Some((field.to_owned(), order));
self
}
pub const fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub const fn offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
pub fn projection(mut self, fields: Vec<&str>) -> Self {
self.projection = Some(fields.into_iter().map(|s| s.to_owned()).collect());
self
}
pub fn build(self) -> Query {
Query {
filters: self.filters,
sort: self.sort,
limit: self.limit,
offset: self.offset,
projection: self.projection,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Aggregation {
Count,
Sum(String),
Avg(String),
Min(String),
Max(String),
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_query_builder_new() {
let qb = QueryBuilder::new();
assert!(qb.filters.is_empty());
assert!(qb.sort.is_none());
assert!(qb.limit.is_none());
assert!(qb.offset.is_none());
assert!(qb.projection.is_none());
}
#[test]
fn test_query_builder_default() {
let qb = QueryBuilder::default();
assert!(qb.filters.is_empty());
assert!(qb.sort.is_none());
assert!(qb.limit.is_none());
assert!(qb.offset.is_none());
assert!(qb.projection.is_none());
}
#[test]
fn test_query_builder_filter_equals() {
let qb = QueryBuilder::new().filter("name", Operator::Equals, json!("Alice"));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Equals(field, value) => {
assert_eq!(field, "name");
assert_eq!(value, &json!("Alice"));
},
_ => panic!("Expected Equals filter"),
}
}
#[test]
fn test_query_builder_filter_greater_than() {
let qb = QueryBuilder::new().filter("age", Operator::GreaterThan, json!(18));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::GreaterThan(field, value) => {
assert_eq!(field, "age");
assert_eq!(value, &json!(18));
},
_ => panic!("Expected GreaterThan filter"),
}
}
#[test]
fn test_query_builder_filter_less_than() {
let qb = QueryBuilder::new().filter("age", Operator::LessThan, json!(65));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::LessThan(field, value) => {
assert_eq!(field, "age");
assert_eq!(value, &json!(65));
},
_ => panic!("Expected LessThan filter"),
}
}
#[test]
fn test_query_builder_filter_greater_or_equal() {
let qb = QueryBuilder::new().filter("age", Operator::GreaterOrEqual, json!(18));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::GreaterOrEqual(field, value) => {
assert_eq!(field, "age");
assert_eq!(value, &json!(18));
},
_ => panic!("Expected GreaterOrEqual filter"),
}
}
#[test]
fn test_query_builder_filter_less_or_equal() {
let qb = QueryBuilder::new().filter("age", Operator::LessOrEqual, json!(65));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::LessOrEqual(field, value) => {
assert_eq!(field, "age");
assert_eq!(value, &json!(65));
},
_ => panic!("Expected LessOrEqual filter"),
}
}
#[test]
fn test_query_builder_filter_contains_valid() {
let qb = QueryBuilder::new().filter("name", Operator::Contains, json!("Ali"));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Contains(field, value) => {
assert_eq!(field, "name");
assert_eq!(value, "Ali");
},
_ => panic!("Expected Contains filter"),
}
}
#[test]
fn test_query_builder_filter_contains_invalid() {
let qb = QueryBuilder::new().filter("name", Operator::Contains, json!(123));
assert!(qb.filters.is_empty());
}
#[test]
fn test_query_builder_filter_starts_with_valid() {
let qb = QueryBuilder::new().filter("name", Operator::StartsWith, json!("Ali"));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::StartsWith(field, value) => {
assert_eq!(field, "name");
assert_eq!(value, "Ali");
},
_ => panic!("Expected StartsWith filter"),
}
}
#[test]
fn test_query_builder_filter_starts_with_invalid() {
let qb = QueryBuilder::new().filter("name", Operator::StartsWith, json!(123));
assert!(qb.filters.is_empty());
}
#[test]
fn test_query_builder_filter_ends_with_valid() {
let qb = QueryBuilder::new().filter("name", Operator::EndsWith, json!("ice"));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::EndsWith(field, value) => {
assert_eq!(field, "name");
assert_eq!(value, "ice");
},
_ => panic!("Expected EndsWith filter"),
}
}
#[test]
fn test_query_builder_filter_ends_with_invalid() {
let qb = QueryBuilder::new().filter("name", Operator::EndsWith, json!(123));
assert!(qb.filters.is_empty());
}
#[test]
fn test_query_builder_filter_in_valid() {
let qb = QueryBuilder::new().filter("status", Operator::In, json!(["active", "inactive"]));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::In(field, values) => {
assert_eq!(field, "status");
assert_eq!(values, &vec![json!("active"), json!("inactive")]);
},
_ => panic!("Expected In filter"),
}
}
#[test]
fn test_query_builder_filter_in_invalid() {
let qb = QueryBuilder::new().filter("status", Operator::In, json!("active"));
assert!(qb.filters.is_empty());
}
#[test]
fn test_query_builder_filter_exists_bool() {
let qb = QueryBuilder::new().filter("name", Operator::Exists, json!(true));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Exists(field, exists) => {
assert_eq!(field, "name");
assert!(*exists);
},
_ => panic!("Expected Exists filter"),
}
}
#[test]
fn test_query_builder_filter_exists_number() {
let qb = QueryBuilder::new().filter("name", Operator::Exists, json!(1));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Exists(field, exists) => {
assert_eq!(field, "name");
assert!(*exists);
},
_ => panic!("Expected Exists filter"),
}
}
#[test]
fn test_query_builder_filter_exists_false() {
let qb = QueryBuilder::new().filter("name", Operator::Exists, json!(false));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Exists(field, exists) => {
assert_eq!(field, "name");
assert!(!*exists);
},
_ => panic!("Expected Exists filter"),
}
}
#[test]
fn test_query_builder_filter_exists_string() {
let qb = QueryBuilder::new().filter("name", Operator::Exists, json!("yes"));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Exists(field, exists) => {
assert_eq!(field, "name");
assert!(*exists); },
_ => panic!("Expected Exists filter"),
}
}
#[test]
fn test_query_builder_sort() {
let qb = QueryBuilder::new().sort("age", SortOrder::Descending);
assert_eq!(qb.sort, Some(("age".to_string(), SortOrder::Descending)));
}
#[test]
fn test_query_builder_limit() {
let qb = QueryBuilder::new().limit(10);
assert_eq!(qb.limit, Some(10));
}
#[test]
fn test_query_builder_offset() {
let qb = QueryBuilder::new().offset(5);
assert_eq!(qb.offset, Some(5));
}
#[test]
fn test_query_builder_projection() {
let qb = QueryBuilder::new().projection(vec!["name", "age"]);
assert_eq!(
qb.projection,
Some(vec!["name".to_string(), "age".to_string()])
);
}
#[test]
fn test_query_builder_and() {
let qb = QueryBuilder::new()
.filter("age", Operator::GreaterThan, json!(18))
.and(Filter::Equals("status".to_string(), json!("active")));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::And(left, right) => {
match **left {
Filter::GreaterThan(ref field, _) => assert_eq!(field, "age"),
_ => panic!("Expected GreaterThan in left"),
}
match **right {
Filter::Equals(ref field, _) => assert_eq!(field, "status"),
_ => panic!("Expected Equals in right"),
}
},
_ => panic!("Expected And filter"),
}
}
#[test]
fn test_query_builder_or() {
let qb = QueryBuilder::new()
.filter("age", Operator::GreaterThan, json!(18))
.or(Filter::Equals("status".to_string(), json!("active")));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Or(left, right) => {
match **left {
Filter::GreaterThan(ref field, _) => assert_eq!(field, "age"),
_ => panic!("Expected GreaterThan in left"),
}
match **right {
Filter::Equals(ref field, _) => assert_eq!(field, "status"),
_ => panic!("Expected Equals in right"),
}
},
_ => panic!("Expected Or filter"),
}
}
#[test]
fn test_query_builder_build() {
let query = QueryBuilder::new()
.filter("age", Operator::GreaterThan, json!(18))
.sort("name", SortOrder::Ascending)
.limit(10)
.offset(5)
.projection(vec!["name", "age"])
.build();
assert_eq!(query.filters.len(), 1);
assert_eq!(query.sort, Some(("name".to_string(), SortOrder::Ascending)));
assert_eq!(query.limit, Some(10));
assert_eq!(query.offset, Some(5));
assert_eq!(
query.projection,
Some(vec!["name".to_string(), "age".to_string()])
);
}
#[test]
fn test_query_builder_filter_exists_number_zero() {
let qb = QueryBuilder::new().filter("name", Operator::Exists, json!(0));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Exists(field, exists) => {
assert_eq!(field, "name");
assert!(!*exists);
},
_ => panic!("Expected Exists filter"),
}
}
#[test]
fn test_query_builder_and_empty() {
let qb = QueryBuilder::new().and(Filter::Equals("status".to_string(), json!("active")));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Equals(field, _) => assert_eq!(field, "status"),
_ => panic!("Expected Equals filter"),
}
}
#[test]
fn test_query_builder_or_empty() {
let qb = QueryBuilder::new().or(Filter::Equals("status".to_string(), json!("active")));
assert_eq!(qb.filters.len(), 1);
match &qb.filters[0] {
Filter::Equals(field, _) => assert_eq!(field, "status"),
_ => panic!("Expected Equals filter"),
}
}
}