Skip to content

Commit e5a2609

Browse files
committed
Allow queries to specify a JSONPath for both values being selected and filters
1 parent 791e283 commit e5a2609

3 files changed

Lines changed: 127 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ serde_json = "1.0"
1111
uuid = { version = "1.8.0", features = ["v7", "serde"] }
1212
chrono = { version = "0.4.38", features = ["serde"] }
1313
jsonb = "0.5.5"
14+
jsonpath-rust = "0.7"
1415

1516
[dev-dependencies]
1617
tempfile = "3.10.1"

src/query.rs

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use serde_json::Value;
22
use std::cmp::Ordering;
3+
use std::str::FromStr;
4+
use jsonpath_rust::JsonPath;
35

46
#[derive(Debug, Clone)]
57
pub enum Expression {
68
FieldReference(String), // dot notation e.g. "a.b"
9+
JsonPath(String), // JSONPath e.g. "$.a.b"
710
Literal(Value),
811
Binary {
912
left: Box<Expression>,
@@ -17,16 +20,31 @@ pub enum Expression {
1720
},
1821
}
1922

20-
#[derive(Debug, Clone)]
23+
#[derive(Debug, Clone, PartialEq)]
2124
pub enum BinaryOperator {
2225
Eq, Neq, Lt, Lte, Gt, Gte
2326
}
2427

25-
#[derive(Debug, Clone)]
28+
#[derive(Debug, Clone, PartialEq)]
2629
pub enum LogicalOperator {
2730
And, Or
2831
}
2932

33+
#[derive(Debug)]
34+
pub enum LogicalPlan {
35+
Scan { collection: String },
36+
Filter { input: Box<LogicalPlan>, predicate: Expression },
37+
Project { input: Box<LogicalPlan>, projections: Vec<Expression> },
38+
Limit { input: Box<LogicalPlan>, limit: usize },
39+
Offset { input: Box<LogicalPlan>, offset: usize },
40+
}
41+
42+
#[derive(Debug)]
43+
pub enum Statement {
44+
Insert { collection: String, documents: Vec<Value> },
45+
Select(LogicalPlan),
46+
}
47+
3048
pub trait QueryOperator {
3149
fn next(&mut self) -> Option<(String, Value)>;
3250
}
@@ -98,6 +116,9 @@ impl QueryOperator for ProjectOperator {
98116
Expression::FieldReference(path) => {
99117
new_doc.insert(path.clone(), value);
100118
}
119+
Expression::JsonPath(path) => {
120+
new_doc.insert(path.clone(), value);
121+
}
101122
_ => {
102123
// Fallback/TODO: Handle computed columns alias
103124
}
@@ -165,6 +186,24 @@ fn evaluate_expression(expr: &Expression, doc: &Value) -> Value {
165186
Expression::FieldReference(path) => {
166187
get_path(doc, path).unwrap_or(Value::Null)
167188
}
189+
Expression::JsonPath(path) => {
190+
if let Ok(p) = JsonPath::from_str(path) {
191+
let inst = p.find(doc);
192+
if let Some(arr) = inst.as_array() {
193+
if arr.is_empty() {
194+
Value::Null
195+
} else if arr.len() == 1 {
196+
arr[0].clone()
197+
} else {
198+
inst.clone()
199+
}
200+
} else {
201+
inst
202+
}
203+
} else {
204+
Value::Null
205+
}
206+
}
168207
Expression::Literal(val) => val.clone(),
169208
Expression::Binary { left, op, right } => {
170209
let l_val = evaluate_expression(left, doc);
@@ -298,6 +337,28 @@ mod tests {
298337
assert!(filter.next().is_none());
299338
}
300339

340+
#[test]
341+
fn test_jsonpath() {
342+
let data = vec![
343+
("1".to_string(), json!({"a": {"b": 10}})),
344+
("2".to_string(), json!({"a": {"b": 20}})),
345+
];
346+
let source = Box::new(MockSource::new(data));
347+
348+
// Filter: $.a.b > 15
349+
let predicate = Expression::Binary {
350+
left: Box::new(Expression::JsonPath("$.a.b".to_string())),
351+
op: BinaryOperator::Gt,
352+
right: Box::new(Expression::Literal(json!(15))),
353+
};
354+
355+
let mut filter = FilterOperator::new(source, predicate);
356+
357+
let (id, _) = filter.next().unwrap();
358+
assert_eq!(id, "2");
359+
assert!(filter.next().is_none());
360+
}
361+
301362
#[test]
302363
fn test_project() {
303364
let data = vec![

0 commit comments

Comments
 (0)