BREAKING CHANGE(core): replace the TypeScript database engine with a Rust-backed embedded server and bridge
This commit is contained in:
574
rust/crates/rustdb-query/src/matcher.rs
Normal file
574
rust/crates/rustdb-query/src/matcher.rs
Normal file
@@ -0,0 +1,574 @@
|
||||
use bson::{Bson, Document};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::field_path::get_nested_value;
|
||||
|
||||
/// Query matching engine.
|
||||
/// Evaluates filter documents against BSON documents.
|
||||
pub struct QueryMatcher;
|
||||
|
||||
impl QueryMatcher {
|
||||
/// Test whether a single document matches a filter.
|
||||
pub fn matches(doc: &Document, filter: &Document) -> bool {
|
||||
Self::matches_filter(doc, filter)
|
||||
}
|
||||
|
||||
/// Filter a slice of documents, returning those that match.
|
||||
pub fn filter(docs: &[Document], filter: &Document) -> Vec<Document> {
|
||||
if filter.is_empty() {
|
||||
return docs.to_vec();
|
||||
}
|
||||
docs.iter()
|
||||
.filter(|doc| Self::matches_filter(doc, filter))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find the first document matching a filter.
|
||||
pub fn find_one(docs: &[Document], filter: &Document) -> Option<Document> {
|
||||
docs.iter()
|
||||
.find(|doc| Self::matches_filter(doc, filter))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn matches_filter(doc: &Document, filter: &Document) -> bool {
|
||||
for (key, value) in filter {
|
||||
if !Self::matches_condition(doc, key, value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn matches_condition(doc: &Document, key: &str, condition: &Bson) -> bool {
|
||||
match key {
|
||||
"$and" => Self::match_logical_and(doc, condition),
|
||||
"$or" => Self::match_logical_or(doc, condition),
|
||||
"$nor" => Self::match_logical_nor(doc, condition),
|
||||
"$not" => Self::match_logical_not(doc, condition),
|
||||
"$expr" => {
|
||||
// Basic $expr support - just return true for now
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
// Field condition
|
||||
match condition {
|
||||
Bson::Document(cond_doc) if Self::has_operators(cond_doc) => {
|
||||
Self::match_field_operators(doc, key, cond_doc)
|
||||
}
|
||||
// Implicit equality
|
||||
_ => Self::match_equality(doc, key, condition),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_operators(doc: &Document) -> bool {
|
||||
doc.keys().any(|k| k.starts_with('$'))
|
||||
}
|
||||
|
||||
/// Public accessor for has_operators (used by update engine).
|
||||
pub fn has_operators_pub(doc: &Document) -> bool {
|
||||
Self::has_operators(doc)
|
||||
}
|
||||
|
||||
/// Public accessor for bson_compare (used by update engine).
|
||||
pub fn bson_compare_pub(a: &Bson, b: &Bson) -> Option<std::cmp::Ordering> {
|
||||
Self::bson_compare(a, b)
|
||||
}
|
||||
|
||||
fn match_equality(doc: &Document, field: &str, expected: &Bson) -> bool {
|
||||
let actual = Self::resolve_field(doc, field);
|
||||
match actual {
|
||||
Some(val) => Self::bson_equals(&val, expected),
|
||||
None => matches!(expected, Bson::Null),
|
||||
}
|
||||
}
|
||||
|
||||
fn match_field_operators(doc: &Document, field: &str, operators: &Document) -> bool {
|
||||
let actual = Self::resolve_field(doc, field);
|
||||
|
||||
for (op, op_value) in operators {
|
||||
let result = match op.as_str() {
|
||||
"$eq" => Self::op_eq(&actual, op_value),
|
||||
"$ne" => Self::op_ne(&actual, op_value),
|
||||
"$gt" => Self::op_cmp(&actual, op_value, CmpOp::Gt),
|
||||
"$gte" => Self::op_cmp(&actual, op_value, CmpOp::Gte),
|
||||
"$lt" => Self::op_cmp(&actual, op_value, CmpOp::Lt),
|
||||
"$lte" => Self::op_cmp(&actual, op_value, CmpOp::Lte),
|
||||
"$in" => Self::op_in(&actual, op_value),
|
||||
"$nin" => Self::op_nin(&actual, op_value),
|
||||
"$exists" => Self::op_exists(&actual, op_value),
|
||||
"$type" => Self::op_type(&actual, op_value),
|
||||
"$regex" => Self::op_regex(&actual, op_value, operators.get("$options")),
|
||||
"$not" => Self::op_not(doc, field, op_value),
|
||||
"$elemMatch" => Self::op_elem_match(&actual, op_value),
|
||||
"$size" => Self::op_size(&actual, op_value),
|
||||
"$all" => Self::op_all(&actual, op_value),
|
||||
"$mod" => Self::op_mod(&actual, op_value),
|
||||
"$options" => continue, // handled by $regex
|
||||
_ => true, // unknown operator, skip
|
||||
};
|
||||
if !result {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn resolve_field(doc: &Document, field: &str) -> Option<Bson> {
|
||||
if field.contains('.') {
|
||||
get_nested_value(doc, field)
|
||||
} else {
|
||||
doc.get(field).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
fn bson_equals(a: &Bson, b: &Bson) -> bool {
|
||||
match (a, b) {
|
||||
(Bson::Int32(x), Bson::Int64(y)) => (*x as i64) == *y,
|
||||
(Bson::Int64(x), Bson::Int32(y)) => *x == (*y as i64),
|
||||
(Bson::Int32(x), Bson::Double(y)) => (*x as f64) == *y,
|
||||
(Bson::Double(x), Bson::Int32(y)) => *x == (*y as f64),
|
||||
(Bson::Int64(x), Bson::Double(y)) => (*x as f64) == *y,
|
||||
(Bson::Double(x), Bson::Int64(y)) => *x == (*y as f64),
|
||||
// For arrays, check if any element matches (implicit $elemMatch)
|
||||
(Bson::Array(arr), _) if !matches!(b, Bson::Array(_)) => {
|
||||
arr.iter().any(|elem| Self::bson_equals(elem, b))
|
||||
}
|
||||
_ => a == b,
|
||||
}
|
||||
}
|
||||
|
||||
fn bson_compare(a: &Bson, b: &Bson) -> Option<std::cmp::Ordering> {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
match (a, b) {
|
||||
// Numeric comparisons (cross-type)
|
||||
(Bson::Int32(x), Bson::Int32(y)) => Some(x.cmp(y)),
|
||||
(Bson::Int64(x), Bson::Int64(y)) => Some(x.cmp(y)),
|
||||
(Bson::Double(x), Bson::Double(y)) => x.partial_cmp(y),
|
||||
(Bson::Int32(x), Bson::Int64(y)) => Some((*x as i64).cmp(y)),
|
||||
(Bson::Int64(x), Bson::Int32(y)) => Some(x.cmp(&(*y as i64))),
|
||||
(Bson::Int32(x), Bson::Double(y)) => (*x as f64).partial_cmp(y),
|
||||
(Bson::Double(x), Bson::Int32(y)) => x.partial_cmp(&(*y as f64)),
|
||||
(Bson::Int64(x), Bson::Double(y)) => (*x as f64).partial_cmp(y),
|
||||
(Bson::Double(x), Bson::Int64(y)) => x.partial_cmp(&(*y as f64)),
|
||||
|
||||
// String comparisons
|
||||
(Bson::String(x), Bson::String(y)) => Some(x.cmp(y)),
|
||||
|
||||
// DateTime comparisons
|
||||
(Bson::DateTime(x), Bson::DateTime(y)) => Some(x.cmp(y)),
|
||||
|
||||
// Boolean comparisons
|
||||
(Bson::Boolean(x), Bson::Boolean(y)) => Some(x.cmp(y)),
|
||||
|
||||
// ObjectId comparisons
|
||||
(Bson::ObjectId(x), Bson::ObjectId(y)) => Some(x.cmp(y)),
|
||||
|
||||
// Null comparisons
|
||||
(Bson::Null, Bson::Null) => Some(Ordering::Equal),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Operator implementations ---
|
||||
|
||||
fn op_eq(actual: &Option<Bson>, expected: &Bson) -> bool {
|
||||
match actual {
|
||||
Some(val) => Self::bson_equals(val, expected),
|
||||
None => matches!(expected, Bson::Null),
|
||||
}
|
||||
}
|
||||
|
||||
fn op_ne(actual: &Option<Bson>, expected: &Bson) -> bool {
|
||||
!Self::op_eq(actual, expected)
|
||||
}
|
||||
|
||||
fn op_cmp(actual: &Option<Bson>, expected: &Bson, op: CmpOp) -> bool {
|
||||
let val = match actual {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
// For arrays, check if any element satisfies the comparison
|
||||
if let Bson::Array(arr) = val {
|
||||
return arr.iter().any(|elem| {
|
||||
if let Some(ord) = Self::bson_compare(elem, expected) {
|
||||
op.check(ord)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ord) = Self::bson_compare(val, expected) {
|
||||
op.check(ord)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn op_in(actual: &Option<Bson>, values: &Bson) -> bool {
|
||||
let arr = match values {
|
||||
Bson::Array(a) => a,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
match actual {
|
||||
Some(val) => {
|
||||
// For array values, check if any element is in the list
|
||||
if let Bson::Array(actual_arr) = val {
|
||||
actual_arr.iter().any(|elem| {
|
||||
arr.iter().any(|v| Self::bson_equals(elem, v))
|
||||
}) || arr.iter().any(|v| Self::bson_equals(val, v))
|
||||
} else {
|
||||
arr.iter().any(|v| Self::bson_equals(val, v))
|
||||
}
|
||||
}
|
||||
None => arr.iter().any(|v| matches!(v, Bson::Null)),
|
||||
}
|
||||
}
|
||||
|
||||
fn op_nin(actual: &Option<Bson>, values: &Bson) -> bool {
|
||||
!Self::op_in(actual, values)
|
||||
}
|
||||
|
||||
fn op_exists(actual: &Option<Bson>, expected: &Bson) -> bool {
|
||||
let should_exist = match expected {
|
||||
Bson::Boolean(b) => *b,
|
||||
Bson::Int32(n) => *n != 0,
|
||||
Bson::Int64(n) => *n != 0,
|
||||
_ => true,
|
||||
};
|
||||
actual.is_some() == should_exist
|
||||
}
|
||||
|
||||
fn op_type(actual: &Option<Bson>, expected: &Bson) -> bool {
|
||||
let val = match actual {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let type_num = match expected {
|
||||
Bson::Int32(n) => *n,
|
||||
Bson::String(s) => match s.as_str() {
|
||||
"double" => 1,
|
||||
"string" => 2,
|
||||
"object" => 3,
|
||||
"array" => 4,
|
||||
"binData" => 5,
|
||||
"objectId" => 7,
|
||||
"bool" => 8,
|
||||
"date" => 9,
|
||||
"null" => 10,
|
||||
"regex" => 11,
|
||||
"int" => 16,
|
||||
"long" => 18,
|
||||
"decimal" => 19,
|
||||
"number" => -1, // special: any numeric type
|
||||
_ => return false,
|
||||
},
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
if type_num == -1 {
|
||||
return matches!(val, Bson::Int32(_) | Bson::Int64(_) | Bson::Double(_));
|
||||
}
|
||||
|
||||
let actual_type = match val {
|
||||
Bson::Double(_) => 1,
|
||||
Bson::String(_) => 2,
|
||||
Bson::Document(_) => 3,
|
||||
Bson::Array(_) => 4,
|
||||
Bson::Binary(_) => 5,
|
||||
Bson::ObjectId(_) => 7,
|
||||
Bson::Boolean(_) => 8,
|
||||
Bson::DateTime(_) => 9,
|
||||
Bson::Null => 10,
|
||||
Bson::RegularExpression(_) => 11,
|
||||
Bson::Int32(_) => 16,
|
||||
Bson::Int64(_) => 18,
|
||||
Bson::Decimal128(_) => 19,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
actual_type == type_num
|
||||
}
|
||||
|
||||
fn op_regex(actual: &Option<Bson>, pattern: &Bson, options: Option<&Bson>) -> bool {
|
||||
let val = match actual {
|
||||
Some(Bson::String(s)) => s.as_str(),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let pattern_str = match pattern {
|
||||
Bson::String(s) => s.as_str(),
|
||||
Bson::RegularExpression(re) => re.pattern.as_str(),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let opts = match options {
|
||||
Some(Bson::String(s)) => s.as_str(),
|
||||
_ => match pattern {
|
||||
Bson::RegularExpression(re) => re.options.as_str(),
|
||||
_ => "",
|
||||
},
|
||||
};
|
||||
|
||||
let mut regex_pattern = String::new();
|
||||
if opts.contains('i') {
|
||||
regex_pattern.push_str("(?i)");
|
||||
}
|
||||
if opts.contains('m') {
|
||||
regex_pattern.push_str("(?m)");
|
||||
}
|
||||
if opts.contains('s') {
|
||||
regex_pattern.push_str("(?s)");
|
||||
}
|
||||
regex_pattern.push_str(pattern_str);
|
||||
|
||||
match Regex::new(®ex_pattern) {
|
||||
Ok(re) => re.is_match(val),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn op_not(doc: &Document, field: &str, condition: &Bson) -> bool {
|
||||
match condition {
|
||||
Bson::Document(cond_doc) => !Self::match_field_operators(doc, field, cond_doc),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn op_elem_match(actual: &Option<Bson>, condition: &Bson) -> bool {
|
||||
let arr = match actual {
|
||||
Some(Bson::Array(a)) => a,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let cond_doc = match condition {
|
||||
Bson::Document(d) => d,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
arr.iter().any(|elem| {
|
||||
if let Bson::Document(elem_doc) = elem {
|
||||
Self::matches_filter(elem_doc, cond_doc)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn op_size(actual: &Option<Bson>, expected: &Bson) -> bool {
|
||||
let arr = match actual {
|
||||
Some(Bson::Array(a)) => a,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let expected_size = match expected {
|
||||
Bson::Int32(n) => *n as usize,
|
||||
Bson::Int64(n) => *n as usize,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
arr.len() == expected_size
|
||||
}
|
||||
|
||||
fn op_all(actual: &Option<Bson>, expected: &Bson) -> bool {
|
||||
let arr = match actual {
|
||||
Some(Bson::Array(a)) => a,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let expected_arr = match expected {
|
||||
Bson::Array(a) => a,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
expected_arr.iter().all(|expected_val| {
|
||||
arr.iter().any(|elem| Self::bson_equals(elem, expected_val))
|
||||
})
|
||||
}
|
||||
|
||||
fn op_mod(actual: &Option<Bson>, expected: &Bson) -> bool {
|
||||
let val = match actual {
|
||||
Some(v) => match v {
|
||||
Bson::Int32(n) => *n as i64,
|
||||
Bson::Int64(n) => *n,
|
||||
Bson::Double(n) => *n as i64,
|
||||
_ => return false,
|
||||
},
|
||||
None => return false,
|
||||
};
|
||||
|
||||
let arr = match expected {
|
||||
Bson::Array(a) if a.len() == 2 => a,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let divisor = match &arr[0] {
|
||||
Bson::Int32(n) => *n as i64,
|
||||
Bson::Int64(n) => *n,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let remainder = match &arr[1] {
|
||||
Bson::Int32(n) => *n as i64,
|
||||
Bson::Int64(n) => *n,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
if divisor == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
val % divisor == remainder
|
||||
}
|
||||
|
||||
// --- Logical operators ---
|
||||
|
||||
fn match_logical_and(doc: &Document, conditions: &Bson) -> bool {
|
||||
match conditions {
|
||||
Bson::Array(arr) => arr.iter().all(|cond| {
|
||||
if let Bson::Document(cond_doc) = cond {
|
||||
Self::matches_filter(doc, cond_doc)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn match_logical_or(doc: &Document, conditions: &Bson) -> bool {
|
||||
match conditions {
|
||||
Bson::Array(arr) => arr.iter().any(|cond| {
|
||||
if let Bson::Document(cond_doc) = cond {
|
||||
Self::matches_filter(doc, cond_doc)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn match_logical_nor(doc: &Document, conditions: &Bson) -> bool {
|
||||
!Self::match_logical_or(doc, conditions)
|
||||
}
|
||||
|
||||
fn match_logical_not(doc: &Document, condition: &Bson) -> bool {
|
||||
match condition {
|
||||
Bson::Document(cond_doc) => !Self::matches_filter(doc, cond_doc),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum CmpOp {
|
||||
Gt,
|
||||
Gte,
|
||||
Lt,
|
||||
Lte,
|
||||
}
|
||||
|
||||
impl CmpOp {
|
||||
fn check(self, ord: std::cmp::Ordering) -> bool {
|
||||
use std::cmp::Ordering;
|
||||
match self {
|
||||
CmpOp::Gt => ord == Ordering::Greater,
|
||||
CmpOp::Gte => ord == Ordering::Greater || ord == Ordering::Equal,
|
||||
CmpOp::Lt => ord == Ordering::Less,
|
||||
CmpOp::Lte => ord == Ordering::Less || ord == Ordering::Equal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_equality() {
|
||||
let doc = bson::doc! { "name": "Alice", "age": 30 };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "name": "Alice" }));
|
||||
assert!(!QueryMatcher::matches(&doc, &bson::doc! { "name": "Bob" }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comparison_operators() {
|
||||
let doc = bson::doc! { "age": 30 };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "age": { "$gt": 25 } }));
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "age": { "$gte": 30 } }));
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "age": { "$lt": 35 } }));
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "age": { "$lte": 30 } }));
|
||||
assert!(!QueryMatcher::matches(&doc, &bson::doc! { "age": { "$gt": 30 } }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_operator() {
|
||||
let doc = bson::doc! { "status": "active" };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "status": { "$in": ["active", "pending"] } }));
|
||||
assert!(!QueryMatcher::matches(&doc, &bson::doc! { "status": { "$in": ["closed"] } }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exists_operator() {
|
||||
let doc = bson::doc! { "name": "Alice" };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "name": { "$exists": true } }));
|
||||
assert!(!QueryMatcher::matches(&doc, &bson::doc! { "age": { "$exists": true } }));
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "age": { "$exists": false } }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logical_or() {
|
||||
let doc = bson::doc! { "age": 30 };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! {
|
||||
"$or": [{ "age": 30 }, { "age": 40 }]
|
||||
}));
|
||||
assert!(!QueryMatcher::matches(&doc, &bson::doc! {
|
||||
"$or": [{ "age": 20 }, { "age": 40 }]
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logical_and() {
|
||||
let doc = bson::doc! { "age": 30, "name": "Alice" };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! {
|
||||
"$and": [{ "age": 30 }, { "name": "Alice" }]
|
||||
}));
|
||||
assert!(!QueryMatcher::matches(&doc, &bson::doc! {
|
||||
"$and": [{ "age": 30 }, { "name": "Bob" }]
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dot_notation() {
|
||||
let doc = bson::doc! { "address": { "city": "NYC" } };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "address.city": "NYC" }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ne_operator() {
|
||||
let doc = bson::doc! { "status": "active" };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "status": { "$ne": "closed" } }));
|
||||
assert!(!QueryMatcher::matches(&doc, &bson::doc! { "status": { "$ne": "active" } }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cross_type_numeric_equality() {
|
||||
let doc = bson::doc! { "count": 5_i32 };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! { "count": 5_i64 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_filter_matches_all() {
|
||||
let doc = bson::doc! { "x": 1 };
|
||||
assert!(QueryMatcher::matches(&doc, &bson::doc! {}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user