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<()> {
243 let (name, id_field) = match f.field {
244 Field::Agent => ("agent", true),
245 Field::Space => ("space", true),
246 Field::Salience => ("salience", false),
247 Field::Confidence => ("confidence", false),
248 _ => return Ok(()),
249 };
250 let op_ok = if id_field {
251 matches!(f.op, Operator::Eq | Operator::Neq | Operator::In)
252 } else {
253 matches!(
254 f.op,
255 Operator::Eq
256 | Operator::Neq
257 | Operator::Lt
258 | Operator::Lte
259 | Operator::Gt
260 | Operator::Gte
261 | Operator::In
262 )
263 };
264 if !op_ok {
265 let allowed = if id_field {
266 "=, != or IN"
267 } else {
268 "=, !=, <, <=, >, >= or IN"
269 };
270 return Err(MenteError::Query(format!(
271 "{name} supports only {allowed}, found {:?}",
272 f.op
273 )));
274 }
275 let value_ok = |v: &Value| {
276 if id_field {
277 matches!(v, Value::Uuid(_))
278 } else {
279 matches!(v, Value::Number(_) | Value::Integer(_))
280 }
281 };
282 let ok = match &f.value {
283 Value::List(items) => f.op == Operator::In && items.iter().all(value_ok),
284 v => f.op != Operator::In && value_ok(v),
285 };
286 if ok {
287 Ok(())
288 } else {
289 let expected = if id_field { "a UUID" } else { "a number" };
290 Err(MenteError::Query(format!(
291 "{name} must be compared with {expected}, found {:?}",
292 f.value
293 )))
294 }
295}
296
297fn plan_relate(relate: &RelateStatement) -> MenteResult<QueryPlan> {
298 Ok(QueryPlan::EdgeInsert {
299 source: relate.source,
300 target: relate.target,
301 edge_type: relate.edge_type,
302 weight: relate.weight.unwrap_or(1.0),
303 })
304}
305
306fn plan_traverse(trav: &TraverseStatement) -> MenteResult<QueryPlan> {
307 Ok(QueryPlan::GraphTraversal {
308 start: trav.start,
309 depth: trav.depth,
310 edge_types: trav.edge_filter.clone().unwrap_or_default(),
311 })
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::lexer::tokenize;
318 use crate::parser::Parser;
319
320 fn plan_mql(input: &str) -> QueryPlan {
321 let tokens = tokenize(input).unwrap();
322 let stmt = Parser::parse(&tokens).unwrap();
323 plan(&stmt).unwrap()
324 }
325
326 #[test]
327 fn test_near_produces_vector_search() {
328 let qp = plan_mql("RECALL memories NEAR [0.1, 0.2, 0.3] LIMIT 5");
329 match qp {
330 QueryPlan::VectorSearch { query, k, .. } => {
331 assert_eq!(query, vec![0.1, 0.2, 0.3]);
332 assert_eq!(k, 5);
333 }
334 _ => panic!("expected VectorSearch, got {:?}", qp),
335 }
336 }
337
338 #[test]
339 fn test_similar_to_produces_vector_search() {
340 let qp = plan_mql(r#"RECALL memories WHERE content ~> "database migration" LIMIT 10"#);
341 match qp {
342 QueryPlan::VectorSearch { k, .. } => {
343 assert_eq!(k, 10);
344 }
345 _ => panic!("expected VectorSearch, got {:?}", qp),
346 }
347 }
348
349 #[test]
350 fn test_tag_filter_produces_tag_scan() {
351 let qp = plan_mql(r#"RECALL memories WHERE tag = "backend" LIMIT 5"#);
352 match qp {
353 QueryPlan::TagScan { tags, limit, .. } => {
354 assert_eq!(tags, vec!["backend".to_string()]);
355 assert_eq!(limit, Some(5));
356 }
357 _ => panic!("expected TagScan, got {:?}", qp),
358 }
359 }
360
361 #[test]
362 fn test_created_range_produces_temporal_scan_with_every_filter() {
363 let qp = plan_mql(
364 "RECALL memories WHERE created > 100 AND created >= 50 AND created < 900 AND type = episodic ORDER BY salience DESC LIMIT 10",
365 );
366 match qp {
367 QueryPlan::TemporalScan {
368 start,
369 end,
370 filters,
371 limit,
372 order_by,
373 } => {
374 assert_eq!((start, end), (100, 900), "tightest bounds");
375 assert_eq!(filters.len(), 4, "range filters kept for the re-check");
376 assert!(filters.iter().any(|f| f.field == Field::Type));
377 assert_eq!(limit, Some(10));
378 assert!(order_by.is_some_and(|ob| ob.descending));
379 }
380 other => panic!("expected TemporalScan, got {:?}", other),
381 }
382 match plan_mql("RECALL memories WHERE created > 5") {
383 QueryPlan::TemporalScan { limit, .. } => assert_eq!(limit, Some(DEFAULT_LIMIT)),
384 other => panic!("expected TemporalScan, got {:?}", other),
385 }
386 }
387
388 #[test]
389 fn test_accessed_range_is_not_a_creation_index_scan() {
390 match plan_mql("RECALL memories WHERE accessed > 100 LIMIT 5") {
391 QueryPlan::TagScan { filters, .. } => assert_eq!(filters.len(), 1),
392 other => panic!("expected TagScan, got {:?}", other),
393 }
394 }
395
396 #[test]
397 fn test_owner_and_score_filters_are_validated() {
398 let id = "550e8400-e29b-41d4-a716-446655440000";
399 for ok in [
400 format!("RECALL memories WHERE agent = {id}"),
401 format!("RECALL memories WHERE agent = \"{id}\""),
402 format!("RECALL memories WHERE space != {id}"),
403 format!("RECALL memories WHERE agent IN [{id}, {id}]"),
404 "RECALL memories WHERE salience > 0.5".to_string(),
405 "RECALL memories WHERE confidence <= 1".to_string(),
406 "RECALL memories WHERE salience IN [0.1, 0.2]".to_string(),
407 ] {
408 let tokens = tokenize(&ok).unwrap();
409 let stmt = Parser::parse(&tokens).unwrap();
410 assert!(plan(&stmt).is_ok(), "{ok}");
411 }
412 for bad in [
413 "RECALL memories WHERE agent = \"alice\"".to_string(),
414 format!("RECALL memories WHERE agent < {id}"),
415 "RECALL memories WHERE space = 1".to_string(),
416 "RECALL memories WHERE salience = \"high\"".to_string(),
417 "RECALL memories WHERE confidence CONTAINS 1".to_string(),
418 "RECALL memories WHERE confidence IN [1, \"x\"]".to_string(),
419 "RECALL memories WHERE NOT (type = episodic OR agent = \"bob\")".to_string(),
420 "CONSOLIDATE WHERE salience < \"low\"".to_string(),
421 ] {
422 let tokens = tokenize(&bad).unwrap();
423 let stmt = Parser::parse(&tokens).unwrap();
424 assert!(
425 matches!(plan(&stmt), Err(MenteError::Query(_))),
426 "{bad}: expected a query error"
427 );
428 }
429 }
430
431 #[test]
432 fn test_forget_produces_delete() {
433 let qp = plan_mql("FORGET 550e8400-e29b-41d4-a716-446655440000");
434 match qp {
435 QueryPlan::Delete { id } => {
436 assert_eq!(
437 id,
438 "550e8400-e29b-41d4-a716-446655440000"
439 .parse::<MemoryId>()
440 .unwrap()
441 );
442 }
443 _ => panic!("expected Delete, got {:?}", qp),
444 }
445 }
446
447 #[test]
448 fn test_traverse_produces_graph_traversal() {
449 let qp = plan_mql(
450 "TRAVERSE 550e8400-e29b-41d4-a716-446655440000 DEPTH 3 WHERE edge_type = caused",
451 );
452 match qp {
453 QueryPlan::GraphTraversal {
454 depth, edge_types, ..
455 } => {
456 assert_eq!(depth, 3);
457 assert_eq!(edge_types, vec![EdgeType::Caused]);
458 }
459 _ => panic!("expected GraphTraversal, got {:?}", qp),
460 }
461 }
462
463 #[test]
464 fn test_relate_produces_edge_insert() {
465 let qp = plan_mql(
466 "RELATE 550e8400-e29b-41d4-a716-446655440000 -> 660e8400-e29b-41d4-a716-446655440000 AS caused WITH weight = 0.8",
467 );
468 match qp {
469 QueryPlan::EdgeInsert {
470 edge_type, weight, ..
471 } => {
472 assert_eq!(edge_type, EdgeType::Caused);
473 assert!((weight - 0.8).abs() < f32::EPSILON);
474 }
475 _ => panic!("expected EdgeInsert, got {:?}", qp),
476 }
477 }
478}