1use crate::ast::*;
4use mentedb_core::edge::EdgeType;
5use mentedb_core::error::{MenteError, MenteResult};
6use mentedb_core::types::{MemoryId, Timestamp};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum QueryPlan {
12 VectorSearch {
13 query: Vec<f32>,
14 k: usize,
15 filters: Vec<Filter>,
16 #[serde(default)]
19 condition: Option<Condition>,
20 #[serde(default)]
22 order_by: Option<OrderBy>,
23 },
24 TagScan {
25 tags: Vec<String>,
26 filters: Vec<Filter>,
27 limit: Option<usize>,
28 #[serde(default)]
31 condition: Option<Condition>,
32 #[serde(default)]
34 order_by: Option<OrderBy>,
35 },
36 TemporalScan {
41 start: Timestamp,
42 end: Timestamp,
43 filters: Vec<Filter>,
44 #[serde(default)]
46 limit: Option<usize>,
47 #[serde(default)]
49 order_by: Option<OrderBy>,
50 },
51 GraphTraversal {
52 start: MemoryId,
53 depth: usize,
54 edge_types: Vec<EdgeType>,
55 },
56 PointLookup {
57 id: MemoryId,
58 },
59 EdgeInsert {
60 source: MemoryId,
61 target: MemoryId,
62 edge_type: EdgeType,
63 weight: f32,
64 },
65 Delete {
66 id: MemoryId,
67 },
68 Consolidate {
69 filters: Vec<Filter>,
70 },
71}
72
73const DEFAULT_LIMIT: usize = 20;
74
75pub fn plan(statement: &Statement) -> MenteResult<QueryPlan> {
77 match statement {
78 Statement::Recall(recall) => {
79 validate_filters(&recall.filters)?;
80 if let Some(cond) = &recall.condition {
81 validate_condition(cond)?;
82 }
83 }
84 Statement::Consolidate(cons) => validate_filters(&cons.filters)?,
85 _ => {}
86 }
87 match statement {
88 Statement::Recall(recall) => plan_recall(recall),
89 Statement::Relate(relate) => plan_relate(relate),
90 Statement::Forget(forget) => Ok(QueryPlan::Delete { id: forget.target }),
91 Statement::Consolidate(cons) => Ok(QueryPlan::Consolidate {
92 filters: cons.filters.clone(),
93 }),
94 Statement::Traverse(trav) => plan_traverse(trav),
95 }
96}
97
98fn plan_recall(recall: &RecallStatement) -> MenteResult<QueryPlan> {
99 let limit = recall.limit.unwrap_or(DEFAULT_LIMIT);
100
101 if let Some(ref vec) = recall.near {
103 return Ok(QueryPlan::VectorSearch {
104 query: vec.clone(),
105 k: limit,
106 filters: recall.filters.clone(),
107 condition: recall.condition.clone(),
108 order_by: recall.order_by.clone(),
109 });
110 }
111
112 if recall.condition.is_some() {
116 return Ok(QueryPlan::TagScan {
117 tags: Vec::new(),
118 filters: Vec::new(),
119 limit: Some(limit),
120 condition: recall.condition.clone(),
121 order_by: recall.order_by.clone(),
122 });
123 }
124
125 if let Some(sim_filter) = recall.filters.iter().find(|f| f.op == Operator::SimilarTo) {
127 if let Value::Text(ref _text) = sim_filter.value {
128 return Ok(QueryPlan::VectorSearch {
132 query: Vec::new(), k: limit,
134 filters: recall.filters.clone(),
135 condition: None,
136 order_by: recall.order_by.clone(),
137 });
138 }
139 return Err(MenteError::Query(
141 "~> operator requires a text value on the right-hand side".into(),
142 ));
143 }
144
145 let tag_filters: Vec<&Filter> = recall
147 .filters
148 .iter()
149 .filter(|f| f.field == Field::Tag)
150 .collect();
151 if !tag_filters.is_empty() && recall.filters.iter().all(|f| f.field == Field::Tag) {
152 let tags: Vec<String> = tag_filters
153 .iter()
154 .filter_map(|f| match &f.value {
155 Value::Text(t) => Some(t.clone()),
156 _ => None,
157 })
158 .collect();
159 return Ok(QueryPlan::TagScan {
160 tags,
161 filters: Vec::new(),
162 limit: Some(limit),
163 condition: None,
164 order_by: recall.order_by.clone(),
165 });
166 }
167
168 let mut start: Option<Timestamp> = None;
172 let mut end: Option<Timestamp> = None;
173 for f in &recall.filters {
174 if f.field != Field::Created {
175 continue;
176 }
177 let Some(ts) = timestamp_value(&f.value) else {
178 continue;
179 };
180 match f.op {
181 Operator::Gt | Operator::Gte => start = Some(start.map_or(ts, |s| s.max(ts))),
182 Operator::Lt | Operator::Lte => end = Some(end.map_or(ts, |e| e.min(ts))),
183 _ => {}
184 }
185 }
186 if start.is_some() || end.is_some() {
187 return Ok(QueryPlan::TemporalScan {
188 start: start.unwrap_or(0),
189 end: end.unwrap_or(Timestamp::MAX),
190 filters: recall.filters.clone(),
193 limit: Some(limit),
194 order_by: recall.order_by.clone(),
195 });
196 }
197
198 Ok(QueryPlan::TagScan {
200 tags: Vec::new(),
201 filters: recall.filters.clone(),
202 limit: Some(limit),
203 condition: None,
204 order_by: recall.order_by.clone(),
205 })
206}
207
208fn timestamp_value(v: &Value) -> Option<Timestamp> {
210 match v {
211 Value::Integer(i) => Some((*i).max(0) as Timestamp),
212 Value::Number(n) if n.is_finite() => Some(n.max(0.0) as Timestamp),
213 _ => None,
214 }
215}
216
217pub fn validate_filters(filters: &[Filter]) -> MenteResult<()> {
220 filters.iter().try_for_each(validate_filter)
221}
222
223pub fn validate_condition(c: &Condition) -> MenteResult<()> {
225 match c {
226 Condition::Leaf(f) => validate_filter(f),
227 Condition::And(children) | Condition::Or(children) => {
228 children.iter().try_for_each(validate_condition)
229 }
230 Condition::Not(inner) => validate_condition(inner),
231 }
232}
233
234pub fn validate_filter(f: &Filter) -> MenteResult<()> {
244 let (name, id_field) = match f.field {
245 Field::Agent => ("agent", true),
246 Field::Space => ("space", true),
247 Field::Salience => ("salience", false),
248 Field::Confidence => ("confidence", false),
249 _ => return Ok(()),
250 };
251 let op_ok = if id_field {
252 matches!(f.op, Operator::Eq | Operator::Neq | Operator::In)
253 } else {
254 matches!(
255 f.op,
256 Operator::Eq
257 | Operator::Neq
258 | Operator::Lt
259 | Operator::Lte
260 | Operator::Gt
261 | Operator::Gte
262 | Operator::In
263 )
264 };
265 if !op_ok {
266 let allowed = if id_field {
267 "=, != or IN"
268 } else {
269 "=, !=, <, <=, >, >= or IN"
270 };
271 return Err(MenteError::Query(format!(
272 "{name} supports only {allowed}, found {:?}",
273 f.op
274 )));
275 }
276 let value_ok = |v: &Value| {
277 if id_field {
278 matches!(v, Value::Uuid(_))
279 } else {
280 matches!(v, Value::Number(_) | Value::Integer(_))
281 }
282 };
283 let ok = match &f.value {
284 Value::List(items) => f.op == Operator::In && items.iter().all(value_ok),
285 v => f.op != Operator::In && value_ok(v),
286 };
287 if ok {
288 Ok(())
289 } else {
290 let expected = if id_field { "a UUID" } else { "a number" };
291 Err(MenteError::Query(format!(
292 "{name} must be compared with {expected}, found {:?}",
293 f.value
294 )))
295 }
296}
297
298fn plan_relate(relate: &RelateStatement) -> MenteResult<QueryPlan> {
299 Ok(QueryPlan::EdgeInsert {
300 source: relate.source,
301 target: relate.target,
302 edge_type: relate.edge_type,
303 weight: relate.weight.unwrap_or(1.0),
304 })
305}
306
307fn plan_traverse(trav: &TraverseStatement) -> MenteResult<QueryPlan> {
308 Ok(QueryPlan::GraphTraversal {
309 start: trav.start,
310 depth: trav.depth,
311 edge_types: trav.edge_filter.clone().unwrap_or_default(),
312 })
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318 use crate::lexer::tokenize;
319 use crate::parser::Parser;
320
321 fn plan_mql(input: &str) -> QueryPlan {
322 let tokens = tokenize(input).unwrap();
323 let stmt = Parser::parse(&tokens).unwrap();
324 plan(&stmt).unwrap()
325 }
326
327 #[test]
328 fn test_near_produces_vector_search() {
329 let qp = plan_mql("RECALL memories NEAR [0.1, 0.2, 0.3] LIMIT 5");
330 match qp {
331 QueryPlan::VectorSearch { query, k, .. } => {
332 assert_eq!(query, vec![0.1, 0.2, 0.3]);
333 assert_eq!(k, 5);
334 }
335 _ => panic!("expected VectorSearch, got {:?}", qp),
336 }
337 }
338
339 #[test]
340 fn test_similar_to_produces_vector_search() {
341 let qp = plan_mql(r#"RECALL memories WHERE content ~> "database migration" LIMIT 10"#);
342 match qp {
343 QueryPlan::VectorSearch { k, .. } => {
344 assert_eq!(k, 10);
345 }
346 _ => panic!("expected VectorSearch, got {:?}", qp),
347 }
348 }
349
350 #[test]
351 fn test_tag_filter_produces_tag_scan() {
352 let qp = plan_mql(r#"RECALL memories WHERE tag = "backend" LIMIT 5"#);
353 match qp {
354 QueryPlan::TagScan { tags, limit, .. } => {
355 assert_eq!(tags, vec!["backend".to_string()]);
356 assert_eq!(limit, Some(5));
357 }
358 _ => panic!("expected TagScan, got {:?}", qp),
359 }
360 }
361
362 #[test]
363 fn test_created_range_produces_temporal_scan_with_every_filter() {
364 let qp = plan_mql(
365 "RECALL memories WHERE created > 100 AND created >= 50 AND created < 900 AND type = episodic ORDER BY salience DESC LIMIT 10",
366 );
367 match qp {
368 QueryPlan::TemporalScan {
369 start,
370 end,
371 filters,
372 limit,
373 order_by,
374 } => {
375 assert_eq!((start, end), (100, 900), "tightest bounds");
376 assert_eq!(filters.len(), 4, "range filters kept for the re-check");
377 assert!(filters.iter().any(|f| f.field == Field::Type));
378 assert_eq!(limit, Some(10));
379 assert!(order_by.is_some_and(|ob| ob.descending));
380 }
381 other => panic!("expected TemporalScan, got {:?}", other),
382 }
383 match plan_mql("RECALL memories WHERE created > 5") {
384 QueryPlan::TemporalScan { limit, .. } => assert_eq!(limit, Some(DEFAULT_LIMIT)),
385 other => panic!("expected TemporalScan, got {:?}", other),
386 }
387 }
388
389 #[test]
390 fn test_accessed_range_is_not_a_creation_index_scan() {
391 match plan_mql("RECALL memories WHERE accessed > 100 LIMIT 5") {
392 QueryPlan::TagScan { filters, .. } => assert_eq!(filters.len(), 1),
393 other => panic!("expected TagScan, got {:?}", other),
394 }
395 }
396
397 #[test]
398 fn test_owner_and_score_filters_are_validated() {
399 let id = "550e8400-e29b-41d4-a716-446655440000";
400 for ok in [
401 format!("RECALL memories WHERE agent = {id}"),
402 format!("RECALL memories WHERE agent = \"{id}\""),
403 format!("RECALL memories WHERE space != {id}"),
404 format!("RECALL memories WHERE agent IN [{id}, {id}]"),
405 "RECALL memories WHERE salience > 0.5".to_string(),
406 "RECALL memories WHERE confidence <= 1".to_string(),
407 "RECALL memories WHERE salience IN [0.1, 0.2]".to_string(),
408 ] {
409 let tokens = tokenize(&ok).unwrap();
410 let stmt = Parser::parse(&tokens).unwrap();
411 assert!(plan(&stmt).is_ok(), "{ok}");
412 }
413 for bad in [
414 "RECALL memories WHERE agent = \"alice\"".to_string(),
415 format!("RECALL memories WHERE agent < {id}"),
416 "RECALL memories WHERE space = 1".to_string(),
417 "RECALL memories WHERE salience = \"high\"".to_string(),
418 "RECALL memories WHERE confidence CONTAINS 1".to_string(),
419 "RECALL memories WHERE confidence IN [1, \"x\"]".to_string(),
420 "RECALL memories WHERE NOT (type = episodic OR agent = \"bob\")".to_string(),
421 "CONSOLIDATE WHERE salience < \"low\"".to_string(),
422 ] {
423 let tokens = tokenize(&bad).unwrap();
424 let stmt = Parser::parse(&tokens).unwrap();
425 assert!(
426 matches!(plan(&stmt), Err(MenteError::Query(_))),
427 "{bad}: expected a query error"
428 );
429 }
430 }
431
432 #[test]
433 fn test_forget_produces_delete() {
434 let qp = plan_mql("FORGET 550e8400-e29b-41d4-a716-446655440000");
435 match qp {
436 QueryPlan::Delete { id } => {
437 assert_eq!(
438 id,
439 "550e8400-e29b-41d4-a716-446655440000"
440 .parse::<MemoryId>()
441 .unwrap()
442 );
443 }
444 _ => panic!("expected Delete, got {:?}", qp),
445 }
446 }
447
448 #[test]
449 fn test_traverse_produces_graph_traversal() {
450 let qp = plan_mql(
451 "TRAVERSE 550e8400-e29b-41d4-a716-446655440000 DEPTH 3 WHERE edge_type = caused",
452 );
453 match qp {
454 QueryPlan::GraphTraversal {
455 depth, edge_types, ..
456 } => {
457 assert_eq!(depth, 3);
458 assert_eq!(edge_types, vec![EdgeType::Caused]);
459 }
460 _ => panic!("expected GraphTraversal, got {:?}", qp),
461 }
462 }
463
464 #[test]
465 fn test_relate_produces_edge_insert() {
466 let qp = plan_mql(
467 "RELATE 550e8400-e29b-41d4-a716-446655440000 -> 660e8400-e29b-41d4-a716-446655440000 AS caused WITH weight = 0.8",
468 );
469 match qp {
470 QueryPlan::EdgeInsert {
471 edge_type, weight, ..
472 } => {
473 assert_eq!(edge_type, EdgeType::Caused);
474 assert!((weight - 0.8).abs() < f32::EPSILON);
475 }
476 _ => panic!("expected EdgeInsert, got {:?}", qp),
477 }
478 }
479}