Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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