Skip to content
280 changes: 13 additions & 267 deletions crates/storage-mongodb/src/condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@ pub fn condition_to_filter(expr: &Expr, maps: &ExpressionMaps) -> Result<Documen

/// Resolve a path to the `MongoDB` field path within `item_data`.
///
/// `DynamoDB` paths like `address.city` or `tags[0]` become
/// `item_data.address.city` or `item_data.tags.L.0` in `MongoDB`.
/// `item_data` holds each attribute value in its typed form, so a map's
/// members sit under its `M` field and a list's elements under its `L` field.
/// `DynamoDB` paths like `address.city`, `tags[0]` or `tags[0].name` become
/// `item_data.address.M.city`, `item_data.tags.L.0` or
/// `item_data.tags.L.0.M.name` in `MongoDB`.
fn resolve_path_to_field(
elements: &[PathElement],
maps: &ExpressionMaps,
) -> Result<String, StorageError> {
let mut parts = vec!["item_data".to_string()];
for elem in elements {
for (position, elem) in elements.iter().enumerate() {
match elem {
PathElement::Attribute(name) => {
let resolved = if let Some(stripped) = name.strip_prefix('#') {
Expand All @@ -39,6 +42,11 @@ fn resolve_path_to_field(
} else {
name.clone()
};
// A member name after the top-level attribute is a key of
// the enclosing map.
if position > 0 {
parts.push("M".to_string());
}
parts.push(resolved);
}
PathElement::Index(idx) => {
Expand Down Expand Up @@ -493,267 +501,5 @@ fn regex_escape(s: &str) -> String {
// ============================================================================

#[cfg(test)]
mod tests {
use super::*;
use extenddb_core::expression::ExpressionMaps;
use extenddb_core::types::AttributeValue;
use std::collections::HashMap;

fn make_maps(names: Vec<(&str, &str)>, values: Vec<(&str, AttributeValue)>) -> ExpressionMaps {
let names_map: HashMap<String, String> = names
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let values_map: HashMap<String, AttributeValue> = values
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
ExpressionMaps::new(names_map, values_map)
}

#[test]
fn test_attribute_exists() {
let maps = make_maps(vec![], vec![]);
let expr = Expr::Function {
name: "attribute_exists".to_string(),
args: vec![Expr::Path(vec![PathElement::Attribute("foo".to_string())])],
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(filter, doc! { "item_data.foo": { "$exists": true } });
}

#[test]
fn test_attribute_not_exists() {
let maps = make_maps(vec![], vec![]);
let expr = Expr::Function {
name: "attribute_not_exists".to_string(),
args: vec![Expr::Path(vec![PathElement::Attribute("bar".to_string())])],
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(filter, doc! { "item_data.bar": { "$exists": false } });
}

#[test]
fn test_equality_comparison_string() {
let maps = make_maps(
vec![],
vec![(":val", AttributeValue::S("hello".to_string()))],
);
let expr = Expr::Compare {
left: Box::new(Expr::Path(vec![PathElement::Attribute("name".to_string())])),
op: CompareOp::Eq,
right: Box::new(Expr::Placeholder(":val".to_string())),
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(filter, doc! { "item_data.name.S": "hello" });
}

#[test]
fn test_less_than_comparison_number() {
let maps = make_maps(vec![], vec![(":min", AttributeValue::N("100".to_string()))]);
let expr = Expr::Compare {
left: Box::new(Expr::Path(vec![PathElement::Attribute(
"price".to_string(),
)])),
op: CompareOp::Lt,
right: Box::new(Expr::Placeholder(":min".to_string())),
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(filter, doc! { "item_data.price.N": { "$lt": "100" } });
}

#[test]
fn test_and_condition() {
let maps = make_maps(
vec![],
vec![
(":v1", AttributeValue::S("active".to_string())),
(":v2", AttributeValue::N("5".to_string())),
],
);
let expr = Expr::And(
Box::new(Expr::Compare {
left: Box::new(Expr::Path(vec![PathElement::Attribute(
"status".to_string(),
)])),
op: CompareOp::Eq,
right: Box::new(Expr::Placeholder(":v1".to_string())),
}),
Box::new(Expr::Compare {
left: Box::new(Expr::Path(vec![PathElement::Attribute(
"count".to_string(),
)])),
op: CompareOp::Gt,
right: Box::new(Expr::Placeholder(":v2".to_string())),
}),
);
let filter = condition_to_filter(&expr, &maps).unwrap();
let expected = doc! {
"$and": [
{ "item_data.status.S": "active" },
{ "item_data.count.N": { "$gt": "5" } }
]
};
assert_eq!(filter, expected);
}

#[test]
fn test_or_condition() {
let maps = make_maps(
vec![],
vec![
(":a", AttributeValue::S("x".to_string())),
(":b", AttributeValue::S("y".to_string())),
],
);
let expr = Expr::Or(
Box::new(Expr::Compare {
left: Box::new(Expr::Path(vec![PathElement::Attribute("f".to_string())])),
op: CompareOp::Eq,
right: Box::new(Expr::Placeholder(":a".to_string())),
}),
Box::new(Expr::Compare {
left: Box::new(Expr::Path(vec![PathElement::Attribute("f".to_string())])),
op: CompareOp::Eq,
right: Box::new(Expr::Placeholder(":b".to_string())),
}),
);
let filter = condition_to_filter(&expr, &maps).unwrap();
let expected = doc! {
"$or": [
{ "item_data.f.S": "x" },
{ "item_data.f.S": "y" }
]
};
assert_eq!(filter, expected);
}

#[test]
fn test_not_condition() {
let maps = make_maps(vec![], vec![]);
let expr = Expr::Not(Box::new(Expr::Function {
name: "attribute_exists".to_string(),
args: vec![Expr::Path(vec![PathElement::Attribute(
"deleted".to_string(),
)])],
}));
let filter = condition_to_filter(&expr, &maps).unwrap();
let expected = doc! {
"$nor": [{ "item_data.deleted": { "$exists": true } }]
};
assert_eq!(filter, expected);
}

#[test]
fn test_begins_with() {
let maps = make_maps(
vec![],
vec![(":prefix", AttributeValue::S("user#".to_string()))],
);
let expr = Expr::Function {
name: "begins_with".to_string(),
args: vec![
Expr::Path(vec![PathElement::Attribute("sk".to_string())]),
Expr::Placeholder(":prefix".to_string()),
],
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(filter, doc! { "item_data.sk.S": { "$regex": "^user#" } });
}

#[test]
fn test_between() {
let maps = make_maps(
vec![],
vec![
(":lo", AttributeValue::N("10".to_string())),
(":hi", AttributeValue::N("20".to_string())),
],
);
let expr = Expr::Between {
operand: Box::new(Expr::Path(vec![PathElement::Attribute("age".to_string())])),
low: Box::new(Expr::Placeholder(":lo".to_string())),
high: Box::new(Expr::Placeholder(":hi".to_string())),
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(
filter,
doc! { "item_data.age.N": { "$gte": "10", "$lte": "20" } }
);
}

#[test]
fn test_in_condition() {
let maps = make_maps(
vec![],
vec![
(":v1", AttributeValue::S("a".to_string())),
(":v2", AttributeValue::S("b".to_string())),
(":v3", AttributeValue::S("c".to_string())),
],
);
let expr = Expr::In {
operand: Box::new(Expr::Path(vec![PathElement::Attribute("x".to_string())])),
list: vec![
Expr::Placeholder(":v1".to_string()),
Expr::Placeholder(":v2".to_string()),
Expr::Placeholder(":v3".to_string()),
],
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(filter, doc! { "item_data.x.S": { "$in": ["a", "b", "c"] } });
}

#[test]
fn test_name_ref_resolution() {
let maps = make_maps(
vec![("n", "status")],
vec![(":v", AttributeValue::S("active".to_string()))],
);
let expr = Expr::Compare {
left: Box::new(Expr::Path(vec![PathElement::Attribute("#n".to_string())])),
op: CompareOp::Eq,
right: Box::new(Expr::Placeholder(":v".to_string())),
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(filter, doc! { "item_data.status.S": "active" });
}

#[test]
fn test_ne_comparison() {
let maps = make_maps(
vec![],
vec![(":v", AttributeValue::S("deleted".to_string()))],
);
let expr = Expr::Compare {
left: Box::new(Expr::Path(vec![PathElement::Attribute(
"status".to_string(),
)])),
op: CompareOp::Ne,
right: Box::new(Expr::Placeholder(":v".to_string())),
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(filter, doc! { "item_data.status.S": { "$ne": "deleted" } });
}

#[test]
fn test_regex_escape() {
assert_eq!(regex_escape("user.name"), "user\\.name");
assert_eq!(regex_escape("a+b"), "a\\+b");
assert_eq!(regex_escape("normal"), "normal");
}

#[test]
fn test_attribute_type() {
let maps = make_maps(vec![], vec![(":t", AttributeValue::S("S".to_string()))]);
let expr = Expr::Function {
name: "attribute_type".to_string(),
args: vec![
Expr::Path(vec![PathElement::Attribute("field".to_string())]),
Expr::Placeholder(":t".to_string()),
],
};
let filter = condition_to_filter(&expr, &maps).unwrap();
assert_eq!(filter, doc! { "item_data.field.S": { "$exists": true } });
}
}
#[path = "condition_tests.rs"]
mod tests;
Loading
Loading