1use pest::Parser;
4use pest_derive::Parser;
5
6use hirn_core::types::Layer;
7
8use super::ast::*;
9
10#[derive(Parser)]
13#[grammar = "parser/hirnql.pest"]
14struct HirnQlParser;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ParseError {
21 pub message: String,
22 pub line: usize,
23 pub column: usize,
24}
25
26impl std::fmt::Display for ParseError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(
29 f,
30 "parse error at {}:{}: {}",
31 self.line, self.column, self.message
32 )
33 }
34}
35
36impl std::error::Error for ParseError {}
37
38impl ParseError {
39 fn simple(message: impl Into<String>) -> Self {
41 Self {
42 message: message.into(),
43 line: 1,
44 column: 1,
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct QueryLimits {
52 pub max_query_length: usize,
54 pub max_expand_depth: usize,
56 pub max_limit: usize,
58 pub max_context_budget: usize,
62 pub max_iterative_hops: usize,
67}
68
69impl Default for QueryLimits {
70 fn default() -> Self {
71 Self {
72 max_query_length: 1_048_576, max_expand_depth: 10,
74 max_limit: 10_000,
75 max_context_budget: 1_000_000,
76 max_iterative_hops: 5,
77 }
78 }
79}
80
81pub fn parse(input: &str) -> Result<Statement, ParseError> {
83 parse_with_limits(input, &QueryLimits::default())
84}
85
86pub fn parse_with_limits(input: &str, limits: &QueryLimits) -> Result<Statement, ParseError> {
88 if input.len() > limits.max_query_length {
89 return Err(ParseError::simple(format!(
90 "query too large: {} bytes exceeds maximum of {} bytes",
91 input.len(),
92 limits.max_query_length
93 )));
94 }
95
96 let pairs = HirnQlParser::parse(Rule::statement, input).map_err(|e| {
97 let (line, col) = match e.line_col {
98 pest::error::LineColLocation::Pos((l, c)) => (l, c),
99 pest::error::LineColLocation::Span((l, c), _) => (l, c),
100 };
101
102 let msg = format_pest_error(&e, input);
103 ParseError {
104 message: msg,
105 line,
106 column: col,
107 }
108 })?;
109
110 let statement_pair = pairs
111 .into_iter()
112 .next()
113 .ok_or_else(|| ParseError::simple("empty input"))?;
114
115 let stmt = build_statement(statement_pair)?;
116 validate_limits(&stmt, limits)?;
117 Ok(stmt)
118}
119
120fn validate_limits(stmt: &Statement, limits: &QueryLimits) -> Result<(), ParseError> {
122 match stmt {
123 Statement::Recall(r) => {
124 if let Some(limit) = r.limit {
125 check_limit(limit, limits.max_limit)?;
126 }
127 if let Some(budget) = r.budget {
128 check_budget(budget, limits.max_context_budget)?;
129 }
130 if let Some(ref expand) = r.expand {
131 check_depth(expand.depth, limits.max_expand_depth)?;
132 }
133 }
134 Statement::Think(t) => {
135 if let Some(limit) = t.limit {
136 check_limit(limit, limits.max_limit)?;
137 }
138 if let Some(budget) = t.budget {
139 check_budget(budget, limits.max_context_budget)?;
140 }
141 if let Some(hops) = t.max_hops {
142 check_max_hops(hops, limits.max_iterative_hops)?;
143 }
144 if let Some(ref expand) = t.expand {
145 check_depth(expand.depth, limits.max_expand_depth)?;
146 }
147 }
148 Statement::RecallEvents(r) => {
149 if let Some(limit) = r.limit {
150 check_limit(limit, limits.max_limit)?;
151 }
152 }
153 Statement::Traverse(t) => {
154 check_depth(t.depth, limits.max_expand_depth)?;
155 if let Some(limit) = t.limit {
156 check_limit(limit, limits.max_limit)?;
157 }
158 }
159 Statement::Explain(e) => validate_limits(&e.inner, limits)?,
160 _ => {}
161 }
162 Ok(())
163}
164
165fn check_limit(value: usize, max: usize) -> Result<(), ParseError> {
166 if value > max {
167 return Err(ParseError::simple(format!(
168 "LIMIT {value} exceeds maximum allowed value of {max}"
169 )));
170 }
171 Ok(())
172}
173
174fn check_depth(value: usize, max: usize) -> Result<(), ParseError> {
175 if value > max {
176 return Err(ParseError::simple(format!(
177 "DEPTH {value} exceeds maximum allowed value of {max}"
178 )));
179 }
180 Ok(())
181}
182
183fn check_budget(value: usize, max: usize) -> Result<(), ParseError> {
184 if value > max {
185 return Err(ParseError::simple(format!(
186 "BUDGET {value} exceeds maximum allowed value of {max}"
187 )));
188 }
189 Ok(())
190}
191
192fn check_max_hops(value: usize, max: usize) -> Result<(), ParseError> {
193 if value > max {
194 return Err(ParseError::simple(format!(
195 "MAX_HOPS {value} exceeds maximum allowed value of {max}"
196 )));
197 }
198 Ok(())
199}
200
201fn format_pest_error(e: &pest::error::Error<Rule>, input: &str) -> String {
203 let base = e.variant.message().to_string();
205
206 let trimmed = input.trim();
207 if let Some(first_word) = trimmed.split_whitespace().next() {
208 let upper = first_word.to_uppercase();
209 let known = [
210 "RECALL",
211 "THINK",
212 "REMEMBER",
213 "FORGET",
214 "CORRECT",
215 "SUPERSEDE",
216 "RETRACT",
217 "CONNECT",
218 "INSPECT",
219 "HISTORY",
220 "TRACE",
221 "CONSOLIDATE",
222 "WATCH",
223 "TRAVERSE",
224 "EXPLAIN",
225 "CREATE",
226 "DROP",
227 "GRANT",
228 "REVOKE",
229 "SHOW",
230 ];
231 if !known.contains(&upper.as_str()) {
232 return format!("unknown verb '{first_word}', did you mean 'RECALL'?");
233 }
234 }
235
236 base
237}
238
239fn unsupported_embedded_statement(rule: Rule) -> Result<Statement, ParseError> {
242 Err(ParseError::simple(match rule {
243 Rule::remember_stmt => {
244 "REMEMBER is not supported via embedded HirnQL anymore; use the direct memory view APIs instead"
245 }
246 Rule::forget_stmt => {
247 "FORGET is not supported via embedded HirnQL anymore; use the direct memory view APIs instead"
248 }
249 Rule::connect_stmt => {
250 "CONNECT is not supported via embedded HirnQL anymore; use the graph view APIs instead"
251 }
252 Rule::consolidate_stmt => {
253 "CONSOLIDATE is not supported via HirnQL anymore; use db.admin().consolidate().execute() instead"
254 }
255 Rule::watch_stmt => {
256 "WATCH is not supported via embedded HirnQL anymore; use the event or daemon APIs instead"
257 }
258 _ => "statement is not supported via embedded HirnQL anymore",
259 }))
260}
261
262fn build_statement(pair: pest::iterators::Pair<'_, Rule>) -> Result<Statement, ParseError> {
263 let inner = pair
264 .into_inner()
265 .next()
266 .ok_or_else(|| ParseError::simple("empty statement"))?;
267
268 match inner.as_rule() {
269 Rule::recall_events_stmt => Ok(Statement::RecallEvents(build_recall_events(inner)?)),
270 Rule::recall_stmt => build_recall(inner).map(|stmt| Statement::Recall(Box::new(stmt))),
271 Rule::think_stmt => build_think(inner).map(|stmt| Statement::Think(Box::new(stmt))),
272 Rule::remember_stmt => unsupported_embedded_statement(Rule::remember_stmt),
273 Rule::forget_stmt => unsupported_embedded_statement(Rule::forget_stmt),
274 Rule::correct_stmt => build_correct(inner).map(Statement::Correct),
275 Rule::supersede_stmt => build_supersede(inner).map(Statement::Supersede),
276 Rule::merge_memory_stmt => build_merge_memory(inner).map(Statement::MergeMemory),
277 Rule::retract_stmt => build_retract(inner).map(Statement::Retract),
278 Rule::connect_stmt => unsupported_embedded_statement(Rule::connect_stmt),
279 Rule::inspect_stmt => Ok(Statement::Inspect(build_inspect(inner)?)),
280 Rule::history_stmt => Ok(Statement::History(build_history(inner)?)),
281 Rule::trace_stmt => Ok(Statement::Trace(build_trace(inner)?)),
282 Rule::consolidate_stmt => unsupported_embedded_statement(Rule::consolidate_stmt),
283 Rule::watch_stmt => unsupported_embedded_statement(Rule::watch_stmt),
284 Rule::traverse_stmt => build_traverse(inner).map(Statement::Traverse),
285 Rule::explain_stmt => build_explain(inner),
286 Rule::explain_causes_stmt => build_explain_causes(inner).map(Statement::ExplainCauses),
287 Rule::what_if_stmt => build_what_if(inner).map(Statement::WhatIf),
288 Rule::counterfactual_stmt => build_counterfactual(inner).map(Statement::Counterfactual),
289 Rule::create_realm_stmt => Ok(Statement::CreateRealm(build_create_realm(inner)?)),
290 Rule::drop_realm_stmt => Ok(Statement::DropRealm(build_drop_realm(inner)?)),
291 Rule::grant_stmt => build_grant(inner).map(Statement::Grant),
292 Rule::revoke_stmt => build_revoke(inner).map(Statement::Revoke),
293 Rule::show_policies_stmt => Ok(Statement::ShowPolicies(build_show_policies(inner)?)),
294 Rule::explain_policy_stmt => build_explain_policy(inner).map(Statement::ExplainPolicy),
295 Rule::show_cluster_stmt => Ok(Statement::ShowCluster),
296 Rule::set_tier_policy_stmt => Ok(Statement::SetTierPolicy(build_set_tier_policy(inner)?)),
297 _ => Err(ParseError::simple(format!(
298 "unexpected rule: {:?}",
299 inner.as_rule()
300 ))),
301 }
302}
303
304fn build_recall(pair: pest::iterators::Pair<'_, Rule>) -> Result<RecallStmt, ParseError> {
305 let mut stmt = RecallStmt {
306 layers: vec![],
307 about: String::new(),
308 involving: None,
309 temporal: None,
310 as_of: None,
311 expand: None,
312 follow_causes: None,
313 where_clauses: vec![],
314 subquery_filters: vec![],
315 modality: None,
316 resource_roles: None,
317 hydration_modes: None,
318 artifact_kinds: None,
319 depth_mode: None,
320 with_prospective: None,
321 with_mcfa: None,
322 with_conflicts: false,
323 provenance_depth: None,
324 topic: None,
325 group_by: None,
326 projection: None,
327 output_format: None,
328 result_format: None,
329 budget: None,
330 namespace: None,
331 from_realms: None,
332 consistency: None,
333 limit: None,
334 hybrid: false,
335 };
336
337 for inner in pair.into_inner() {
338 match inner.as_rule() {
339 Rule::layer_filter => stmt.layers = build_layer_filter(inner),
340 Rule::about_clause => stmt.about = extract_about(inner)?,
341 Rule::involving_clause => stmt.involving = Some(extract_string_list(inner)?),
342 Rule::temporal_clause => stmt.temporal = Some(build_temporal(inner)?),
343 Rule::as_of_clause => stmt.as_of = Some(build_as_of(inner)?),
344 Rule::expand_clause => stmt.expand = Some(build_expand(inner)?),
345 Rule::follow_causes_clause => stmt.follow_causes = Some(extract_follow_causes(inner)?),
346 Rule::where_clause => {
347 let child = inner
349 .into_inner()
350 .next()
351 .ok_or_else(|| ParseError::simple("empty WHERE clause"))?;
352 match child.as_rule() {
353 Rule::in_subquery_condition => {
354 stmt.subquery_filters.push(build_in_subquery(child)?);
355 }
356 Rule::condition => {
357 stmt.where_clauses.push(build_condition(child)?);
358 }
359 _ => {}
360 }
361 }
362 Rule::group_by_clause => stmt.group_by = Some(build_group_by(inner)),
363 Rule::select_clause => stmt.projection = Some(build_field_list(inner)),
364 Rule::as_clause => stmt.output_format = Some(build_output_format(inner)),
365 Rule::format_clause => stmt.result_format = Some(build_format_clause(inner)),
366 Rule::budget_clause => stmt.budget = Some(extract_budget(inner)?),
367 Rule::namespace_clause => stmt.namespace = Some(extract_namespace(inner)),
368 Rule::from_realm_clause => stmt.from_realms = Some(extract_realm_list(inner)),
369 Rule::consistency_clause => stmt.consistency = Some(build_consistency(inner)),
370 Rule::limit_clause => stmt.limit = Some(extract_limit(inner)?),
371 Rule::modality_clause => stmt.modality = Some(build_modality_list(inner)?),
372 Rule::resource_role_clause => {
373 stmt.resource_roles = Some(build_evidence_role_list(inner)?);
374 }
375 Rule::hydration_clause => {
376 stmt.hydration_modes = Some(build_hydration_mode_list(inner)?);
377 }
378 Rule::artifact_clause => {
379 stmt.artifact_kinds = Some(build_artifact_kind_list(inner)?);
380 }
381 Rule::depth_clause => stmt.depth_mode = Some(build_depth_mode(inner)?),
382 Rule::topic_clause => stmt.topic = Some(extract_string_from_clause(inner)?),
383 Rule::with_prospective_clause => stmt.with_prospective = Some(build_on_off(inner)?),
384 Rule::with_mcfa_clause => stmt.with_mcfa = Some(build_on_off(inner)?),
385 Rule::with_conflicts_clause => stmt.with_conflicts = true,
386 Rule::with_provenance_clause => {
387 stmt.provenance_depth = Some(extract_integer_from_clause(inner)?);
388 }
389 Rule::hybrid_clause => stmt.hybrid = true,
390 _ => {}
391 }
392 }
393
394 if stmt.about.is_empty() {
395 return Err(ParseError::simple("RECALL requires ABOUT clause"));
396 }
397
398 Ok(stmt)
399}
400
401fn build_recall_events(
402 pair: pest::iterators::Pair<'_, Rule>,
403) -> Result<RecallEventsStmt, ParseError> {
404 let mut stmt = RecallEventsStmt {
405 entity_filter: None,
406 where_clauses: vec![],
407 temporal: None,
408 namespace: None,
409 limit: None,
410 };
411
412 for inner in pair.into_inner() {
413 match inner.as_rule() {
414 Rule::events_for_clause => {
415 stmt.entity_filter = Some(extract_string_from_clause(inner)?);
416 }
417 Rule::where_clause => stmt.where_clauses.push(build_where(inner)?),
418 Rule::temporal_clause => stmt.temporal = Some(build_temporal(inner)?),
419 Rule::namespace_clause => stmt.namespace = Some(extract_namespace(inner)),
420 Rule::limit_clause => stmt.limit = Some(extract_limit(inner)?),
421 _ => {}
422 }
423 }
424
425 Ok(stmt)
426}
427
428fn build_think(pair: pest::iterators::Pair<'_, Rule>) -> Result<ThinkStmt, ParseError> {
429 let mut stmt = ThinkStmt {
430 about: String::new(),
431 involving: None,
432 temporal: None,
433 expand: None,
434 follow_causes: None,
435 where_clauses: vec![],
436 output_format: None,
437 budget: None,
438 namespace: None,
439 consistency: None,
440 limit: None,
441 hybrid: false,
442 mode: RetrievalMode::Local,
443 depth_mode: None,
444 with_prospective: None,
445 with_mcfa: None,
446 provenance_depth: None,
447 max_hops: None,
448 community_depth: None,
449 };
450
451 for inner in pair.into_inner() {
452 match inner.as_rule() {
453 Rule::about_clause => stmt.about = extract_about(inner)?,
454 Rule::involving_clause => stmt.involving = Some(extract_string_list(inner)?),
455 Rule::temporal_clause => stmt.temporal = Some(build_temporal(inner)?),
456 Rule::expand_clause => stmt.expand = Some(build_expand(inner)?),
457 Rule::follow_causes_clause => stmt.follow_causes = Some(extract_follow_causes(inner)?),
458 Rule::where_clause => stmt.where_clauses.push(build_where(inner)?),
459 Rule::as_clause => stmt.output_format = Some(build_output_format(inner)),
460 Rule::budget_clause => stmt.budget = Some(extract_budget(inner)?),
461 Rule::namespace_clause => stmt.namespace = Some(extract_namespace(inner)),
462 Rule::consistency_clause => stmt.consistency = Some(build_consistency(inner)),
463 Rule::limit_clause => stmt.limit = Some(extract_limit(inner)?),
464 Rule::global_clause => stmt.mode = RetrievalMode::Global,
465 Rule::mode_clause => {
466 let (mode, max_hops) = build_retrieval_mode_with_hops(inner)?;
467 stmt.mode = mode;
468 if max_hops.is_some() {
469 stmt.max_hops = max_hops;
470 }
471 }
472 Rule::hybrid_clause => stmt.hybrid = true,
473 Rule::depth_clause => stmt.depth_mode = Some(build_depth_mode(inner)?),
474 Rule::with_prospective_clause => stmt.with_prospective = Some(build_on_off(inner)?),
475 Rule::with_mcfa_clause => stmt.with_mcfa = Some(build_on_off(inner)?),
476 Rule::with_provenance_clause => {
477 stmt.provenance_depth = Some(extract_integer_from_clause(inner)?);
478 }
479 Rule::community_depth_clause => {
480 stmt.community_depth = Some(extract_integer_from_clause(inner)?);
481 }
482 _ => {}
483 }
484 }
485
486 Ok(stmt)
487}
488
489fn build_correct(pair: pest::iterators::Pair<'_, Rule>) -> Result<CorrectStmt, ParseError> {
490 let mut target = None;
491 let mut updates = Vec::new();
492 let mut reason = None;
493 let mut observed_at = None;
494 let mut caused_by = None;
495 let mut namespace = None;
496
497 for inner in pair.into_inner() {
498 match inner.as_rule() {
499 Rule::semantic_target_ref if target.is_none() => {
500 target = Some(build_semantic_target_ref(inner)?);
501 }
502 Rule::set_assignment_list => updates = build_set_assignment_list(inner)?,
503 Rule::reason_clause => reason = Some(extract_string_from_clause(inner)?),
504 Rule::observed_at_clause => observed_at = Some(extract_string_from_clause(inner)?),
505 Rule::caused_by_clause => caused_by = Some(extract_string_from_clause(inner)?),
506 Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
507 _ => {}
508 }
509 }
510
511 Ok(CorrectStmt {
512 target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
513 updates,
514 reason,
515 observed_at,
516 caused_by,
517 namespace,
518 })
519}
520
521fn build_supersede(pair: pest::iterators::Pair<'_, Rule>) -> Result<SupersedeStmt, ParseError> {
522 let mut target = None;
523 let mut updates = Vec::new();
524 let mut reason = None;
525 let mut observed_at = None;
526 let mut caused_by = None;
527 let mut namespace = None;
528
529 for inner in pair.into_inner() {
530 match inner.as_rule() {
531 Rule::semantic_target_ref if target.is_none() => {
532 target = Some(build_semantic_target_ref(inner)?);
533 }
534 Rule::set_assignment_list => updates = build_set_assignment_list(inner)?,
535 Rule::reason_clause => reason = Some(extract_string_from_clause(inner)?),
536 Rule::observed_at_clause => observed_at = Some(extract_string_from_clause(inner)?),
537 Rule::caused_by_clause => caused_by = Some(extract_string_from_clause(inner)?),
538 Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
539 _ => {}
540 }
541 }
542
543 Ok(SupersedeStmt {
544 target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
545 updates,
546 reason,
547 observed_at,
548 caused_by,
549 namespace,
550 })
551}
552
553fn build_merge_memory(
554 pair: pest::iterators::Pair<'_, Rule>,
555) -> Result<MergeMemoryStmt, ParseError> {
556 let mut sources = Vec::new();
557 let mut target = None;
558 let mut updates = Vec::new();
559 let mut reason = None;
560 let mut observed_at = None;
561 let mut caused_by = None;
562 let mut namespace = None;
563
564 for inner in pair.into_inner() {
565 match inner.as_rule() {
566 Rule::semantic_target_list if sources.is_empty() => {
567 sources = build_semantic_target_list(inner)?;
568 }
569 Rule::semantic_target_ref if target.is_none() => {
570 target = Some(build_semantic_target_ref(inner)?);
571 }
572 Rule::merge_set_clause => {
573 for child in inner.into_inner() {
574 if child.as_rule() == Rule::set_assignment_list {
575 updates = build_set_assignment_list(child)?;
576 }
577 }
578 }
579 Rule::reason_clause => reason = Some(extract_string_from_clause(inner)?),
580 Rule::observed_at_clause => observed_at = Some(extract_string_from_clause(inner)?),
581 Rule::caused_by_clause => caused_by = Some(extract_string_from_clause(inner)?),
582 Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
583 _ => {}
584 }
585 }
586
587 Ok(MergeMemoryStmt {
588 sources,
589 target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
590 updates,
591 reason,
592 observed_at,
593 caused_by,
594 namespace,
595 })
596}
597
598fn build_retract(pair: pest::iterators::Pair<'_, Rule>) -> Result<RetractStmt, ParseError> {
599 let mut target = None;
600 let mut reason = None;
601 let mut observed_at = None;
602 let mut caused_by = None;
603 let mut namespace = None;
604
605 for inner in pair.into_inner() {
606 match inner.as_rule() {
607 Rule::semantic_target_ref if target.is_none() => {
608 target = Some(build_semantic_target_ref(inner)?);
609 }
610 Rule::reason_clause => reason = Some(extract_string_from_clause(inner)?),
611 Rule::observed_at_clause => observed_at = Some(extract_string_from_clause(inner)?),
612 Rule::caused_by_clause => caused_by = Some(extract_string_from_clause(inner)?),
613 Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
614 _ => {}
615 }
616 }
617
618 Ok(RetractStmt {
619 target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
620 reason,
621 observed_at,
622 caused_by,
623 namespace,
624 })
625}
626
627fn build_inspect(pair: pest::iterators::Pair<'_, Rule>) -> Result<InspectStmt, ParseError> {
628 let target = pair
629 .into_inner()
630 .find(|p| p.as_rule() == Rule::semantic_target_ref)
631 .map(build_semantic_target_ref)
632 .transpose()?
633 .unwrap_or_else(|| SemanticTargetRef::Memory(String::new()));
634 Ok(InspectStmt { target })
635}
636
637fn build_history(pair: pest::iterators::Pair<'_, Rule>) -> Result<HistoryStmt, ParseError> {
638 let mut target = None;
639 let mut namespace = None;
640
641 for inner in pair.into_inner() {
642 match inner.as_rule() {
643 Rule::semantic_target_ref if target.is_none() => {
644 target = Some(build_semantic_target_ref(inner)?);
645 }
646 Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
647 _ => {}
648 }
649 }
650
651 Ok(HistoryStmt {
652 target: target.unwrap_or_else(|| SemanticTargetRef::Memory(String::new())),
653 namespace,
654 })
655}
656
657fn build_trace(pair: pest::iterators::Pair<'_, Rule>) -> Result<TraceStmt, ParseError> {
658 let target = pair
659 .into_inner()
660 .find(|p| p.as_rule() == Rule::semantic_target_ref)
661 .map(build_semantic_target_ref)
662 .transpose()?
663 .unwrap_or_else(|| SemanticTargetRef::Memory(String::new()));
664 Ok(TraceStmt { target })
665}
666
667fn build_semantic_target_list(
668 pair: pest::iterators::Pair<'_, Rule>,
669) -> Result<Vec<SemanticTargetRef>, ParseError> {
670 pair.into_inner()
671 .filter(|child| child.as_rule() == Rule::semantic_target_ref)
672 .map(build_semantic_target_ref)
673 .collect()
674}
675
676fn build_semantic_target_ref(
677 pair: pest::iterators::Pair<'_, Rule>,
678) -> Result<SemanticTargetRef, ParseError> {
679 let Some(inner) = pair.into_inner().next() else {
680 return Ok(SemanticTargetRef::Memory(String::new()));
681 };
682
683 match inner.as_rule() {
684 Rule::logical_target_ref => {
685 let value = inner
686 .into_inner()
687 .find(|child| child.as_rule() == Rule::string_literal)
688 .map(extract_string_value)
689 .transpose()?
690 .unwrap_or_default();
691 Ok(SemanticTargetRef::Logical(value))
692 }
693 Rule::revision_target_ref => {
694 let value = inner
695 .into_inner()
696 .find(|child| child.as_rule() == Rule::string_literal)
697 .map(extract_string_value)
698 .transpose()?
699 .unwrap_or_default();
700 Ok(SemanticTargetRef::Revision(value))
701 }
702 Rule::string_literal => Ok(SemanticTargetRef::Memory(extract_string_value(inner)?)),
703 _ => Err(ParseError::simple(format!(
704 "unexpected semantic target rule: {:?}",
705 inner.as_rule()
706 ))),
707 }
708}
709
710fn build_layer_filter(pair: pest::iterators::Pair<'_, Rule>) -> Vec<Layer> {
713 pair.into_inner()
714 .filter(|p| p.as_rule() == Rule::layer_name)
715 .map(|p| {
716 let s = p.as_str();
717 if s.eq_ignore_ascii_case("episodic") {
718 Layer::Episodic
719 } else if s.eq_ignore_ascii_case("semantic") {
720 Layer::Semantic
721 } else if s.eq_ignore_ascii_case("working") {
722 Layer::Working
723 } else if s.eq_ignore_ascii_case("procedural") {
724 Layer::Procedural
725 } else {
726 Layer::Episodic
727 }
728 })
729 .collect()
730}
731
732fn extract_about(pair: pest::iterators::Pair<'_, Rule>) -> Result<String, ParseError> {
733 let inner = pair
734 .into_inner()
735 .next()
736 .ok_or_else(|| ParseError::simple("empty ABOUT clause"))?;
737 Ok(match inner.as_rule() {
738 Rule::parameter => inner.as_str().to_string(),
739 Rule::string_literal => extract_string_value(inner)?,
740 _ => String::new(),
741 })
742}
743
744fn extract_string_list(pair: pest::iterators::Pair<'_, Rule>) -> Result<Vec<String>, ParseError> {
745 fn inner_list(pair: pest::iterators::Pair<'_, Rule>) -> Result<Vec<String>, ParseError> {
746 let mut result = Vec::new();
747 for p in pair.into_inner() {
748 if p.as_rule() == Rule::string_list {
749 result.extend(inner_list(p)?);
750 } else if p.as_rule() == Rule::string_literal {
751 result.push(extract_string_value(p)?);
752 }
753 }
754 Ok(result)
755 }
756 inner_list(pair)
757}
758
759fn extract_string_value(pair: pest::iterators::Pair<'_, Rule>) -> Result<String, ParseError> {
760 let raw = pair
762 .into_inner()
763 .next()
764 .map(|p| p.as_str().to_string())
765 .unwrap_or_default();
766 unescape_string(&raw)
767}
768
769fn unescape_string(s: &str) -> Result<String, ParseError> {
772 let mut out = String::with_capacity(s.len());
773 let mut chars = s.chars();
774 while let Some(c) = chars.next() {
775 if c == '\\' {
776 match chars.next() {
777 Some('n') => out.push('\n'),
778 Some('t') => out.push('\t'),
779 Some('r') => out.push('\r'),
780 Some('\\') => out.push('\\'),
781 Some('"') => out.push('"'),
782 Some('\'') => out.push('\''),
783 Some(other) => {
784 return Err(ParseError::simple(format!(
785 "invalid escape sequence: '\\{other}'"
786 )));
787 }
788 None => out.push('\\'),
789 }
790 } else {
791 out.push(c);
792 }
793 }
794 Ok(out)
795}
796
797fn extract_string_from_clause(pair: pest::iterators::Pair<'_, Rule>) -> Result<String, ParseError> {
798 for p in pair.into_inner() {
799 match p.as_rule() {
800 Rule::parameter => return Ok(p.as_str().to_string()),
801 Rule::string_literal => return extract_string_value(p),
802 _ => {}
803 }
804 }
805 Ok(String::new())
806}
807
808fn extract_float(pair: pest::iterators::Pair<'_, Rule>) -> Result<f32, ParseError> {
809 let p = pair
810 .into_inner()
811 .find(|p| p.as_rule() == Rule::float_literal)
812 .ok_or_else(|| ParseError::simple("expected float literal"))?;
813 let text = p.as_str();
814 text.parse::<f32>()
815 .map_err(|_| ParseError::simple(format!("invalid float literal: '{text}'")))
816}
817
818fn extract_int(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
819 let p = pair
820 .into_inner()
821 .find(|p| p.as_rule() == Rule::integer_literal || p.as_rule() == Rule::parameter)
822 .ok_or_else(|| ParseError::simple("expected integer literal"))?;
823 if p.as_rule() == Rule::parameter {
825 return Ok(0);
826 }
827 let text = p.as_str();
828 text.parse::<usize>()
829 .map_err(|_| ParseError::simple(format!("invalid integer literal: '{text}'")))
830}
831
832fn build_temporal(pair: pest::iterators::Pair<'_, Rule>) -> Result<TemporalClause, ParseError> {
833 let inner = pair
834 .into_inner()
835 .next()
836 .ok_or_else(|| ParseError::simple("empty temporal clause"))?;
837 Ok(match inner.as_rule() {
838 Rule::after_clause => {
839 let s = inner
840 .into_inner()
841 .find(|p| p.as_rule() == Rule::string_literal)
842 .map(extract_string_value)
843 .transpose()?
844 .unwrap_or_default();
845 TemporalClause::After(s)
846 }
847 Rule::before_clause => {
848 let s = inner
849 .into_inner()
850 .find(|p| p.as_rule() == Rule::string_literal)
851 .map(extract_string_value)
852 .transpose()?
853 .unwrap_or_default();
854 TemporalClause::Before(s)
855 }
856 Rule::between_clause => {
857 let strings: Vec<String> = inner
858 .into_inner()
859 .filter(|p| p.as_rule() == Rule::string_literal)
860 .map(extract_string_value)
861 .collect::<Result<Vec<_>, _>>()?;
862 TemporalClause::Between {
863 start: strings.first().cloned().unwrap_or_default(),
864 end: strings.get(1).cloned().unwrap_or_default(),
865 }
866 }
867 _ => TemporalClause::After(String::new()),
868 })
869}
870
871fn build_expand(pair: pest::iterators::Pair<'_, Rule>) -> Result<ExpandClause, ParseError> {
872 let mut depth = 1;
873 let mut min_weight = None;
874 let mut activation = None;
875
876 for inner in pair.into_inner() {
877 match inner.as_rule() {
878 Rule::integer_literal => {
879 depth = inner.as_str().parse::<usize>().map_err(|_| {
880 ParseError::simple(format!("invalid DEPTH value: '{}'", inner.as_str()))
881 })?;
882 }
883 Rule::min_weight_clause => min_weight = Some(extract_float(inner)?),
884 Rule::activation_clause => {
885 let mode_str = inner
886 .into_inner()
887 .find(|p| p.as_rule() == Rule::activation_mode)
888 .map(|p| p.as_str())
889 .unwrap_or_default();
890 activation = Some(if mode_str.eq_ignore_ascii_case("spreading") {
891 ActivationModeAst::Spreading
892 } else if mode_str.eq_ignore_ascii_case("static") {
893 ActivationModeAst::Static
894 } else if mode_str.eq_ignore_ascii_case("ppr")
895 || mode_str.eq_ignore_ascii_case("pagerank")
896 {
897 ActivationModeAst::Ppr
898 } else {
899 ActivationModeAst::None
900 });
901 }
902 _ => {}
903 }
904 }
905
906 Ok(ExpandClause {
907 depth,
908 min_weight,
909 activation,
910 })
911}
912
913fn extract_follow_causes(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
914 let p = pair
915 .into_inner()
916 .find(|p| p.as_rule() == Rule::integer_literal)
917 .ok_or_else(|| ParseError::simple("FOLLOW CAUSES requires an integer depth"))?;
918 let text = p.as_str();
919 text.parse::<usize>()
920 .map_err(|_| ParseError::simple(format!("invalid FOLLOW CAUSES depth: '{text}'")))
921}
922
923fn build_where(pair: pest::iterators::Pair<'_, Rule>) -> Result<WhereCondition, ParseError> {
924 let condition = pair
925 .into_inner()
926 .find(|p| p.as_rule() == Rule::condition)
927 .ok_or_else(|| ParseError::simple("WHERE clause missing condition"))?;
928 build_condition(condition)
929}
930
931fn build_condition(
933 condition: pest::iterators::Pair<'_, Rule>,
934) -> Result<WhereCondition, ParseError> {
935 let mut field = String::new();
936 let mut op = ComparisonOp::Gt;
937 let mut value = ConditionValue::Float(0.0);
938
939 for inner in condition.into_inner() {
940 match inner.as_rule() {
941 Rule::identifier => field = inner.as_str().to_string(),
942 Rule::comparison_op => {
943 op = match inner.as_str() {
944 ">=" => ComparisonOp::Gte,
945 "<=" => ComparisonOp::Lte,
946 "!=" => ComparisonOp::Neq,
947 ">" => ComparisonOp::Gt,
948 "<" => ComparisonOp::Lt,
949 "=" => ComparisonOp::Eq,
950 _ => ComparisonOp::Eq,
951 };
952 }
953 Rule::float_literal => {
954 let text = inner.as_str();
955 value = ConditionValue::Float(text.parse().map_err(|_| {
956 ParseError::simple(format!("invalid float in WHERE: '{text}'"))
957 })?);
958 }
959 Rule::integer_literal => {
960 let text = inner.as_str();
961 value = ConditionValue::Int(text.parse().map_err(|_| {
962 ParseError::simple(format!("invalid integer in WHERE: '{text}'"))
963 })?);
964 }
965 Rule::string_literal => {
966 value = ConditionValue::String(extract_string_value(inner)?);
967 }
968 Rule::parameter => {
969 value = ConditionValue::Param(inner.as_str().to_string());
970 }
971 _ => {}
972 }
973 }
974
975 Ok(WhereCondition { field, op, value })
976}
977
978fn build_in_subquery(pair: pest::iterators::Pair<'_, Rule>) -> Result<SubqueryFilter, ParseError> {
980 let mut field = String::new();
981 let mut subquery = None;
982
983 for inner in pair.into_inner() {
984 match inner.as_rule() {
985 Rule::identifier => field = inner.as_str().to_string(),
986 Rule::subquery => subquery = Some(build_subquery(inner)?),
987 _ => {}
988 }
989 }
990
991 Ok(SubqueryFilter {
992 field,
993 subquery: subquery.unwrap_or(Subquery {
994 layers: vec![],
995 about: String::new(),
996 involving: None,
997 temporal: None,
998 limit: None,
999 }),
1000 })
1001}
1002
1003fn build_subquery(pair: pest::iterators::Pair<'_, Rule>) -> Result<Subquery, ParseError> {
1005 let mut layers = vec![];
1006 let mut about = String::new();
1007 let mut involving = None;
1008 let mut temporal = None;
1009 let mut limit = None;
1010
1011 for inner in pair.into_inner() {
1012 match inner.as_rule() {
1013 Rule::layer_filter => layers = build_layer_filter(inner),
1014 Rule::about_clause => about = extract_about(inner)?,
1015 Rule::involving_clause => involving = Some(extract_string_list(inner)?),
1016 Rule::temporal_clause => temporal = Some(build_temporal(inner)?),
1017 Rule::limit_clause => limit = Some(extract_limit(inner)?),
1018 _ => {}
1019 }
1020 }
1021
1022 Ok(Subquery {
1023 layers,
1024 about,
1025 involving,
1026 temporal,
1027 limit,
1028 })
1029}
1030
1031fn build_as_of(pair: pest::iterators::Pair<'_, Rule>) -> Result<RecallSnapshotAst, ParseError> {
1033 let inner = pair
1034 .into_inner()
1035 .next()
1036 .ok_or_else(|| ParseError::simple("AS OF clause requires a snapshot target"))?;
1037
1038 match inner.as_rule() {
1039 Rule::string_literal => Ok(RecallSnapshotAst::Unqualified(extract_string_value(inner)?)),
1040 Rule::as_of_observed => Ok(RecallSnapshotAst::Observed(extract_single_string_literal(
1041 inner,
1042 "AS OF OBSERVED",
1043 )?)),
1044 Rule::as_of_recorded => Ok(RecallSnapshotAst::Recorded(extract_single_string_literal(
1045 inner,
1046 "AS OF RECORDED",
1047 )?)),
1048 Rule::as_of_revision => Ok(RecallSnapshotAst::Revision(extract_single_string_literal(
1049 inner,
1050 "AS OF REVISION",
1051 )?)),
1052 other => Err(ParseError::simple(format!(
1053 "unexpected AS OF target: {other:?}"
1054 ))),
1055 }
1056}
1057
1058fn extract_single_string_literal(
1059 pair: pest::iterators::Pair<'_, Rule>,
1060 clause: &str,
1061) -> Result<String, ParseError> {
1062 pair.into_inner()
1063 .find(|p| p.as_rule() == Rule::string_literal)
1064 .map(extract_string_value)
1065 .transpose()?
1066 .ok_or_else(|| ParseError::simple(format!("{clause} requires a string literal")))
1067}
1068
1069fn build_output_format(pair: pest::iterators::Pair<'_, Rule>) -> OutputFormat {
1070 let fmt_str = pair
1071 .into_inner()
1072 .find(|p| p.as_rule() == Rule::output_format)
1073 .map(|p| p.as_str())
1074 .unwrap_or_default();
1075 parse_output_format(fmt_str)
1076}
1077
1078fn build_format_clause(pair: pest::iterators::Pair<'_, Rule>) -> OutputFormat {
1079 let fmt_str = pair
1080 .into_inner()
1081 .find(|p| p.as_rule() == Rule::output_format)
1082 .map(|p| p.as_str())
1083 .unwrap_or_default();
1084 parse_output_format(fmt_str)
1085}
1086
1087fn parse_output_format(s: &str) -> OutputFormat {
1088 if s.eq_ignore_ascii_case("narrative") {
1089 OutputFormat::Narrative
1090 } else if s.eq_ignore_ascii_case("context") {
1091 OutputFormat::Context
1092 } else if s.eq_ignore_ascii_case("graph") {
1093 OutputFormat::Graph
1094 } else if s.eq_ignore_ascii_case("causal_chain") {
1095 OutputFormat::CausalChain
1096 } else if s.eq_ignore_ascii_case("json") {
1097 OutputFormat::Json
1098 } else if s.eq_ignore_ascii_case("csv") {
1099 OutputFormat::Csv
1100 } else if s.eq_ignore_ascii_case("structured") {
1101 OutputFormat::Structured
1102 } else {
1103 OutputFormat::Context
1104 }
1105}
1106
1107fn build_group_by(pair: pest::iterators::Pair<'_, Rule>) -> GroupByClause {
1108 let mut field = String::new();
1109 let mut function = AggFunction::Count;
1110 for inner in pair.into_inner() {
1111 match inner.as_rule() {
1112 Rule::identifier => field = inner.as_str().to_string(),
1113 Rule::agg_function => {
1114 let s = inner.as_str();
1115 function = if s.eq_ignore_ascii_case("count") {
1116 AggFunction::Count
1117 } else if s.eq_ignore_ascii_case("avg") {
1118 AggFunction::Avg
1119 } else if s.eq_ignore_ascii_case("sum") {
1120 AggFunction::Sum
1121 } else if s.eq_ignore_ascii_case("min") {
1122 AggFunction::Min
1123 } else if s.eq_ignore_ascii_case("max") {
1124 AggFunction::Max
1125 } else {
1126 AggFunction::Count
1127 };
1128 }
1129 _ => {}
1130 }
1131 }
1132 GroupByClause { field, function }
1133}
1134
1135fn build_field_list(pair: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
1136 let mut fields = Vec::new();
1137 for inner in pair.into_inner() {
1138 if inner.as_rule() == Rule::field_list {
1139 for field in inner.into_inner() {
1140 if field.as_rule() == Rule::identifier {
1141 fields.push(field.as_str().to_string());
1142 }
1143 }
1144 }
1145 }
1146 fields
1147}
1148
1149fn extract_budget(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
1150 extract_int(pair)
1151}
1152
1153fn extract_namespace(pair: pest::iterators::Pair<'_, Rule>) -> String {
1154 pair.into_inner()
1155 .find_map(|p| match p.as_rule() {
1156 Rule::namespace_identifier => Some(p.as_str().to_string()),
1157 Rule::string_literal => extract_string_value(p).ok(),
1158 _ => None,
1159 })
1160 .unwrap_or_default()
1161}
1162
1163fn extract_realm_list(pair: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
1166 pair.into_inner()
1167 .filter(|p| p.as_rule() == Rule::string_literal)
1168 .filter_map(|p| extract_string_value(p).ok())
1169 .collect()
1170}
1171
1172fn build_consistency(pair: pest::iterators::Pair<'_, Rule>) -> ConsistencyLevel {
1173 let level_str = pair
1174 .into_inner()
1175 .find(|p| p.as_rule() == Rule::consistency_level)
1176 .map(|p| p.as_str())
1177 .unwrap_or_default();
1178 if level_str.eq_ignore_ascii_case("linearizable") {
1179 ConsistencyLevel::Linearizable
1180 } else if level_str.eq_ignore_ascii_case("eventual") {
1181 ConsistencyLevel::Eventual
1182 } else {
1183 ConsistencyLevel::Session
1184 }
1185}
1186
1187fn extract_limit(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
1188 extract_int(pair)
1189}
1190
1191fn build_retrieval_mode_with_hops(
1192 pair: pest::iterators::Pair<'_, Rule>,
1193) -> Result<(RetrievalMode, Option<usize>), ParseError> {
1194 let mut mode = RetrievalMode::Local;
1195 let mut max_hops = None;
1196
1197 for inner in pair.into_inner() {
1198 match inner.as_rule() {
1199 Rule::retrieval_mode => {
1200 let mode_str = inner.as_str();
1201 mode = if mode_str.eq_ignore_ascii_case("global") {
1202 RetrievalMode::Global
1203 } else if mode_str.eq_ignore_ascii_case("hybrid") {
1204 RetrievalMode::Hybrid
1205 } else if mode_str.eq_ignore_ascii_case("raptor") {
1206 RetrievalMode::Raptor
1207 } else if mode_str.eq_ignore_ascii_case("adaptive") {
1208 RetrievalMode::Adaptive
1209 } else if mode_str.eq_ignore_ascii_case("iterative") {
1210 RetrievalMode::Iterative
1211 } else {
1212 RetrievalMode::Local
1213 };
1214 }
1215 Rule::max_hops_clause => {
1216 let hops = extract_integer_from_clause(inner)?;
1217 if hops == 0 || hops > 5 {
1218 return Err(ParseError::simple(format!(
1219 "MAX_HOPS must be between 1 and 5, got {hops}"
1220 )));
1221 }
1222 max_hops = Some(hops);
1223 }
1224 _ => {}
1225 }
1226 }
1227
1228 if max_hops.is_some() && mode != RetrievalMode::Iterative {
1230 return Err(ParseError::simple(
1231 "MAX_HOPS can only be used with MODE ITERATIVE",
1232 ));
1233 }
1234
1235 Ok((mode, max_hops))
1236}
1237
1238fn build_depth_mode(pair: pest::iterators::Pair<'_, Rule>) -> Result<DepthModeAst, ParseError> {
1239 let mode_str = pair
1240 .into_inner()
1241 .find(|p| p.as_rule() == Rule::depth_mode)
1242 .map(|p| p.as_str())
1243 .unwrap_or_default();
1244 if mode_str.eq_ignore_ascii_case("full") {
1245 Ok(DepthModeAst::Full)
1246 } else if mode_str.eq_ignore_ascii_case("summary") {
1247 Ok(DepthModeAst::Summary)
1248 } else if mode_str.eq_ignore_ascii_case("auto") {
1249 Ok(DepthModeAst::Auto)
1250 } else {
1251 Err(ParseError::simple(format!(
1252 "unknown DEPTH mode '{mode_str}', expected AUTO, FULL, or SUMMARY"
1253 )))
1254 }
1255}
1256
1257fn build_on_off(pair: pest::iterators::Pair<'_, Rule>) -> Result<bool, ParseError> {
1258 let val = pair
1259 .into_inner()
1260 .find(|p| p.as_rule() == Rule::on_off)
1261 .map(|p| p.as_str().to_string())
1262 .unwrap_or_default();
1263 if val.eq_ignore_ascii_case("on") {
1264 Ok(true)
1265 } else if val.eq_ignore_ascii_case("off") {
1266 Ok(false)
1267 } else {
1268 Err(ParseError::simple(format!(
1269 "expected ON or OFF, got '{val}'"
1270 )))
1271 }
1272}
1273
1274fn extract_integer_from_clause(pair: pest::iterators::Pair<'_, Rule>) -> Result<usize, ParseError> {
1275 extract_int(pair)
1276}
1277
1278fn build_traverse(pair: pest::iterators::Pair<'_, Rule>) -> Result<TraverseStmt, ParseError> {
1279 let mut from = String::new();
1280 let mut via = None;
1281 let mut depth = 1;
1282 let mut where_clauses = vec![];
1283 let mut limit = None;
1284
1285 for inner in pair.into_inner() {
1286 match inner.as_rule() {
1287 Rule::string_literal => from = extract_string_value(inner)?,
1288 Rule::via_clause => {
1289 let mut rels = vec![];
1290 for child in inner.into_inner() {
1291 if child.as_rule() == Rule::relation_list {
1292 for id in child.into_inner() {
1293 if id.as_rule() == Rule::identifier {
1294 rels.push(id.as_str().to_string());
1295 }
1296 }
1297 }
1298 }
1299 via = Some(rels);
1300 }
1301 Rule::integer_literal => {
1302 depth = inner.as_str().parse::<usize>().map_err(|_| {
1303 ParseError::simple(format!("invalid DEPTH value: '{}'", inner.as_str()))
1304 })?;
1305 }
1306 Rule::where_clause => where_clauses.push(build_where(inner)?),
1307 Rule::limit_clause => limit = Some(extract_limit(inner)?),
1308 _ => {}
1309 }
1310 }
1311
1312 Ok(TraverseStmt {
1313 from,
1314 via,
1315 depth,
1316 where_clauses,
1317 limit,
1318 namespace: None,
1319 })
1320}
1321
1322fn build_explain(pair: pest::iterators::Pair<'_, Rule>) -> Result<Statement, ParseError> {
1323 let mut analyze = false;
1324 let mut inner_stmt = None;
1325
1326 for child in pair.into_inner() {
1327 match child.as_rule() {
1328 Rule::analyze_flag => analyze = true,
1329 Rule::inner_stmt => {
1330 let actual = child
1331 .into_inner()
1332 .next()
1333 .ok_or_else(|| ParseError::simple("EXPLAIN requires a statement"))?;
1334 inner_stmt = Some(match actual.as_rule() {
1335 Rule::recall_events_stmt => {
1336 build_recall_events(actual).map(Statement::RecallEvents)?
1337 }
1338 Rule::recall_stmt => {
1339 build_recall(actual).map(|stmt| Statement::Recall(Box::new(stmt)))?
1340 }
1341 Rule::think_stmt => {
1342 build_think(actual).map(|stmt| Statement::Think(Box::new(stmt)))?
1343 }
1344 Rule::forget_stmt => return unsupported_embedded_statement(Rule::forget_stmt),
1345 Rule::correct_stmt => build_correct(actual).map(Statement::Correct)?,
1346 Rule::supersede_stmt => build_supersede(actual).map(Statement::Supersede)?,
1347 Rule::merge_memory_stmt => {
1348 build_merge_memory(actual).map(Statement::MergeMemory)?
1349 }
1350 Rule::retract_stmt => build_retract(actual).map(Statement::Retract)?,
1351 Rule::history_stmt => build_history(actual).map(Statement::History)?,
1352 Rule::traverse_stmt => build_traverse(actual).map(Statement::Traverse)?,
1353 Rule::inspect_stmt => build_inspect(actual).map(Statement::Inspect)?,
1354 Rule::trace_stmt => build_trace(actual).map(Statement::Trace)?,
1355 Rule::explain_causes_stmt => {
1356 build_explain_causes(actual).map(Statement::ExplainCauses)?
1357 }
1358 Rule::what_if_stmt => build_what_if(actual).map(Statement::WhatIf)?,
1359 Rule::counterfactual_stmt => {
1360 build_counterfactual(actual).map(Statement::Counterfactual)?
1361 }
1362 Rule::show_policies_stmt => {
1363 build_show_policies(actual).map(Statement::ShowPolicies)?
1364 }
1365 Rule::explain_policy_stmt => {
1366 build_explain_policy(actual).map(Statement::ExplainPolicy)?
1367 }
1368 _ => {
1369 return Err(ParseError::simple(format!(
1370 "EXPLAIN not supported for {:?}",
1371 actual.as_rule()
1372 )));
1373 }
1374 });
1375 }
1376 _ => {}
1377 }
1378 }
1379
1380 let inner = inner_stmt.ok_or_else(|| ParseError::simple("EXPLAIN requires a statement"))?;
1381
1382 Ok(Statement::Explain(ExplainStmt {
1383 analyze,
1384 inner: Box::new(inner),
1385 }))
1386}
1387
1388fn build_explain_causes(
1389 pair: pest::iterators::Pair<'_, Rule>,
1390) -> Result<ExplainCausesStmt, ParseError> {
1391 let mut target = String::new();
1392 let mut namespace = None;
1393 let mut depth = None;
1394
1395 for inner in pair.into_inner() {
1396 match inner.as_rule() {
1397 Rule::string_literal => target = extract_string_value(inner)?,
1398 Rule::parameter => target = inner.as_str().to_string(),
1399 Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
1400 Rule::causes_depth_clause => {
1401 depth = Some(extract_integer_from_clause(inner)?);
1402 }
1403 _ => {}
1404 }
1405 }
1406
1407 Ok(ExplainCausesStmt {
1408 target,
1409 namespace,
1410 depth,
1411 })
1412}
1413
1414fn build_what_if(pair: pest::iterators::Pair<'_, Rule>) -> Result<WhatIfStmt, ParseError> {
1415 let mut intervention = String::new();
1416 let mut outcome = String::new();
1417 let mut namespace = None;
1418 let mut got_first = false;
1419
1420 for inner in pair.into_inner() {
1421 match inner.as_rule() {
1422 Rule::string_literal | Rule::parameter => {
1423 let val = if inner.as_rule() == Rule::string_literal {
1424 extract_string_value(inner)?
1425 } else {
1426 inner.as_str().to_string()
1427 };
1428 if !got_first {
1429 intervention = val;
1430 got_first = true;
1431 } else {
1432 outcome = val;
1433 }
1434 }
1435 Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
1436 _ => {}
1437 }
1438 }
1439
1440 Ok(WhatIfStmt {
1441 intervention,
1442 outcome,
1443 namespace,
1444 })
1445}
1446
1447fn build_counterfactual(
1448 pair: pest::iterators::Pair<'_, Rule>,
1449) -> Result<CounterfactualStmt, ParseError> {
1450 let mut antecedent = String::new();
1451 let mut consequent = String::new();
1452 let mut namespace = None;
1453 let mut got_first = false;
1454
1455 for inner in pair.into_inner() {
1456 match inner.as_rule() {
1457 Rule::string_literal | Rule::parameter => {
1458 let val = if inner.as_rule() == Rule::string_literal {
1459 extract_string_value(inner)?
1460 } else {
1461 inner.as_str().to_string()
1462 };
1463 if !got_first {
1464 antecedent = val;
1465 got_first = true;
1466 } else {
1467 consequent = val;
1468 }
1469 }
1470 Rule::namespace_clause => namespace = Some(extract_namespace(inner)),
1471 _ => {}
1472 }
1473 }
1474
1475 Ok(CounterfactualStmt {
1476 antecedent,
1477 consequent,
1478 namespace,
1479 })
1480}
1481
1482fn build_set_assignment_list(
1483 pair: pest::iterators::Pair<'_, Rule>,
1484) -> Result<Vec<SetAssignment>, ParseError> {
1485 let mut assignments = vec![];
1486 for child in pair.into_inner() {
1487 if child.as_rule() == Rule::set_assignment {
1488 assignments.push(build_set_assignment(child)?);
1489 }
1490 }
1491 Ok(assignments)
1492}
1493
1494fn build_set_assignment(
1495 pair: pest::iterators::Pair<'_, Rule>,
1496) -> Result<SetAssignment, ParseError> {
1497 let mut field = String::new();
1498 let mut value = SetValue::Int(0);
1499
1500 for inner in pair.into_inner() {
1501 match inner.as_rule() {
1502 Rule::identifier => field = inner.as_str().to_string(),
1503 Rule::set_value => {
1504 let child = inner
1505 .into_inner()
1506 .next()
1507 .ok_or_else(|| ParseError::simple("empty set value"))?;
1508 value = match child.as_rule() {
1509 Rule::set_function => build_set_function(child)?,
1510 Rule::float_literal => {
1511 let text = child.as_str();
1512 SetValue::Float(text.parse().map_err(|_| {
1513 ParseError::simple(format!("invalid float in SET: '{text}'"))
1514 })?)
1515 }
1516 Rule::integer_literal => {
1517 let text = child.as_str();
1518 SetValue::Int(text.parse().map_err(|_| {
1519 ParseError::simple(format!("invalid integer in SET: '{text}'"))
1520 })?)
1521 }
1522 Rule::string_literal => SetValue::String(extract_string_value(child)?),
1523 _ => SetValue::Int(0),
1524 };
1525 }
1526 _ => {}
1527 }
1528 }
1529
1530 Ok(SetAssignment { field, value })
1531}
1532
1533fn build_set_function(pair: pest::iterators::Pair<'_, Rule>) -> Result<SetValue, ParseError> {
1534 let raw = pair.as_str();
1535 let is_max = raw.len() >= 3 && raw[..3].eq_ignore_ascii_case("max");
1536 let mut field = String::new();
1537 let mut val = 0.0;
1538
1539 for inner in pair.into_inner() {
1540 match inner.as_rule() {
1541 Rule::identifier => field = inner.as_str().to_string(),
1542 Rule::float_literal => {
1543 let t = inner.as_str();
1544 val = t.parse().map_err(|_| {
1545 ParseError::simple(format!("invalid float in SET function: '{t}'"))
1546 })?;
1547 }
1548 Rule::integer_literal => {
1549 let t = inner.as_str();
1550 val = t.parse().map_err(|_| {
1551 ParseError::simple(format!("invalid integer in SET function: '{t}'"))
1552 })?;
1553 }
1554 _ => {}
1555 }
1556 }
1557
1558 Ok(if is_max {
1559 SetValue::Max(field, val)
1560 } else {
1561 SetValue::Min(field, val)
1562 })
1563}
1564
1565fn build_modality_list(pair: pest::iterators::Pair<'_, Rule>) -> Result<Vec<String>, ParseError> {
1568 build_named_list(
1569 pair,
1570 Rule::modality_list,
1571 Rule::modality_name,
1572 "MODALITY clause missing modality list",
1573 )
1574}
1575
1576fn build_evidence_role_list(
1577 pair: pest::iterators::Pair<'_, Rule>,
1578) -> Result<Vec<String>, ParseError> {
1579 build_named_list(
1580 pair,
1581 Rule::evidence_role_list,
1582 Rule::evidence_role_name,
1583 "RESOURCE_ROLE clause missing evidence role list",
1584 )
1585}
1586
1587fn build_hydration_mode_list(
1588 pair: pest::iterators::Pair<'_, Rule>,
1589) -> Result<Vec<String>, ParseError> {
1590 build_named_list(
1591 pair,
1592 Rule::hydration_mode_list,
1593 Rule::hydration_mode_name,
1594 "HYDRATION clause missing hydration mode list",
1595 )
1596}
1597
1598fn build_artifact_kind_list(
1599 pair: pest::iterators::Pair<'_, Rule>,
1600) -> Result<Vec<String>, ParseError> {
1601 build_named_list(
1602 pair,
1603 Rule::artifact_kind_list,
1604 Rule::artifact_kind_name,
1605 "ARTIFACT clause missing artifact kind list",
1606 )
1607}
1608
1609fn build_named_list(
1610 pair: pest::iterators::Pair<'_, Rule>,
1611 list_rule: Rule,
1612 item_rule: Rule,
1613 missing_message: &str,
1614) -> Result<Vec<String>, ParseError> {
1615 let list = pair
1616 .into_inner()
1617 .find(|p| p.as_rule() == list_rule)
1618 .ok_or_else(|| ParseError::simple(missing_message))?;
1619 Ok(list
1620 .into_inner()
1621 .filter(|p| p.as_rule() == item_rule)
1622 .map(|p| p.as_str().to_lowercase())
1623 .collect())
1624}
1625
1626fn build_create_realm(
1629 pair: pest::iterators::Pair<'_, Rule>,
1630) -> Result<CreateRealmStmt, ParseError> {
1631 let mut name = String::new();
1632 let mut description = None;
1633
1634 for inner in pair.into_inner() {
1635 match inner.as_rule() {
1636 Rule::string_literal if name.is_empty() => {
1637 name = extract_string_value(inner)?;
1638 }
1639 Rule::realm_description => {
1640 for child in inner.into_inner() {
1641 if child.as_rule() == Rule::string_literal {
1642 description = Some(extract_string_value(child)?);
1643 }
1644 }
1645 }
1646 _ => {}
1647 }
1648 }
1649
1650 Ok(CreateRealmStmt { name, description })
1651}
1652
1653fn build_drop_realm(pair: pest::iterators::Pair<'_, Rule>) -> Result<DropRealmStmt, ParseError> {
1654 let mut name = String::new();
1655 let mut confirm = false;
1656
1657 for inner in pair.into_inner() {
1658 match inner.as_rule() {
1659 Rule::string_literal if name.is_empty() => {
1660 name = extract_string_value(inner)?;
1661 }
1662 Rule::confirm_flag => confirm = true,
1663 _ => {}
1664 }
1665 }
1666
1667 Ok(DropRealmStmt { name, confirm })
1668}
1669
1670fn build_action_list(pair: pest::iterators::Pair<'_, Rule>) -> Vec<String> {
1673 pair.into_inner()
1674 .filter(|p| p.as_rule() == Rule::action_name)
1675 .map(|p| p.as_str().to_lowercase())
1676 .collect()
1677}
1678
1679fn build_grant_target(pair: pest::iterators::Pair<'_, Rule>) -> Result<GrantTarget, ParseError> {
1680 let raw = pair.as_str();
1681 let is_namespace = raw.to_ascii_lowercase().contains("namespace");
1682 let string_val = pair
1683 .into_inner()
1684 .find(|p| p.as_rule() == Rule::string_literal)
1685 .map(extract_string_value)
1686 .transpose()?
1687 .unwrap_or_default();
1688
1689 Ok(if is_namespace {
1690 GrantTarget::Namespace(string_val)
1691 } else {
1692 GrantTarget::Realm(string_val)
1693 })
1694}
1695
1696fn build_principal_ref(pair: pest::iterators::Pair<'_, Rule>) -> Result<PrincipalRef, ParseError> {
1697 let raw = pair.as_str();
1698 let is_team = raw.to_ascii_lowercase().contains("team");
1699 let string_val = pair
1700 .into_inner()
1701 .find(|p| p.as_rule() == Rule::string_literal)
1702 .map(extract_string_value)
1703 .transpose()?
1704 .unwrap_or_default();
1705
1706 Ok(if is_team {
1707 PrincipalRef::Team(string_val)
1708 } else {
1709 PrincipalRef::Agent(string_val)
1710 })
1711}
1712
1713fn build_grant(pair: pest::iterators::Pair<'_, Rule>) -> Result<GrantStmt, ParseError> {
1714 let mut actions = Vec::new();
1715 let mut target = None;
1716 let mut principal = None;
1717
1718 for inner in pair.into_inner() {
1719 match inner.as_rule() {
1720 Rule::action_list => actions = build_action_list(inner),
1721 Rule::grant_target => target = Some(build_grant_target(inner)?),
1722 Rule::principal_ref => principal = Some(build_principal_ref(inner)?),
1723 _ => {}
1724 }
1725 }
1726
1727 Ok(GrantStmt {
1728 actions,
1729 target: target
1730 .ok_or_else(|| ParseError::simple("GRANT requires ON NAMESPACE/REALM clause"))?,
1731 principal: principal
1732 .ok_or_else(|| ParseError::simple("GRANT requires TO AGENT/TEAM clause"))?,
1733 })
1734}
1735
1736fn build_revoke(pair: pest::iterators::Pair<'_, Rule>) -> Result<RevokeStmt, ParseError> {
1737 let mut actions = Vec::new();
1738 let mut target = None;
1739 let mut principal = None;
1740
1741 for inner in pair.into_inner() {
1742 match inner.as_rule() {
1743 Rule::action_list => actions = build_action_list(inner),
1744 Rule::grant_target => target = Some(build_grant_target(inner)?),
1745 Rule::principal_ref => principal = Some(build_principal_ref(inner)?),
1746 _ => {}
1747 }
1748 }
1749
1750 Ok(RevokeStmt {
1751 actions,
1752 target: target
1753 .ok_or_else(|| ParseError::simple("REVOKE requires ON NAMESPACE/REALM clause"))?,
1754 principal: principal
1755 .ok_or_else(|| ParseError::simple("REVOKE requires FROM AGENT/TEAM clause"))?,
1756 })
1757}
1758
1759fn build_show_policies(
1762 pair: pest::iterators::Pair<'_, Rule>,
1763) -> Result<ShowPoliciesStmt, ParseError> {
1764 let mut principal = None;
1765
1766 for inner in pair.into_inner() {
1767 if inner.as_rule() == Rule::principal_ref {
1768 principal = Some(build_principal_ref(inner)?);
1769 }
1770 }
1771
1772 Ok(ShowPoliciesStmt { principal })
1773}
1774
1775fn build_explain_policy(
1776 pair: pest::iterators::Pair<'_, Rule>,
1777) -> Result<ExplainPolicyStmt, ParseError> {
1778 let mut principal = None;
1779 let mut resource_type = String::new();
1780 let mut resource_name = String::new();
1781 let mut action = String::new();
1782
1783 let raw = pair.as_str();
1784 let raw_lower = raw.to_ascii_lowercase();
1786 if raw_lower.contains("namespace") {
1787 resource_type = "namespace".to_string();
1788 } else if raw_lower.contains("realm") {
1789 resource_type = "realm".to_string();
1790 }
1791
1792 for inner in pair.into_inner() {
1793 match inner.as_rule() {
1794 Rule::principal_ref => principal = Some(build_principal_ref(inner)?),
1795 Rule::string_literal if resource_name.is_empty() => {
1796 resource_name = extract_string_value(inner)?;
1797 }
1798 Rule::action_name => action = inner.as_str().to_lowercase(),
1799 _ => {}
1800 }
1801 }
1802
1803 Ok(ExplainPolicyStmt {
1804 principal: principal
1805 .ok_or_else(|| ParseError::simple("EXPLAIN POLICY requires FOR AGENT/TEAM clause"))?,
1806 resource_type,
1807 resource_name,
1808 action,
1809 })
1810}
1811
1812fn build_set_tier_policy(
1815 pair: pest::iterators::Pair<'_, Rule>,
1816) -> Result<SetTierPolicyStmt, ParseError> {
1817 let mut field = String::new();
1818 let mut value = None;
1819
1820 for inner in pair.into_inner() {
1821 match inner.as_rule() {
1822 Rule::tier_policy_field => {
1823 field = inner.as_str().to_lowercase();
1824 }
1825 Rule::tier_policy_value => {
1826 let val_inner = inner
1827 .into_inner()
1828 .next()
1829 .ok_or_else(|| ParseError::simple("missing tier policy value"))?;
1830 value = Some(match val_inner.as_rule() {
1831 Rule::string_literal => TierPolicyValue::Str(extract_string_value(val_inner)?),
1832 Rule::float_literal => {
1833 let v: f64 = val_inner
1834 .as_str()
1835 .parse()
1836 .map_err(|_| ParseError::simple("invalid float in SET TIER_POLICY"))?;
1837 TierPolicyValue::Float(v)
1838 }
1839 Rule::integer_literal => {
1840 let v: i64 = val_inner.as_str().parse().map_err(|_| {
1841 ParseError::simple("invalid integer in SET TIER_POLICY")
1842 })?;
1843 TierPolicyValue::Int(v)
1844 }
1845 _ => {
1846 return Err(ParseError::simple("unexpected tier policy value type"));
1847 }
1848 });
1849 }
1850 _ => {}
1851 }
1852 }
1853
1854 Ok(SetTierPolicyStmt {
1855 field,
1856 value: value.ok_or_else(|| ParseError::simple("missing value in SET TIER_POLICY"))?,
1857 })
1858}
1859
1860#[cfg(test)]
1863mod tests {
1864 use super::*;
1865
1866 #[test]
1867 fn parse_minimal_recall() {
1868 let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
1869 match stmt {
1870 Statement::Recall(r) => {
1871 assert_eq!(r.layers, vec![Layer::Episodic]);
1872 assert_eq!(r.about, "test");
1873 assert!(r.limit.is_none());
1874 }
1875 other => panic!("expected Recall, got {other:?}"),
1876 }
1877 }
1878
1879 #[test]
1880 fn parse_full_recall() {
1881 let q = r#"
1882 RECALL semantic, episodic
1883 ABOUT "vector database optimization"
1884 INVOLVING "HNSW", "benchmark"
1885 AFTER "2026-03-01"
1886 EXPAND GRAPH DEPTH 2 MIN_WEIGHT 0.3 ACTIVATION spreading
1887 FOLLOW CAUSES DEPTH 3
1888 WHERE importance > 0.4
1889 WHERE confidence > 0.8
1890 AS NARRATIVE
1891 BUDGET 4096
1892 NAMESPACE shared_knowledge
1893 CONSISTENCY linearizable
1894 LIMIT 20
1895 "#;
1896 let stmt = parse(q).unwrap();
1897 match stmt {
1898 Statement::Recall(r) => {
1899 assert_eq!(r.layers, vec![Layer::Semantic, Layer::Episodic]);
1900 assert_eq!(r.about, "vector database optimization");
1901 assert_eq!(r.involving.unwrap(), vec!["HNSW", "benchmark"]);
1902 assert_eq!(r.temporal, Some(TemporalClause::After("2026-03-01".into())));
1903
1904 let ex = r.expand.unwrap();
1905 assert_eq!(ex.depth, 2);
1906 assert_eq!(ex.min_weight, Some(0.3));
1907 assert_eq!(ex.activation, Some(ActivationModeAst::Spreading));
1908
1909 assert_eq!(r.follow_causes, Some(3));
1910 assert_eq!(r.where_clauses.len(), 2);
1911 assert_eq!(r.output_format, Some(OutputFormat::Narrative));
1912 assert_eq!(r.budget, Some(4096));
1913 assert_eq!(r.namespace, Some("shared_knowledge".into()));
1914 assert_eq!(r.consistency, Some(ConsistencyLevel::Linearizable));
1915 assert_eq!(r.limit, Some(20));
1916 }
1917 other => panic!("expected Recall, got {other:?}"),
1918 }
1919 }
1920
1921 #[test]
1922 fn parse_think_with_budget() {
1923 let q = r#"THINK ABOUT "How should I optimize HNSW?" BUDGET 4096"#;
1924 let stmt = parse(q).unwrap();
1925 match stmt {
1926 Statement::Think(t) => {
1927 assert!(t.about.contains("HNSW"));
1928 assert_eq!(t.budget, Some(4096));
1929 assert_eq!(t.mode, RetrievalMode::Local);
1930 assert_eq!(t.community_depth, None);
1931 }
1932 other => panic!("expected Think, got {other:?}"),
1933 }
1934 }
1935
1936 #[test]
1937 fn parse_think_global() {
1938 let q = r#"THINK GLOBAL ABOUT "summarize themes""#;
1939 let stmt = parse(q).unwrap();
1940 match stmt {
1941 Statement::Think(t) => {
1942 assert!(t.about.contains("themes"));
1943 assert_eq!(t.mode, RetrievalMode::Global);
1944 assert_eq!(t.community_depth, None);
1945 }
1946 other => panic!("expected Think, got {other:?}"),
1947 }
1948 }
1949
1950 #[test]
1951 fn parse_think_hybrid_with_community_depth() {
1952 let q = r#"THINK ABOUT "cross-domain links" MODE hybrid COMMUNITY_DEPTH 3"#;
1953 let stmt = parse(q).unwrap();
1954 match stmt {
1955 Statement::Think(t) => {
1956 assert!(t.about.contains("cross-domain"));
1957 assert_eq!(t.mode, RetrievalMode::Hybrid);
1958 assert_eq!(t.community_depth, Some(3));
1959 assert!(!t.hybrid);
1960 }
1961 other => panic!("expected Think, got {other:?}"),
1962 }
1963 }
1964
1965 #[test]
1966 fn parse_think_query_text_hybrid_clause() {
1967 let q = r#"THINK ABOUT "cross-domain links" HYBRID"#;
1968 let stmt = parse(q).unwrap();
1969 match stmt {
1970 Statement::Think(t) => {
1971 assert_eq!(t.mode, RetrievalMode::Local);
1972 assert!(t.hybrid);
1973 }
1974 other => panic!("expected Think, got {other:?}"),
1975 }
1976 }
1977
1978 #[test]
1979 fn parse_think_mode_local_explicit() {
1980 let q = r#"THINK ABOUT "x" MODE local"#;
1981 let stmt = parse(q).unwrap();
1982 match stmt {
1983 Statement::Think(t) => {
1984 assert_eq!(t.mode, RetrievalMode::Local);
1985 }
1986 other => panic!("expected Think, got {other:?}"),
1987 }
1988 }
1989
1990 #[test]
1991 fn roundtrip_think_global() {
1992 let q = r#"THINK GLOBAL ABOUT "themes" BUDGET 2048 COMMUNITY_DEPTH 2"#;
1993 let stmt1 = parse(q).unwrap();
1994 let rendered = stmt1.to_string();
1995 let stmt2 = parse(&rendered).unwrap();
1996 assert_eq!(stmt1, stmt2);
1997 }
1998
1999 #[test]
2000 fn roundtrip_think_hybrid() {
2001 let q = r#"THINK ABOUT "links" MODE hybrid COMMUNITY_DEPTH 5"#;
2002 let stmt1 = parse(q).unwrap();
2003 let rendered = stmt1.to_string();
2004 let stmt2 = parse(&rendered).unwrap();
2005 assert_eq!(stmt1, stmt2);
2006 }
2007
2008 #[test]
2009 fn roundtrip_think_query_text_hybrid() {
2010 let q = r#"THINK ABOUT "links" HYBRID"#;
2011 let stmt1 = parse(q).unwrap();
2012 let rendered = stmt1.to_string();
2013 let stmt2 = parse(&rendered).unwrap();
2014 assert_eq!(stmt1, stmt2);
2015 }
2016
2017 #[test]
2018 fn parse_think_mode_raptor() {
2019 let q = r#"THINK ABOUT "architecture overview" MODE raptor"#;
2020 let stmt = parse(q).unwrap();
2021 match stmt {
2022 Statement::Think(t) => {
2023 assert!(t.about.contains("architecture"));
2024 assert_eq!(t.mode, RetrievalMode::Raptor);
2025 }
2026 other => panic!("expected Think, got {other:?}"),
2027 }
2028 }
2029
2030 #[test]
2031 fn parse_think_mode_adaptive() {
2032 let q = r#"THINK ABOUT "deployment strategies" MODE adaptive"#;
2033 let stmt = parse(q).unwrap();
2034 match stmt {
2035 Statement::Think(t) => {
2036 assert!(t.about.contains("deployment"));
2037 assert_eq!(t.mode, RetrievalMode::Adaptive);
2038 }
2039 other => panic!("expected Think, got {other:?}"),
2040 }
2041 }
2042
2043 #[test]
2044 fn roundtrip_think_raptor() {
2045 let q = r#"THINK ABOUT "overview" MODE raptor COMMUNITY_DEPTH 3"#;
2046 let stmt1 = parse(q).unwrap();
2047 let rendered = stmt1.to_string();
2048 let stmt2 = parse(&rendered).unwrap();
2049 assert_eq!(stmt1, stmt2);
2050 }
2051
2052 #[test]
2053 fn roundtrip_think_adaptive() {
2054 let q = r#"THINK ABOUT "analysis" MODE adaptive"#;
2055 let stmt1 = parse(q).unwrap();
2056 let rendered = stmt1.to_string();
2057 let stmt2 = parse(&rendered).unwrap();
2058 assert_eq!(stmt1, stmt2);
2059 }
2060
2061 #[test]
2062 fn parse_correct() {
2063 let q = r#"CORRECT "some_id" SET description = "updated", confidence = 0.9 REASON "fix" OBSERVED AT "2026-01-01T00:00:00Z" CAUSED BY "cause_id" NAMESPACE custom"#;
2064 let stmt = parse(q).unwrap();
2065 match stmt {
2066 Statement::Correct(c) => {
2067 assert_eq!(c.target, SemanticTargetRef::Memory("some_id".into()));
2068 assert_eq!(c.updates.len(), 2);
2069 assert_eq!(c.reason.as_deref(), Some("fix"));
2070 assert_eq!(c.observed_at.as_deref(), Some("2026-01-01T00:00:00Z"));
2071 assert_eq!(c.caused_by.as_deref(), Some("cause_id"));
2072 assert_eq!(c.namespace.as_deref(), Some("custom"));
2073 }
2074 other => panic!("expected Correct, got {other:?}"),
2075 }
2076 }
2077
2078 #[test]
2079 fn parse_supersede() {
2080 let q = r#"SUPERSEDE LOGICAL "some_id" SET description = "replacement", confidence = 0.8 REASON "new authority" OBSERVED AT "2026-02-01T00:00:00Z" CAUSED BY "cause_id" NAMESPACE custom"#;
2081 let stmt = parse(q).unwrap();
2082 match stmt {
2083 Statement::Supersede(s) => {
2084 assert_eq!(s.target, SemanticTargetRef::Logical("some_id".into()));
2085 assert_eq!(s.updates.len(), 2);
2086 assert_eq!(s.reason.as_deref(), Some("new authority"));
2087 assert_eq!(s.observed_at.as_deref(), Some("2026-02-01T00:00:00Z"));
2088 assert_eq!(s.caused_by.as_deref(), Some("cause_id"));
2089 assert_eq!(s.namespace.as_deref(), Some("custom"));
2090 }
2091 other => panic!("expected Supersede, got {other:?}"),
2092 }
2093 }
2094
2095 #[test]
2096 fn parse_merge_memory() {
2097 let q = r#"MERGE MEMORY "source_a", REVISION "source_b" INTO LOGICAL "target_id" SET description = "canonical", confidence = 0.95 REASON "deduplicate" OBSERVED AT "2026-03-01T00:00:00Z" CAUSED BY "cause_id" NAMESPACE custom"#;
2098 let stmt = parse(q).unwrap();
2099 match stmt {
2100 Statement::MergeMemory(m) => {
2101 assert_eq!(
2102 m.sources,
2103 vec![
2104 SemanticTargetRef::Memory("source_a".into()),
2105 SemanticTargetRef::Revision("source_b".into()),
2106 ]
2107 );
2108 assert_eq!(m.target, SemanticTargetRef::Logical("target_id".into()));
2109 assert_eq!(m.updates.len(), 2);
2110 assert_eq!(m.reason.as_deref(), Some("deduplicate"));
2111 assert_eq!(m.observed_at.as_deref(), Some("2026-03-01T00:00:00Z"));
2112 assert_eq!(m.caused_by.as_deref(), Some("cause_id"));
2113 assert_eq!(m.namespace.as_deref(), Some("custom"));
2114 }
2115 other => panic!("expected MergeMemory, got {other:?}"),
2116 }
2117 }
2118
2119 #[test]
2120 fn parse_retract() {
2121 let q = r#"RETRACT REVISION "some_id" REASON "obsolete" OBSERVED AT "2026-01-01" CAUSED BY "cause_id" NAMESPACE custom"#;
2122 let stmt = parse(q).unwrap();
2123 match stmt {
2124 Statement::Retract(r) => {
2125 assert_eq!(r.target, SemanticTargetRef::Revision("some_id".into()));
2126 assert_eq!(r.reason.as_deref(), Some("obsolete"));
2127 assert_eq!(r.observed_at.as_deref(), Some("2026-01-01"));
2128 assert_eq!(r.caused_by.as_deref(), Some("cause_id"));
2129 assert_eq!(r.namespace.as_deref(), Some("custom"));
2130 }
2131 other => panic!("expected Retract, got {other:?}"),
2132 }
2133 }
2134
2135 #[test]
2136 fn parse_remember_is_unsupported() {
2137 let err = parse(r#"REMEMBER episode CONTENT "event happened""#).unwrap_err();
2138 assert!(err.message.contains("REMEMBER is not supported"));
2139 }
2140
2141 #[test]
2142 fn parse_forget_is_unsupported() {
2143 let err = parse(r#"FORGET "01J000000000000000000000""#).unwrap_err();
2144 assert!(err.message.contains("FORGET is not supported"));
2145 }
2146
2147 #[test]
2148 fn parse_connect_is_unsupported() {
2149 let q = r#"CONNECT "HNSW_indexing" TO "approximate_nearest_neighbors" AS related_to WEIGHT 0.9"#;
2150 let err = parse(q).unwrap_err();
2151 assert!(err.message.contains("CONNECT is not supported"));
2152 }
2153
2154 #[test]
2155 fn parse_consolidate_is_unsupported() {
2156 let err = parse("CONSOLIDATE WHERE episodic.access_count > 5").unwrap_err();
2157 assert!(err.message.contains("CONSOLIDATE is not supported"));
2158 }
2159
2160 #[test]
2161 fn parse_watch_is_unsupported() {
2162 let err = parse("WATCH ALL FORMAT json").unwrap_err();
2163 assert!(err.message.contains("WATCH is not supported"));
2164 }
2165
2166 #[test]
2167 fn parse_explain_analyze_forget_is_unsupported() {
2168 let err = parse(r#"EXPLAIN ANALYZE FORGET "01J000000000000000000000""#).unwrap_err();
2169 assert!(err.message.contains("FORGET is not supported"));
2170 }
2171
2172 #[test]
2173 fn parse_inspect() {
2174 let q = r#"INSPECT LOGICAL "record_id""#;
2175 let stmt = parse(q).unwrap();
2176 match stmt {
2177 Statement::Inspect(i) => {
2178 assert_eq!(i.target, SemanticTargetRef::Logical("record_id".into()));
2179 }
2180 other => panic!("expected Inspect, got {other:?}"),
2181 }
2182 }
2183
2184 #[test]
2185 fn parse_history() {
2186 let q = r#"HISTORY REVISION "record_id" NAMESPACE custom"#;
2187 let stmt = parse(q).unwrap();
2188 match stmt {
2189 Statement::History(h) => {
2190 assert_eq!(h.target, SemanticTargetRef::Revision("record_id".into()));
2191 assert_eq!(h.namespace.as_deref(), Some("custom"));
2192 }
2193 other => panic!("expected History, got {other:?}"),
2194 }
2195 }
2196
2197 #[test]
2198 fn parse_trace() {
2199 let q = r#"TRACE LOGICAL "semantic:caching_best_practices""#;
2200 let stmt = parse(q).unwrap();
2201 match stmt {
2202 Statement::Trace(t) => {
2203 assert_eq!(
2204 t.target,
2205 SemanticTargetRef::Logical("semantic:caching_best_practices".into())
2206 );
2207 }
2208 other => panic!("expected Trace, got {other:?}"),
2209 }
2210 }
2211
2212 #[test]
2213 fn parse_error_unknown_verb() {
2214 let err = parse("SELECT * FROM memories").unwrap_err();
2215 assert!(err.message.contains("unknown verb"));
2216 assert!(err.message.contains("RECALL"));
2217 }
2218
2219 #[test]
2220 fn parse_error_unterminated_string() {
2221 let err = parse(r#"RECALL episodic ABOUT "unterminated"#).unwrap_err();
2222 assert!(err.line >= 1);
2223 assert!(err.column >= 1);
2224 }
2225
2226 #[test]
2227 fn parse_case_insensitive() {
2228 let q1 = parse(r#"recall episodic about "test""#).unwrap();
2229 let q2 = parse(r#"RECALL EPISODIC ABOUT "test""#).unwrap();
2230 let q3 = parse(r#"Recall Episodic About "test""#).unwrap();
2231 assert_eq!(q1, q2);
2232 assert_eq!(q2, q3);
2233 }
2234
2235 #[test]
2236 fn parse_with_comments() {
2237 let q = "-- this is a comment\nRECALL episodic ABOUT \"test\"";
2238 let stmt = parse(q).unwrap();
2239 assert!(matches!(stmt, Statement::Recall(_)));
2240 }
2241
2242 #[test]
2243 fn parse_single_quoted_strings() {
2244 let q = "RECALL episodic ABOUT 'test query'";
2245 let stmt = parse(q).unwrap();
2246 match stmt {
2247 Statement::Recall(r) => assert_eq!(r.about, "test query"),
2248 other => panic!("expected Recall, got {other:?}"),
2249 }
2250 }
2251
2252 #[test]
2253 fn parse_multiline_query() {
2254 let q = "RECALL episodic\n ABOUT \"test\"\n LIMIT 10";
2255 let stmt = parse(q).unwrap();
2256 match stmt {
2257 Statement::Recall(r) => {
2258 assert_eq!(r.about, "test");
2259 assert_eq!(r.limit, Some(10));
2260 }
2261 other => panic!("expected Recall, got {other:?}"),
2262 }
2263 }
2264
2265 #[test]
2266 fn parse_empty_input() {
2267 let err = parse("").unwrap_err();
2268 assert!(err.line >= 1);
2269 }
2270
2271 #[test]
2272 fn parse_between_temporal() {
2273 let q = r#"RECALL episodic ABOUT "test" BETWEEN "2026-03-01" AND "2026-03-15""#;
2274 let stmt = parse(q).unwrap();
2275 match stmt {
2276 Statement::Recall(r) => {
2277 assert_eq!(
2278 r.temporal,
2279 Some(TemporalClause::Between {
2280 start: "2026-03-01".into(),
2281 end: "2026-03-15".into(),
2282 })
2283 );
2284 }
2285 other => panic!("expected Recall, got {other:?}"),
2286 }
2287 }
2288
2289 #[test]
2290 fn parse_concept_9_4_semantic_search() {
2291 let q = r#"
2292 RECALL semantic, episodic
2293 ABOUT "vector database optimization"
2294 EXPAND GRAPH DEPTH 2 MIN_WEIGHT 0.3 ACTIVATION spreading
2295 WHERE importance > 0.4
2296 LIMIT 20
2297 "#;
2298 let stmt = parse(q).unwrap();
2299 assert!(matches!(stmt, Statement::Recall(_)));
2300 }
2301
2302 #[test]
2303 fn parse_concept_9_4_temporal_narrative() {
2304 let q = r#"
2307 RECALL episodic
2308 ABOUT "deployment and production events"
2309 INVOLVING "deployment", "production"
2310 BETWEEN "2026-03-01" AND "2026-03-15"
2311 AS NARRATIVE
2312 "#;
2313 let stmt = parse(q).unwrap();
2314 assert!(matches!(stmt, Statement::Recall(_)));
2315 }
2316
2317 #[test]
2318 fn parse_concept_9_4_causal_chain() {
2319 let q = r#"
2320 RECALL episodic
2321 ABOUT "production outage"
2322 FOLLOW CAUSES DEPTH 3
2323 AS CAUSAL_CHAIN
2324 "#;
2325 let stmt = parse(q).unwrap();
2326 match stmt {
2327 Statement::Recall(r) => {
2328 assert_eq!(r.follow_causes, Some(3));
2329 assert_eq!(r.output_format, Some(OutputFormat::CausalChain));
2330 }
2331 other => panic!("expected Recall, got {other:?}"),
2332 }
2333 }
2334
2335 #[test]
2336 fn parse_concept_9_4_think() {
2337 let q = r#"
2338 THINK
2339 ABOUT "How should I optimize HNSW for high-dimensional embeddings?"
2340 EXPAND GRAPH DEPTH 2 ACTIVATION spreading
2341 BUDGET 4096
2342 "#;
2343 let stmt = parse(q).unwrap();
2344 match stmt {
2345 Statement::Think(t) => {
2346 assert_eq!(t.budget, Some(4096));
2347 assert!(t.expand.is_some());
2348 }
2349 other => panic!("expected Think, got {other:?}"),
2350 }
2351 }
2352
2353 #[test]
2354 fn parse_concept_9_4_connect_is_unsupported() {
2355 let q = r#"
2356 CONNECT "HNSW_indexing" TO "approximate_nearest_neighbors"
2357 AS related_to
2358 WEIGHT 0.9
2359 "#;
2360 let err = parse(q).unwrap_err();
2361 assert!(err.message.contains("CONNECT is not supported"));
2362 }
2363
2364 #[test]
2365 fn parse_concept_9_4_trace() {
2366 let q = r#"TRACE "semantic:caching_best_practices""#;
2367 let stmt = parse(q).unwrap();
2368 assert!(matches!(stmt, Statement::Trace(_)));
2369 }
2370
2371 #[test]
2372 fn parse_concept_9_4_cross_agent() {
2373 let q = r#"
2374 RECALL semantic
2375 ABOUT "API rate limiting patterns"
2376 WHERE confidence > 0.8
2377 NAMESPACE shared_knowledge
2378 "#;
2379 let stmt = parse(q).unwrap();
2380 match stmt {
2381 Statement::Recall(r) => {
2382 assert_eq!(r.namespace, Some("shared_knowledge".into()));
2383 }
2384 other => panic!("expected Recall, got {other:?}"),
2385 }
2386 }
2387
2388 #[test]
2389 fn parse_concept_9_4_consistency() {
2390 let q = r#"
2391 RECALL semantic
2392 ABOUT "compliance rules"
2393 CONSISTENCY linearizable
2394 "#;
2395 let stmt = parse(q).unwrap();
2396 match stmt {
2397 Statement::Recall(r) => {
2398 assert_eq!(r.consistency, Some(ConsistencyLevel::Linearizable));
2399 }
2400 other => panic!("expected Recall, got {other:?}"),
2401 }
2402 }
2403
2404 #[test]
2405 fn roundtrip_recall() {
2406 let q = r#"RECALL episodic ABOUT "test" LIMIT 10"#;
2407 let stmt1 = parse(q).unwrap();
2408 let rendered = stmt1.to_string();
2409 let stmt2 = parse(&rendered).unwrap();
2410 assert_eq!(stmt1, stmt2);
2411 }
2412
2413 #[test]
2414 fn roundtrip_think() {
2415 let q = r#"THINK ABOUT "optimize queries" BUDGET 4096 LIMIT 5"#;
2416 let stmt1 = parse(q).unwrap();
2417 let rendered = stmt1.to_string();
2418 let stmt2 = parse(&rendered).unwrap();
2419 assert_eq!(stmt1, stmt2);
2420 }
2421
2422 #[test]
2423 fn roundtrip_history() {
2424 let q = r#"HISTORY "id" NAMESPACE custom"#;
2425 let stmt1 = parse(q).unwrap();
2426 let rendered = stmt1.to_string();
2427 let stmt2 = parse(&rendered).unwrap();
2428 assert_eq!(stmt1, stmt2);
2429 }
2430
2431 #[test]
2432 fn roundtrip_inspect() {
2433 let q = r#"INSPECT "id""#;
2434 let stmt1 = parse(q).unwrap();
2435 let rendered = stmt1.to_string();
2436 let stmt2 = parse(&rendered).unwrap();
2437 assert_eq!(stmt1, stmt2);
2438 }
2439
2440 #[test]
2441 fn roundtrip_trace() {
2442 let q = r#"TRACE "id""#;
2443 let stmt1 = parse(q).unwrap();
2444 let rendered = stmt1.to_string();
2445 let stmt2 = parse(&rendered).unwrap();
2446 assert_eq!(stmt1, stmt2);
2447 }
2448
2449 #[test]
2450 fn roundtrip_full_recall() {
2451 let q = r#"RECALL semantic, episodic ABOUT "optimization" INVOLVING "HNSW" AFTER "2026-03-01" EXPAND GRAPH DEPTH 2 MIN_WEIGHT 0.3 ACTIVATION spreading WHERE importance > 0.4 BUDGET 4096 NAMESPACE test LIMIT 20"#;
2452 let stmt1 = parse(q).unwrap();
2453 let rendered = stmt1.to_string();
2454 let stmt2 = parse(&rendered).unwrap();
2455 assert_eq!(stmt1, stmt2);
2456 }
2457
2458 #[test]
2459 fn fuzz_no_panics() {
2460 let inputs = [
2462 "",
2463 " ",
2464 "\n\n",
2465 "SELECT * FROM x",
2466 "RECALL",
2467 "RECALL episodic",
2468 "RECALL episodic ABOUT",
2469 "RECALL ABOUT \"x\"",
2470 "THINK",
2471 "REMEMBER",
2472 "FORGET",
2473 "CONNECT",
2474 "INSPECT",
2475 "HISTORY",
2476 "TRACE",
2477 "CONSOLIDATE",
2478 "😀 unicode",
2479 "RECALL episodic ABOUT \"x\" LIMIT -1",
2480 "RECALL episodic ABOUT \"x\" LIMIT 999999999999",
2481 ];
2482 let long_input = "A".repeat(10000);
2483 let mut inputs_vec: Vec<&str> = inputs.to_vec();
2484 inputs_vec.push(long_input.as_str());
2485 for input in inputs_vec {
2486 let _ = parse(input); }
2488 }
2489
2490 #[test]
2493 fn parse_group_by_count() {
2494 let stmt = parse(r#"RECALL episodic ABOUT "test" GROUP BY entity_type COUNT"#).unwrap();
2495 match stmt {
2496 Statement::Recall(r) => {
2497 let gb = r.group_by.unwrap();
2498 assert_eq!(gb.field, "entity_type");
2499 assert_eq!(gb.function, AggFunction::Count);
2500 }
2501 _ => panic!("expected Recall"),
2502 }
2503 }
2504
2505 #[test]
2506 fn parse_group_by_avg() {
2507 let stmt = parse(r#"RECALL episodic ABOUT "test" GROUP BY importance AVG"#).unwrap();
2508 match stmt {
2509 Statement::Recall(r) => {
2510 let gb = r.group_by.unwrap();
2511 assert_eq!(gb.field, "importance");
2512 assert_eq!(gb.function, AggFunction::Avg);
2513 }
2514 _ => panic!("expected Recall"),
2515 }
2516 }
2517
2518 #[test]
2519 fn parse_select_projection() {
2520 let stmt = parse(r#"RECALL episodic ABOUT "test" SELECT id, summary, importance"#).unwrap();
2521 match stmt {
2522 Statement::Recall(r) => {
2523 let proj = r.projection.unwrap();
2524 assert_eq!(proj, vec!["id", "summary", "importance"]);
2525 }
2526 _ => panic!("expected Recall"),
2527 }
2528 }
2529
2530 #[test]
2531 fn parse_format_json() {
2532 let stmt = parse(r#"RECALL episodic ABOUT "test" FORMAT json"#).unwrap();
2533 match stmt {
2534 Statement::Recall(r) => {
2535 assert_eq!(r.result_format.unwrap(), OutputFormat::Json);
2536 }
2537 _ => panic!("expected Recall"),
2538 }
2539 }
2540
2541 #[test]
2542 fn parse_format_csv() {
2543 let stmt = parse(r#"RECALL episodic ABOUT "test" FORMAT csv"#).unwrap();
2544 match stmt {
2545 Statement::Recall(r) => {
2546 assert_eq!(r.result_format.unwrap(), OutputFormat::Csv);
2547 }
2548 _ => panic!("expected Recall"),
2549 }
2550 }
2551
2552 #[test]
2553 fn parse_group_by_with_no_results_still_parses() {
2554 let stmt =
2556 parse(r#"RECALL episodic ABOUT "nonexistent" GROUP BY entity_type COUNT LIMIT 0"#)
2557 .unwrap();
2558 match stmt {
2559 Statement::Recall(r) => {
2560 assert!(r.group_by.is_some());
2561 assert_eq!(r.limit, Some(0));
2562 }
2563 _ => panic!("expected Recall"),
2564 }
2565 }
2566
2567 #[test]
2568 fn parse_select_single_field() {
2569 let stmt = parse(r#"RECALL episodic ABOUT "test" SELECT id"#).unwrap();
2570 match stmt {
2571 Statement::Recall(r) => {
2572 assert_eq!(r.projection.unwrap(), vec!["id"]);
2573 }
2574 _ => panic!("expected Recall"),
2575 }
2576 }
2577
2578 #[test]
2579 fn parse_combined_group_by_and_format() {
2580 let stmt = parse(
2581 r#"RECALL episodic ABOUT "test" GROUP BY entity_type COUNT FORMAT json LIMIT 10"#,
2582 )
2583 .unwrap();
2584 match stmt {
2585 Statement::Recall(r) => {
2586 assert!(r.group_by.is_some());
2587 assert_eq!(r.result_format.unwrap(), OutputFormat::Json);
2588 assert_eq!(r.limit, Some(10));
2589 }
2590 _ => panic!("expected Recall"),
2591 }
2592 }
2593
2594 #[test]
2597 fn parse_as_of_clause() {
2598 let stmt =
2599 parse(r#"RECALL episodic ABOUT "deployment" AS OF "2026-03-01T12:00:00Z" LIMIT 5"#)
2600 .unwrap();
2601 match stmt {
2602 Statement::Recall(r) => {
2603 assert_eq!(
2604 r.as_of.unwrap(),
2605 RecallSnapshotAst::Unqualified("2026-03-01T12:00:00Z".to_string())
2606 );
2607 assert_eq!(r.limit, Some(5));
2608 }
2609 _ => panic!("expected Recall"),
2610 }
2611 }
2612
2613 #[test]
2614 fn parse_explicit_observed_as_of_clause() {
2615 let stmt = parse(
2616 r#"RECALL episodic ABOUT "deployment" AS OF OBSERVED "2026-03-01T12:00:00Z" LIMIT 5"#,
2617 )
2618 .unwrap();
2619 match stmt {
2620 Statement::Recall(r) => {
2621 assert_eq!(
2622 r.as_of.unwrap(),
2623 RecallSnapshotAst::Observed("2026-03-01T12:00:00Z".to_string())
2624 );
2625 assert_eq!(r.limit, Some(5));
2626 }
2627 _ => panic!("expected Recall"),
2628 }
2629 }
2630
2631 #[test]
2632 fn parse_recorded_as_of_clause() {
2633 let stmt = parse(
2634 r#"RECALL episodic ABOUT "deployment" AS OF RECORDED "2026-03-01T12:00:00Z" LIMIT 5"#,
2635 )
2636 .unwrap();
2637 match stmt {
2638 Statement::Recall(r) => {
2639 assert_eq!(
2640 r.as_of.unwrap(),
2641 RecallSnapshotAst::Recorded("2026-03-01T12:00:00Z".to_string())
2642 );
2643 assert_eq!(r.limit, Some(5));
2644 }
2645 _ => panic!("expected Recall"),
2646 }
2647 }
2648
2649 #[test]
2650 fn parse_revision_as_of_clause() {
2651 let stmt = parse(
2652 r#"RECALL semantic ABOUT "deployment" AS OF REVISION "01HW7N0Z5CH9R1R7Z4S4V5Y4QF" LIMIT 5"#,
2653 )
2654 .unwrap();
2655 match stmt {
2656 Statement::Recall(r) => {
2657 assert_eq!(
2658 r.as_of.unwrap(),
2659 RecallSnapshotAst::Revision("01HW7N0Z5CH9R1R7Z4S4V5Y4QF".to_string())
2660 );
2661 assert_eq!(r.limit, Some(5));
2662 }
2663 _ => panic!("expected Recall"),
2664 }
2665 }
2666
2667 #[test]
2668 fn parse_in_subquery() {
2669 let stmt = parse(
2670 r#"RECALL episodic ABOUT "outage" WHERE entity IN (RECALL semantic ABOUT "critical services") LIMIT 10"#,
2671 )
2672 .unwrap();
2673 match stmt {
2674 Statement::Recall(r) => {
2675 assert_eq!(r.subquery_filters.len(), 1);
2676 assert_eq!(r.subquery_filters[0].field, "entity");
2677 assert_eq!(r.subquery_filters[0].subquery.about, "critical services");
2678 assert_eq!(r.subquery_filters[0].subquery.layers, vec![Layer::Semantic]);
2679 assert_eq!(r.limit, Some(10));
2680 }
2681 _ => panic!("expected Recall"),
2682 }
2683 }
2684
2685 #[test]
2686 fn parse_subquery_with_temporal() {
2687 let stmt = parse(
2688 r#"RECALL episodic ABOUT "bugs" WHERE entity IN (RECALL episodic ABOUT "releases" AFTER "2026-01-01" LIMIT 5)"#,
2689 )
2690 .unwrap();
2691 match stmt {
2692 Statement::Recall(r) => {
2693 assert_eq!(r.subquery_filters.len(), 1);
2694 let sq = &r.subquery_filters[0].subquery;
2695 assert_eq!(sq.about, "releases");
2696 assert!(sq.temporal.is_some());
2697 assert_eq!(sq.limit, Some(5));
2698 }
2699 _ => panic!("expected Recall"),
2700 }
2701 }
2702
2703 #[test]
2704 fn parse_subquery_with_involving() {
2705 let stmt = parse(
2706 r#"RECALL semantic ABOUT "patterns" WHERE entity IN (RECALL episodic ABOUT "events" INVOLVING "auth", "db")"#,
2707 )
2708 .unwrap();
2709 match stmt {
2710 Statement::Recall(r) => {
2711 assert_eq!(r.subquery_filters.len(), 1);
2712 let sq = &r.subquery_filters[0].subquery;
2713 assert_eq!(
2714 sq.involving.as_ref().unwrap(),
2715 &vec!["auth".to_string(), "db".to_string()]
2716 );
2717 }
2718 _ => panic!("expected Recall"),
2719 }
2720 }
2721
2722 #[test]
2723 fn parse_as_of_with_where() {
2724 let stmt =
2725 parse(r#"RECALL episodic ABOUT "events" AS OF "2026-06-01" WHERE importance > 0.5"#)
2726 .unwrap();
2727 match stmt {
2728 Statement::Recall(r) => {
2729 assert_eq!(
2730 r.as_of.unwrap(),
2731 RecallSnapshotAst::Unqualified("2026-06-01".to_string())
2732 );
2733 assert_eq!(r.where_clauses.len(), 1);
2734 assert_eq!(r.where_clauses[0].field, "importance");
2735 }
2736 _ => panic!("expected Recall"),
2737 }
2738 }
2739
2740 #[test]
2741 fn parse_where_with_both_condition_and_subquery() {
2742 let stmt = parse(
2743 r#"RECALL episodic ABOUT "test" WHERE importance > 0.5 WHERE entity IN (RECALL semantic ABOUT "services")"#,
2744 )
2745 .unwrap();
2746 match stmt {
2747 Statement::Recall(r) => {
2748 assert_eq!(r.where_clauses.len(), 1);
2749 assert_eq!(r.subquery_filters.len(), 1);
2750 }
2751 _ => panic!("expected Recall"),
2752 }
2753 }
2754
2755 #[test]
2758 fn parse_traverse_minimal() {
2759 let stmt = parse(r#"TRAVERSE FROM "node1" DEPTH 3"#).unwrap();
2760 match stmt {
2761 Statement::Traverse(t) => {
2762 assert_eq!(t.from, "node1");
2763 assert_eq!(t.depth, 3);
2764 assert!(t.via.is_none());
2765 assert!(t.where_clauses.is_empty());
2766 assert!(t.limit.is_none());
2767 }
2768 other => panic!("expected Traverse, got {other:?}"),
2769 }
2770 }
2771
2772 #[test]
2773 fn parse_traverse_with_via() {
2774 let stmt =
2775 parse(r#"TRAVERSE FROM "concept_a" VIA causes, related_to DEPTH 2 LIMIT 10"#).unwrap();
2776 match stmt {
2777 Statement::Traverse(t) => {
2778 assert_eq!(t.from, "concept_a");
2779 assert_eq!(t.via.as_ref().unwrap(), &["causes", "related_to"]);
2780 assert_eq!(t.depth, 2);
2781 assert_eq!(t.limit, Some(10));
2782 }
2783 other => panic!("expected Traverse, got {other:?}"),
2784 }
2785 }
2786
2787 #[test]
2788 fn parse_traverse_with_where() {
2789 let stmt =
2790 parse(r#"TRAVERSE FROM "root" DEPTH 5 WHERE weight > 0.5 WHERE confidence > 0.3"#)
2791 .unwrap();
2792 match stmt {
2793 Statement::Traverse(t) => {
2794 assert_eq!(t.depth, 5);
2795 assert_eq!(t.where_clauses.len(), 2);
2796 assert_eq!(t.where_clauses[0].field, "weight");
2797 assert_eq!(t.where_clauses[1].field, "confidence");
2798 }
2799 other => panic!("expected Traverse, got {other:?}"),
2800 }
2801 }
2802
2803 #[test]
2804 fn roundtrip_traverse() {
2805 let q = r#"TRAVERSE FROM "node1" VIA causes DEPTH 3 LIMIT 10"#;
2806 let stmt1 = parse(q).unwrap();
2807 let rendered = stmt1.to_string();
2808 let stmt2 = parse(&rendered).unwrap();
2809 assert_eq!(stmt1, stmt2);
2810 }
2811
2812 #[test]
2815 fn parse_positional_param_in_about() {
2816 let stmt = parse(r#"RECALL episodic ABOUT $1 LIMIT 10"#).unwrap();
2817 match stmt {
2818 Statement::Recall(r) => assert_eq!(r.about, "$1"),
2819 _ => panic!("expected Recall"),
2820 }
2821 }
2822
2823 #[test]
2824 fn parse_named_param_in_about() {
2825 let stmt = parse(r#"RECALL episodic ABOUT $query LIMIT 5"#).unwrap();
2826 match stmt {
2827 Statement::Recall(r) => assert_eq!(r.about, "$query"),
2828 _ => panic!("expected Recall"),
2829 }
2830 }
2831
2832 #[test]
2833 fn parse_param_in_where_condition() {
2834 let stmt = parse(r#"RECALL episodic ABOUT "test" WHERE importance > $threshold"#).unwrap();
2835 match stmt {
2836 Statement::Recall(r) => {
2837 assert_eq!(r.where_clauses.len(), 1);
2838 assert_eq!(
2839 r.where_clauses[0].value,
2840 ConditionValue::Param("$threshold".into())
2841 );
2842 }
2843 _ => panic!("expected Recall"),
2844 }
2845 }
2846
2847 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
2850 enum ExpectedExplainInner {
2851 Recall,
2852 RecallEvents,
2853 Think,
2854 Correct,
2855 Supersede,
2856 MergeMemory,
2857 Retract,
2858 History,
2859 Traverse,
2860 Inspect,
2861 Trace,
2862 ExplainCauses,
2863 WhatIf,
2864 Counterfactual,
2865 ShowPolicies,
2866 ExplainPolicy,
2867 }
2868
2869 fn explain_inner_matches(statement: &Statement, expected: ExpectedExplainInner) -> bool {
2870 match expected {
2871 ExpectedExplainInner::Recall => matches!(statement, Statement::Recall(_)),
2872 ExpectedExplainInner::RecallEvents => matches!(statement, Statement::RecallEvents(_)),
2873 ExpectedExplainInner::Think => matches!(statement, Statement::Think(_)),
2874 ExpectedExplainInner::Correct => matches!(statement, Statement::Correct(_)),
2875 ExpectedExplainInner::Supersede => matches!(statement, Statement::Supersede(_)),
2876 ExpectedExplainInner::MergeMemory => matches!(statement, Statement::MergeMemory(_)),
2877 ExpectedExplainInner::Retract => matches!(statement, Statement::Retract(_)),
2878 ExpectedExplainInner::History => matches!(statement, Statement::History(_)),
2879 ExpectedExplainInner::Traverse => matches!(statement, Statement::Traverse(_)),
2880 ExpectedExplainInner::Inspect => matches!(statement, Statement::Inspect(_)),
2881 ExpectedExplainInner::Trace => matches!(statement, Statement::Trace(_)),
2882 ExpectedExplainInner::ExplainCauses => {
2883 matches!(statement, Statement::ExplainCauses(_))
2884 }
2885 ExpectedExplainInner::WhatIf => matches!(statement, Statement::WhatIf(_)),
2886 ExpectedExplainInner::Counterfactual => {
2887 matches!(statement, Statement::Counterfactual(_))
2888 }
2889 ExpectedExplainInner::ShowPolicies => matches!(statement, Statement::ShowPolicies(_)),
2890 ExpectedExplainInner::ExplainPolicy => {
2891 matches!(statement, Statement::ExplainPolicy(_))
2892 }
2893 }
2894 }
2895
2896 fn assert_explain_shape(
2897 query: &str,
2898 expected_analyze: bool,
2899 expected_inner: ExpectedExplainInner,
2900 ) {
2901 let stmt = parse(query).unwrap();
2902 match stmt {
2903 Statement::Explain(e) => {
2904 assert_eq!(
2905 e.analyze, expected_analyze,
2906 "unexpected analyze flag for `{query}`"
2907 );
2908 assert!(
2909 explain_inner_matches(e.inner.as_ref(), expected_inner),
2910 "unexpected inner statement for `{query}`: {:?}",
2911 e.inner
2912 );
2913 }
2914 other => panic!("expected Explain, got {other:?} for `{query}`"),
2915 }
2916 }
2917
2918 #[test]
2919 fn parse_explain_statement_matrix() {
2920 for (query, expected_inner) in [
2921 (
2922 r#"EXPLAIN RECALL episodic ABOUT "test""#,
2923 ExpectedExplainInner::Recall,
2924 ),
2925 (
2926 r#"EXPLAIN RECALL EVENTS LIMIT 10"#,
2927 ExpectedExplainInner::RecallEvents,
2928 ),
2929 (
2930 r#"EXPLAIN THINK ABOUT "reasoning""#,
2931 ExpectedExplainInner::Think,
2932 ),
2933 (
2934 r#"EXPLAIN CORRECT "01ARZ3NDEKTSV4RRFFQ69G5FAV" SET description = "updated""#,
2935 ExpectedExplainInner::Correct,
2936 ),
2937 (
2938 r#"EXPLAIN SUPERSEDE "01ARZ3NDEKTSV4RRFFQ69G5FAV" SET description = "replacement""#,
2939 ExpectedExplainInner::Supersede,
2940 ),
2941 (
2942 r#"EXPLAIN RETRACT "01ARZ3NDEKTSV4RRFFQ69G5FAV" REASON "obsolete""#,
2943 ExpectedExplainInner::Retract,
2944 ),
2945 (
2946 r#"EXPLAIN HISTORY "01ARZ3NDEKTSV4RRFFQ69G5FAV" NAMESPACE custom"#,
2947 ExpectedExplainInner::History,
2948 ),
2949 (
2950 r#"EXPLAIN TRAVERSE FROM "01ARZ3NDEKTSV4RRFFQ69G5FAV" DEPTH 3"#,
2951 ExpectedExplainInner::Traverse,
2952 ),
2953 (
2954 r#"EXPLAIN INSPECT LOGICAL "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
2955 ExpectedExplainInner::Inspect,
2956 ),
2957 (
2958 r#"EXPLAIN TRACE LOGICAL "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
2959 ExpectedExplainInner::Trace,
2960 ),
2961 (
2962 r#"EXPLAIN EXPLAIN CAUSES "incident" DEPTH 2"#,
2963 ExpectedExplainInner::ExplainCauses,
2964 ),
2965 (
2966 r#"EXPLAIN WHAT_IF "increase timeout" THEN "fewer errors""#,
2967 ExpectedExplainInner::WhatIf,
2968 ),
2969 (
2970 r#"EXPLAIN COUNTERFACTUAL "cause" THEN "effect""#,
2971 ExpectedExplainInner::Counterfactual,
2972 ),
2973 (
2974 r#"EXPLAIN SHOW POLICIES FOR AGENT "agent-007""#,
2975 ExpectedExplainInner::ShowPolicies,
2976 ),
2977 (
2978 r#"EXPLAIN EXPLAIN POLICY FOR AGENT "agent-007" ON NAMESPACE "default" ACTION recall"#,
2979 ExpectedExplainInner::ExplainPolicy,
2980 ),
2981 ] {
2982 assert_explain_shape(query, false, expected_inner);
2983 }
2984 }
2985
2986 #[test]
2987 fn parse_explain_analyze_statement_matrix() {
2988 for (query, expected_inner) in [
2989 (
2990 r#"EXPLAIN ANALYZE RECALL episodic ABOUT "test""#,
2991 ExpectedExplainInner::Recall,
2992 ),
2993 (
2994 r#"EXPLAIN ANALYZE RECALL EVENTS LIMIT 10"#,
2995 ExpectedExplainInner::RecallEvents,
2996 ),
2997 (
2998 r#"EXPLAIN ANALYZE THINK ABOUT "reasoning""#,
2999 ExpectedExplainInner::Think,
3000 ),
3001 (
3002 r#"EXPLAIN ANALYZE CORRECT "01ARZ3NDEKTSV4RRFFQ69G5FAV" SET description = "updated""#,
3003 ExpectedExplainInner::Correct,
3004 ),
3005 (
3006 r#"EXPLAIN ANALYZE SUPERSEDE "01ARZ3NDEKTSV4RRFFQ69G5FAV" SET description = "replacement""#,
3007 ExpectedExplainInner::Supersede,
3008 ),
3009 (
3010 r#"EXPLAIN ANALYZE MERGE MEMORY "01ARZ3NDEKTSV4RRFFQ69G5FAA" INTO "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
3011 ExpectedExplainInner::MergeMemory,
3012 ),
3013 (
3014 r#"EXPLAIN ANALYZE RETRACT "01ARZ3NDEKTSV4RRFFQ69G5FAV" REASON "obsolete""#,
3015 ExpectedExplainInner::Retract,
3016 ),
3017 (
3018 r#"EXPLAIN ANALYZE HISTORY "01ARZ3NDEKTSV4RRFFQ69G5FAV" NAMESPACE custom"#,
3019 ExpectedExplainInner::History,
3020 ),
3021 (
3022 r#"EXPLAIN ANALYZE TRAVERSE FROM "01ARZ3NDEKTSV4RRFFQ69G5FAV" DEPTH 3"#,
3023 ExpectedExplainInner::Traverse,
3024 ),
3025 (
3026 r#"EXPLAIN ANALYZE INSPECT LOGICAL "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
3027 ExpectedExplainInner::Inspect,
3028 ),
3029 (
3030 r#"EXPLAIN ANALYZE TRACE LOGICAL "01ARZ3NDEKTSV4RRFFQ69G5FAV""#,
3031 ExpectedExplainInner::Trace,
3032 ),
3033 (
3034 r#"EXPLAIN ANALYZE EXPLAIN CAUSES "incident" DEPTH 2"#,
3035 ExpectedExplainInner::ExplainCauses,
3036 ),
3037 (
3038 r#"EXPLAIN ANALYZE WHAT_IF "increase timeout" THEN "fewer errors""#,
3039 ExpectedExplainInner::WhatIf,
3040 ),
3041 (
3042 r#"EXPLAIN ANALYZE COUNTERFACTUAL "cause" THEN "effect""#,
3043 ExpectedExplainInner::Counterfactual,
3044 ),
3045 (
3046 r#"EXPLAIN ANALYZE SHOW POLICIES FOR AGENT "agent-007""#,
3047 ExpectedExplainInner::ShowPolicies,
3048 ),
3049 (
3050 r#"EXPLAIN ANALYZE EXPLAIN POLICY FOR AGENT "agent-007" ON NAMESPACE "default" ACTION recall"#,
3051 ExpectedExplainInner::ExplainPolicy,
3052 ),
3053 ] {
3054 assert_explain_shape(query, true, expected_inner);
3055 }
3056 }
3057
3058 #[test]
3059 fn parse_explain_rejects_non_wrappable_statement_classes() {
3060 for prefix in ["EXPLAIN", "EXPLAIN ANALYZE"] {
3061 for inner in [
3062 r#"REMEMBER episode CONTENT "data""#,
3063 "WATCH ALL FORMAT json",
3064 r#"CONNECT "01ARZ3NDEKTSV4RRFFQ69G5FAV" TO "01ARZ3NDEKTSV4RRFFQ69G5FAA" AS causes"#,
3065 r#"GRANT recall ON NAMESPACE "default" TO AGENT "agent-007""#,
3066 r#"REVOKE forget ON NAMESPACE "sensitive" FROM AGENT "rogue""#,
3067 "SET TIER_POLICY semantic_archive_threshold = 0.2",
3068 r#"CONSOLIDATE WHERE episodic.access_count > 5"#,
3069 r#"CREATE REALM "analytics""#,
3070 r#"DROP REALM "analytics" CONFIRM"#,
3071 "SHOW CLUSTER",
3072 ] {
3073 let query = format!("{prefix} {inner}");
3074 assert!(
3075 parse(&query).is_err(),
3076 "`{query}` should be rejected by the EXPLAIN wrapper allowlist"
3077 );
3078 }
3079 }
3080 }
3081
3082 #[test]
3083 fn parse_modality_filter() {
3084 let stmt = parse(r#"RECALL episodic ABOUT "login page" MODALITY image, text"#).unwrap();
3085 match stmt {
3086 Statement::Recall(r) => {
3087 assert_eq!(r.about, "login page");
3088 let mods = r.modality.unwrap();
3089 assert_eq!(mods, vec!["image".to_string(), "text".to_string()]);
3090 }
3091 _ => panic!("expected Recall"),
3092 }
3093 }
3094
3095 #[test]
3096 fn parse_modality_single() {
3097 let stmt = parse(r#"RECALL episodic ABOUT "diagrams" MODALITY image"#).unwrap();
3098 match stmt {
3099 Statement::Recall(r) => {
3100 let mods = r.modality.unwrap();
3101 assert_eq!(mods, vec!["image".to_string()]);
3102 }
3103 _ => panic!("expected Recall"),
3104 }
3105 }
3106
3107 #[test]
3108 fn parse_recall_without_modality_is_none() {
3109 let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3110 match stmt {
3111 Statement::Recall(r) => {
3112 assert!(r.modality.is_none());
3113 }
3114 _ => panic!("expected Recall"),
3115 }
3116 }
3117
3118 #[test]
3119 fn parse_modality_with_other_clauses() {
3120 let stmt = parse(
3121 r#"RECALL episodic ABOUT "query" WHERE importance > 0.5 MODALITY code, text LIMIT 10"#,
3122 )
3123 .unwrap();
3124 match stmt {
3125 Statement::Recall(r) => {
3126 let mods = r.modality.unwrap();
3127 assert_eq!(mods, vec!["code".to_string(), "text".to_string()]);
3128 assert_eq!(r.limit, Some(10));
3129 assert_eq!(r.where_clauses.len(), 1);
3130 }
3131 _ => panic!("expected Recall"),
3132 }
3133 }
3134
3135 #[test]
3136 fn parse_modality_extended_profiles() {
3137 let stmt = parse(
3138 r#"RECALL episodic ABOUT "artifact" MODALITY video, document, composite, external LIMIT 10"#,
3139 )
3140 .unwrap();
3141 match stmt {
3142 Statement::Recall(r) => {
3143 let mods = r.modality.unwrap();
3144 assert_eq!(
3145 mods,
3146 vec![
3147 "video".to_string(),
3148 "document".to_string(),
3149 "composite".to_string(),
3150 "external".to_string(),
3151 ]
3152 );
3153 }
3154 _ => panic!("expected Recall"),
3155 }
3156 }
3157
3158 #[test]
3159 fn parse_resource_aware_recall_clauses() {
3160 let stmt = parse(
3161 r#"RECALL episodic ABOUT "artifact" MODALITY image RESOURCE_ROLE source, proof HYDRATION preview, full ARTIFACT preview, caption LIMIT 5"#,
3162 )
3163 .unwrap();
3164 match stmt {
3165 Statement::Recall(r) => {
3166 assert_eq!(r.modality.unwrap(), vec!["image".to_string()]);
3167 assert_eq!(
3168 r.resource_roles.unwrap(),
3169 vec!["source".to_string(), "proof".to_string()]
3170 );
3171 assert_eq!(
3172 r.hydration_modes.unwrap(),
3173 vec!["preview".to_string(), "full".to_string()]
3174 );
3175 assert_eq!(
3176 r.artifact_kinds.unwrap(),
3177 vec!["preview".to_string(), "caption".to_string()]
3178 );
3179 }
3180 _ => panic!("expected Recall"),
3181 }
3182 }
3183
3184 #[test]
3185 fn parse_recall_without_resource_aware_clauses_is_none() {
3186 let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3187 match stmt {
3188 Statement::Recall(r) => {
3189 assert!(r.resource_roles.is_none());
3190 assert!(r.hydration_modes.is_none());
3191 assert!(r.artifact_kinds.is_none());
3192 }
3193 _ => panic!("expected Recall"),
3194 }
3195 }
3196
3197 #[test]
3200 fn parse_create_realm() {
3201 let stmt = parse(r#"CREATE REALM "analytics""#).unwrap();
3202 match stmt {
3203 Statement::CreateRealm(r) => {
3204 assert_eq!(r.name, "analytics");
3205 assert!(r.description.is_none());
3206 }
3207 _ => panic!("expected CreateRealm"),
3208 }
3209 }
3210
3211 #[test]
3212 fn parse_create_realm_with_description() {
3213 let stmt = parse(r#"CREATE REALM "analytics" DESCRIPTION "For analytics data""#).unwrap();
3214 match stmt {
3215 Statement::CreateRealm(r) => {
3216 assert_eq!(r.name, "analytics");
3217 assert_eq!(r.description.as_deref(), Some("For analytics data"));
3218 }
3219 _ => panic!("expected CreateRealm"),
3220 }
3221 }
3222
3223 #[test]
3224 fn parse_drop_realm() {
3225 let stmt = parse(r#"DROP REALM "analytics""#).unwrap();
3226 match stmt {
3227 Statement::DropRealm(r) => {
3228 assert_eq!(r.name, "analytics");
3229 assert!(!r.confirm);
3230 }
3231 _ => panic!("expected DropRealm"),
3232 }
3233 }
3234
3235 #[test]
3236 fn parse_drop_realm_confirm() {
3237 let stmt = parse(r#"DROP REALM "analytics" CONFIRM"#).unwrap();
3238 match stmt {
3239 Statement::DropRealm(r) => {
3240 assert_eq!(r.name, "analytics");
3241 assert!(r.confirm);
3242 }
3243 _ => panic!("expected DropRealm"),
3244 }
3245 }
3246
3247 #[test]
3248 fn parse_grant_single_action() {
3249 let stmt = parse(r#"GRANT recall ON NAMESPACE "default" TO AGENT "agent-007""#).unwrap();
3250 match stmt {
3251 Statement::Grant(g) => {
3252 assert_eq!(g.actions, vec!["recall"]);
3253 assert_eq!(g.target, GrantTarget::Namespace("default".into()));
3254 assert_eq!(g.principal, PrincipalRef::Agent("agent-007".into()));
3255 }
3256 _ => panic!("expected Grant"),
3257 }
3258 }
3259
3260 #[test]
3261 fn parse_grant_multiple_actions() {
3262 let stmt =
3263 parse(r#"GRANT recall, remember, think ON REALM "prod" TO TEAM "data-scientists""#)
3264 .unwrap();
3265 match stmt {
3266 Statement::Grant(g) => {
3267 assert_eq!(g.actions, vec!["recall", "remember", "think"]);
3268 assert_eq!(g.target, GrantTarget::Realm("prod".into()));
3269 assert_eq!(g.principal, PrincipalRef::Team("data-scientists".into()));
3270 }
3271 _ => panic!("expected Grant"),
3272 }
3273 }
3274
3275 #[test]
3276 fn parse_revoke() {
3277 let stmt = parse(r#"REVOKE forget ON NAMESPACE "sensitive" FROM AGENT "rogue""#).unwrap();
3278 match stmt {
3279 Statement::Revoke(r) => {
3280 assert_eq!(r.actions, vec!["forget"]);
3281 assert_eq!(r.target, GrantTarget::Namespace("sensitive".into()));
3282 assert_eq!(r.principal, PrincipalRef::Agent("rogue".into()));
3283 }
3284 _ => panic!("expected Revoke"),
3285 }
3286 }
3287
3288 #[test]
3289 fn parse_show_policies() {
3290 let stmt = parse(r#"SHOW POLICIES"#).unwrap();
3291 match stmt {
3292 Statement::ShowPolicies(s) => {
3293 assert!(s.principal.is_none());
3294 }
3295 _ => panic!("expected ShowPolicies"),
3296 }
3297 }
3298
3299 #[test]
3300 fn parse_show_policies_for_agent() {
3301 let stmt = parse(r#"SHOW POLICIES FOR AGENT "agent-007""#).unwrap();
3302 match stmt {
3303 Statement::ShowPolicies(s) => {
3304 assert_eq!(s.principal, Some(PrincipalRef::Agent("agent-007".into())));
3305 }
3306 _ => panic!("expected ShowPolicies"),
3307 }
3308 }
3309
3310 #[test]
3311 fn parse_explain_policy() {
3312 let stmt =
3313 parse(r#"EXPLAIN POLICY FOR AGENT "agent-007" ON NAMESPACE "default" ACTION recall"#)
3314 .unwrap();
3315 match stmt {
3316 Statement::ExplainPolicy(e) => {
3317 assert_eq!(e.principal, PrincipalRef::Agent("agent-007".into()));
3318 assert_eq!(e.resource_type, "namespace");
3319 assert_eq!(e.resource_name, "default");
3320 assert_eq!(e.action, "recall");
3321 }
3322 _ => panic!("expected ExplainPolicy"),
3323 }
3324 }
3325
3326 #[test]
3327 fn parse_explain_policy_on_realm() {
3328 let stmt =
3329 parse(r#"EXPLAIN POLICY FOR TEAM "analysts" ON REALM "prod" ACTION remember"#).unwrap();
3330 match stmt {
3331 Statement::ExplainPolicy(e) => {
3332 assert_eq!(e.principal, PrincipalRef::Team("analysts".into()));
3333 assert_eq!(e.resource_type, "realm");
3334 assert_eq!(e.resource_name, "prod");
3335 assert_eq!(e.action, "remember");
3336 }
3337 _ => panic!("expected ExplainPolicy"),
3338 }
3339 }
3340
3341 #[test]
3342 fn roundtrip_create_realm() {
3343 let stmt = parse(r#"CREATE REALM "test-realm" DESCRIPTION "A test realm""#).unwrap();
3344 let display = stmt.to_string();
3345 assert!(display.contains("CREATE REALM"));
3346 assert!(display.contains("test-realm"));
3347 assert!(display.contains("DESCRIPTION"));
3348 }
3349
3350 #[test]
3351 fn roundtrip_grant() {
3352 let stmt = parse(r#"GRANT recall, think ON NAMESPACE "ns1" TO AGENT "a1""#).unwrap();
3353 let display = stmt.to_string();
3354 assert!(display.contains("GRANT recall, think"));
3355 assert!(display.contains("NAMESPACE"));
3356 assert!(display.contains("AGENT"));
3357 }
3358
3359 #[test]
3360 fn recall_events_basic() {
3361 let stmt = parse(r#"RECALL EVENTS WHERE event_type = "access_denied""#).unwrap();
3362 match stmt {
3363 Statement::RecallEvents(r) => {
3364 assert_eq!(r.where_clauses.len(), 1);
3365 assert_eq!(r.where_clauses[0].field, "event_type");
3366 }
3367 _ => panic!("expected RecallEvents"),
3368 }
3369 }
3370
3371 #[test]
3372 fn recall_events_multiple_filters() {
3373 let stmt = parse(
3374 r#"RECALL EVENTS WHERE agent = "agent-007" WHERE event_type = "access_denied" LIMIT 100"#,
3375 )
3376 .unwrap();
3377 match stmt {
3378 Statement::RecallEvents(r) => {
3379 assert_eq!(r.where_clauses.len(), 2);
3380 assert_eq!(r.limit, Some(100));
3381 }
3382 _ => panic!("expected RecallEvents"),
3383 }
3384 }
3385
3386 #[test]
3387 fn recall_events_with_temporal() {
3388 let stmt = parse(
3389 r#"RECALL EVENTS WHERE event_type = "policy_changed" AFTER "2026-01-01" LIMIT 50"#,
3390 )
3391 .unwrap();
3392 match stmt {
3393 Statement::RecallEvents(r) => {
3394 assert!(r.temporal.is_some());
3395 assert_eq!(r.limit, Some(50));
3396 }
3397 _ => panic!("expected RecallEvents"),
3398 }
3399 }
3400
3401 #[test]
3402 fn roundtrip_recall_events() {
3403 let stmt = parse(
3404 r#"RECALL EVENTS WHERE agent_id = "a1" WHERE event_type = "access_denied" NAMESPACE test LIMIT 10"#,
3405 )
3406 .unwrap();
3407 let display = stmt.to_string();
3408 assert!(display.contains("RECALL EVENTS"));
3409 assert!(display.contains("LIMIT 10"));
3410 }
3411
3412 #[test]
3414 fn parse_show_cluster() {
3415 let stmt = parse("SHOW CLUSTER").unwrap();
3416 assert!(matches!(stmt, Statement::ShowCluster));
3417 }
3418
3419 #[test]
3420 fn parse_show_cluster_status() {
3421 let stmt = parse("SHOW CLUSTER STATUS").unwrap();
3422 assert!(matches!(stmt, Statement::ShowCluster));
3423 }
3424
3425 #[test]
3426 fn parse_show_cluster_case_insensitive() {
3427 let stmt = parse("show cluster").unwrap();
3428 assert!(matches!(stmt, Statement::ShowCluster));
3429 }
3430
3431 #[test]
3432 fn roundtrip_show_cluster() {
3433 let stmt = parse("SHOW CLUSTER").unwrap();
3434 assert_eq!(stmt.to_string(), "SHOW CLUSTER");
3435 }
3436
3437 #[test]
3438 fn parse_recall_hybrid() {
3439 let q = r#"RECALL episodic ABOUT "semantic search" LIMIT 10 HYBRID"#;
3440 let stmt = parse(q).unwrap();
3441 if let Statement::Recall(r) = &stmt {
3442 assert!(r.hybrid);
3443 assert_eq!(r.about, "semantic search");
3444 assert_eq!(r.limit, Some(10));
3445 } else {
3446 panic!("expected Recall");
3447 }
3448 }
3449
3450 #[test]
3451 fn parse_recall_without_hybrid() {
3452 let q = r#"RECALL episodic ABOUT "query" LIMIT 5"#;
3453 let stmt = parse(q).unwrap();
3454 if let Statement::Recall(r) = &stmt {
3455 assert!(!r.hybrid);
3456 } else {
3457 panic!("expected Recall");
3458 }
3459 }
3460
3461 #[test]
3462 fn parse_recall_hybrid_case_insensitive() {
3463 let q = r#"RECALL episodic ABOUT "test" hybrid"#;
3464 let stmt = parse(q).unwrap();
3465 if let Statement::Recall(r) = &stmt {
3466 assert!(r.hybrid);
3467 } else {
3468 panic!("expected Recall");
3469 }
3470 }
3471
3472 #[test]
3473 fn roundtrip_recall_hybrid() {
3474 let q = r#"RECALL episodic ABOUT "hybrid test" LIMIT 10 HYBRID"#;
3475 let stmt1 = parse(q).unwrap();
3476 let rendered = stmt1.to_string();
3477 assert!(rendered.contains("HYBRID"));
3478 let stmt2 = parse(&rendered).unwrap();
3479 assert_eq!(stmt1, stmt2);
3480 }
3481
3482 #[test]
3485 fn invalid_escape_sequence_returns_error() {
3486 let q = r#"RECALL episodic ABOUT "hello\qworld""#;
3487 let result = parse(q);
3488 assert!(result.is_err(), "expected parse error for \\q escape");
3489 }
3490
3491 #[test]
3492 fn valid_escape_newline_succeeds() {
3493 let q = r#"RECALL episodic ABOUT "hello\nworld""#;
3494 let stmt = parse(q).unwrap();
3495 if let Statement::Recall(r) = &stmt {
3496 assert_eq!(r.about, "hello\nworld");
3497 } else {
3498 panic!("expected Recall");
3499 }
3500 }
3501
3502 #[test]
3503 fn valid_escape_tab_succeeds() {
3504 let q = r#"RECALL episodic ABOUT "col1\tcol2""#;
3505 let stmt = parse(q).unwrap();
3506 if let Statement::Recall(r) = &stmt {
3507 assert_eq!(r.about, "col1\tcol2");
3508 } else {
3509 panic!("expected Recall");
3510 }
3511 }
3512
3513 #[test]
3514 fn valid_escape_backslash_succeeds() {
3515 let q = r#"RECALL episodic ABOUT "path\\to\\file""#;
3516 let stmt = parse(q).unwrap();
3517 if let Statement::Recall(r) = &stmt {
3518 assert_eq!(r.about, "path\\to\\file");
3519 } else {
3520 panic!("expected Recall");
3521 }
3522 }
3523
3524 #[test]
3525 fn valid_escape_quote_succeeds() {
3526 let q = r#"RECALL episodic ABOUT "say \"hello\"" "#;
3527 let stmt = parse(q).unwrap();
3528 if let Statement::Recall(r) = &stmt {
3529 assert_eq!(r.about, "say \"hello\"");
3530 } else {
3531 panic!("expected Recall");
3532 }
3533 }
3534
3535 #[test]
3538 fn parse_recall_activation_ppr() {
3539 let q = r#"RECALL episodic ABOUT "test" EXPAND GRAPH DEPTH 3 ACTIVATION PPR"#;
3540 let stmt = parse(q).unwrap();
3541 if let Statement::Recall(r) = &stmt {
3542 let expand = r.expand.as_ref().expect("should have expand clause");
3543 assert_eq!(expand.activation, Some(ActivationModeAst::Ppr));
3544 assert_eq!(expand.depth, 3);
3545 } else {
3546 panic!("expected Recall");
3547 }
3548 }
3549
3550 #[test]
3551 fn parse_recall_activation_pagerank() {
3552 let q = r#"RECALL episodic ABOUT "test" EXPAND GRAPH DEPTH 2 ACTIVATION PAGERANK"#;
3553 let stmt = parse(q).unwrap();
3554 if let Statement::Recall(r) = &stmt {
3555 let expand = r.expand.as_ref().expect("should have expand clause");
3556 assert_eq!(expand.activation, Some(ActivationModeAst::Ppr));
3557 assert_eq!(expand.depth, 2);
3558 } else {
3559 panic!("expected Recall");
3560 }
3561 }
3562
3563 #[test]
3566 fn limit_overflow_returns_error() {
3567 let q = r#"RECALL episodic ABOUT "test" LIMIT 99999999999999999999"#;
3568 let result = parse(q);
3569 assert!(result.is_err(), "expected error for overflow LIMIT");
3570 }
3571
3572 #[test]
3573 fn limit_valid_still_works() {
3574 let q = r#"RECALL episodic ABOUT "test" LIMIT 10"#;
3575 let stmt = parse(q).unwrap();
3576 if let Statement::Recall(r) = &stmt {
3577 assert_eq!(r.limit, Some(10));
3578 } else {
3579 panic!("expected Recall");
3580 }
3581 }
3582
3583 #[test]
3586 fn query_too_large_returns_error() {
3587 let limits = QueryLimits {
3588 max_query_length: 50,
3589 ..Default::default()
3590 };
3591 let q = &"x".repeat(100);
3592 let result = parse_with_limits(q, &limits);
3593 assert!(result.is_err());
3594 assert!(result.unwrap_err().message.contains("exceeds maximum"));
3595 }
3596
3597 #[test]
3598 fn expand_depth_exceeds_limit() {
3599 let limits = QueryLimits {
3600 max_expand_depth: 5,
3601 ..Default::default()
3602 };
3603 let q = r#"RECALL episodic ABOUT "test" EXPAND GRAPH DEPTH 6"#;
3604 let result = parse_with_limits(q, &limits);
3605 let err = result.unwrap_err();
3606 assert!(
3607 err.message.contains("DEPTH") || err.message.contains("depth"),
3608 "expected depth error, got: {}",
3609 err.message
3610 );
3611 }
3612
3613 #[test]
3614 fn limit_exceeds_max_returns_error() {
3615 let limits = QueryLimits {
3616 max_limit: 100,
3617 ..Default::default()
3618 };
3619 let q = r#"RECALL episodic ABOUT "test" LIMIT 200"#;
3620 let result = parse_with_limits(q, &limits);
3621 assert!(result.is_err());
3622 let msg = result.unwrap_err().message.to_lowercase();
3623 assert!(msg.contains("limit") || msg.contains("exceed"));
3624 }
3625
3626 #[test]
3627 fn normal_query_with_default_limits_succeeds() {
3628 let limits = QueryLimits::default();
3629 let q = r#"RECALL episodic ABOUT "test" LIMIT 100"#;
3630 let stmt = parse_with_limits(q, &limits).unwrap();
3631 if let Statement::Recall(r) = &stmt {
3632 assert_eq!(r.limit, Some(100));
3633 } else {
3634 panic!("expected Recall");
3635 }
3636 }
3637
3638 #[test]
3641 fn parse_recall_depth_auto() {
3642 let stmt = parse(r#"RECALL episodic ABOUT "test" DEPTH AUTO"#).unwrap();
3643 match stmt {
3644 Statement::Recall(r) => assert_eq!(r.depth_mode, Some(DepthModeAst::Auto)),
3645 other => panic!("expected Recall, got {other:?}"),
3646 }
3647 }
3648
3649 #[test]
3650 fn parse_recall_depth_full() {
3651 let stmt = parse(r#"RECALL episodic ABOUT "test" DEPTH FULL"#).unwrap();
3652 match stmt {
3653 Statement::Recall(r) => assert_eq!(r.depth_mode, Some(DepthModeAst::Full)),
3654 other => panic!("expected Recall, got {other:?}"),
3655 }
3656 }
3657
3658 #[test]
3659 fn parse_recall_depth_summary() {
3660 let stmt = parse(r#"RECALL episodic ABOUT "test" DEPTH SUMMARY"#).unwrap();
3661 match stmt {
3662 Statement::Recall(r) => assert_eq!(r.depth_mode, Some(DepthModeAst::Summary)),
3663 other => panic!("expected Recall, got {other:?}"),
3664 }
3665 }
3666
3667 #[test]
3668 fn parse_think_depth_full() {
3669 let stmt = parse(r#"THINK ABOUT "test" DEPTH FULL"#).unwrap();
3670 match stmt {
3671 Statement::Think(t) => assert_eq!(t.depth_mode, Some(DepthModeAst::Full)),
3672 other => panic!("expected Think, got {other:?}"),
3673 }
3674 }
3675
3676 #[test]
3677 fn parse_recall_depth_omitted_is_none() {
3678 let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3679 match stmt {
3680 Statement::Recall(r) => assert_eq!(r.depth_mode, None),
3681 other => panic!("expected Recall, got {other:?}"),
3682 }
3683 }
3684
3685 #[test]
3688 fn parse_recall_with_prospective_on() {
3689 let stmt = parse(r#"RECALL episodic ABOUT "test" WITH PROSPECTIVE ON"#).unwrap();
3690 match stmt {
3691 Statement::Recall(r) => assert_eq!(r.with_prospective, Some(true)),
3692 other => panic!("expected Recall, got {other:?}"),
3693 }
3694 }
3695
3696 #[test]
3697 fn parse_recall_with_prospective_off() {
3698 let stmt = parse(r#"RECALL episodic ABOUT "test" WITH PROSPECTIVE OFF"#).unwrap();
3699 match stmt {
3700 Statement::Recall(r) => assert_eq!(r.with_prospective, Some(false)),
3701 other => panic!("expected Recall, got {other:?}"),
3702 }
3703 }
3704
3705 #[test]
3706 fn parse_think_with_prospective_on() {
3707 let stmt = parse(r#"THINK ABOUT "test" WITH PROSPECTIVE ON"#).unwrap();
3708 match stmt {
3709 Statement::Think(t) => assert_eq!(t.with_prospective, Some(true)),
3710 other => panic!("expected Think, got {other:?}"),
3711 }
3712 }
3713
3714 #[test]
3717 fn parse_recall_with_mcfa_on() {
3718 let stmt = parse(r#"RECALL episodic ABOUT "test" WITH MCFA_DEFENSE ON"#).unwrap();
3719 match stmt {
3720 Statement::Recall(r) => assert_eq!(r.with_mcfa, Some(true)),
3721 other => panic!("expected Recall, got {other:?}"),
3722 }
3723 }
3724
3725 #[test]
3726 fn parse_recall_with_mcfa_off() {
3727 let stmt = parse(r#"RECALL episodic ABOUT "test" WITH MCFA_DEFENSE OFF"#).unwrap();
3728 match stmt {
3729 Statement::Recall(r) => assert_eq!(r.with_mcfa, Some(false)),
3730 other => panic!("expected Recall, got {other:?}"),
3731 }
3732 }
3733
3734 #[test]
3735 fn parse_think_with_mcfa_off() {
3736 let stmt = parse(r#"THINK ABOUT "test" WITH MCFA_DEFENSE OFF"#).unwrap();
3737 match stmt {
3738 Statement::Think(t) => assert_eq!(t.with_mcfa, Some(false)),
3739 other => panic!("expected Think, got {other:?}"),
3740 }
3741 }
3742
3743 #[test]
3746 fn parse_recall_with_conflicts() {
3747 let stmt = parse(r#"RECALL episodic ABOUT "test" WITH CONFLICTS"#).unwrap();
3748 match stmt {
3749 Statement::Recall(r) => assert!(r.with_conflicts),
3750 other => panic!("expected Recall, got {other:?}"),
3751 }
3752 }
3753
3754 #[test]
3755 fn parse_recall_without_conflicts_default() {
3756 let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3757 match stmt {
3758 Statement::Recall(r) => assert!(!r.with_conflicts),
3759 other => panic!("expected Recall, got {other:?}"),
3760 }
3761 }
3762
3763 #[test]
3766 fn parse_recall_topic() {
3767 let stmt = parse(r#"RECALL episodic ABOUT "test" TOPIC "deployment""#).unwrap();
3768 match stmt {
3769 Statement::Recall(r) => {
3770 assert_eq!(r.topic, Some("deployment".to_string()));
3771 }
3772 other => panic!("expected Recall, got {other:?}"),
3773 }
3774 }
3775
3776 #[test]
3777 fn parse_recall_topic_with_temporal() {
3778 let stmt = parse(
3779 r#"RECALL episodic ABOUT "test" BETWEEN "2026-01-01" AND "2026-06-01" TOPIC "deployment""#,
3780 )
3781 .unwrap();
3782 match stmt {
3783 Statement::Recall(r) => {
3784 assert_eq!(r.topic, Some("deployment".to_string()));
3785 assert_eq!(
3786 r.temporal,
3787 Some(TemporalClause::Between {
3788 start: "2026-01-01".into(),
3789 end: "2026-06-01".into()
3790 })
3791 );
3792 }
3793 other => panic!("expected Recall, got {other:?}"),
3794 }
3795 }
3796
3797 #[test]
3798 fn parse_recall_topic_omitted_is_none() {
3799 let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
3800 match stmt {
3801 Statement::Recall(r) => assert_eq!(r.topic, None),
3802 other => panic!("expected Recall, got {other:?}"),
3803 }
3804 }
3805
3806 #[test]
3809 fn parse_think_mode_iterative() {
3810 let stmt = parse(r#"THINK ABOUT "test" BUDGET 4096 MODE ITERATIVE MAX_HOPS 3"#).unwrap();
3811 match stmt {
3812 Statement::Think(t) => {
3813 assert_eq!(t.mode, RetrievalMode::Iterative);
3814 assert_eq!(t.max_hops, Some(3));
3815 assert_eq!(t.budget, Some(4096));
3816 }
3817 other => panic!("expected Think, got {other:?}"),
3818 }
3819 }
3820
3821 #[test]
3822 fn parse_think_mode_iterative_without_max_hops() {
3823 let stmt = parse(r#"THINK ABOUT "test" MODE ITERATIVE"#).unwrap();
3824 match stmt {
3825 Statement::Think(t) => {
3826 assert_eq!(t.mode, RetrievalMode::Iterative);
3827 assert_eq!(t.max_hops, None);
3828 }
3829 other => panic!("expected Think, got {other:?}"),
3830 }
3831 }
3832
3833 #[test]
3834 fn parse_think_mode_iterative_max_hops_validation_zero() {
3835 let result = parse(r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 0"#);
3836 assert!(result.is_err());
3837 let msg = result.unwrap_err().message;
3838 assert!(msg.contains("MAX_HOPS must be between 1 and 5"));
3839 }
3840
3841 #[test]
3842 fn parse_think_mode_iterative_max_hops_validation_too_high() {
3843 let result = parse(r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 6"#);
3844 assert!(result.is_err());
3845 let msg = result.unwrap_err().message;
3846 assert!(msg.contains("MAX_HOPS must be between 1 and 5"));
3847 }
3848
3849 #[test]
3850 fn parse_think_mode_iterative_max_hops_boundary_valid() {
3851 let stmt = parse(r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 1"#).unwrap();
3852 match stmt {
3853 Statement::Think(t) => assert_eq!(t.max_hops, Some(1)),
3854 other => panic!("expected Think, got {other:?}"),
3855 }
3856
3857 let stmt = parse(r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 5"#).unwrap();
3858 match stmt {
3859 Statement::Think(t) => assert_eq!(t.max_hops, Some(5)),
3860 other => panic!("expected Think, got {other:?}"),
3861 }
3862 }
3863
3864 #[test]
3865 fn parse_think_non_iterative_is_default() {
3866 let stmt = parse(r#"THINK ABOUT "test""#).unwrap();
3867 match stmt {
3868 Statement::Think(t) => {
3869 assert_eq!(t.mode, RetrievalMode::Local);
3870 assert_eq!(t.max_hops, None);
3871 }
3872 other => panic!("expected Think, got {other:?}"),
3873 }
3874 }
3875
3876 #[test]
3877 fn parse_think_mode_adaptive_clause() {
3878 let stmt = parse(r#"THINK ABOUT "test" MODE ADAPTIVE"#).unwrap();
3879 match stmt {
3880 Statement::Think(t) => assert_eq!(t.mode, RetrievalMode::Adaptive),
3881 other => panic!("expected Think, got {other:?}"),
3882 }
3883 }
3884
3885 #[test]
3886 fn parse_think_mode_raptor_clause() {
3887 let stmt = parse(r#"THINK ABOUT "test" MODE RAPTOR"#).unwrap();
3888 match stmt {
3889 Statement::Think(t) => assert_eq!(t.mode, RetrievalMode::Raptor),
3890 other => panic!("expected Think, got {other:?}"),
3891 }
3892 }
3893
3894 #[test]
3895 fn parse_think_mode_hybrid_clause() {
3896 let stmt = parse(r#"THINK ABOUT "test" MODE HYBRID"#).unwrap();
3897 match stmt {
3898 Statement::Think(t) => assert_eq!(t.mode, RetrievalMode::Hybrid),
3899 other => panic!("expected Think, got {other:?}"),
3900 }
3901 }
3902
3903 #[test]
3906 fn parse_recall_events_between() {
3907 let stmt = parse(r#"RECALL EVENTS BETWEEN "2026-03-01" AND "2026-03-15""#).unwrap();
3908 match stmt {
3909 Statement::RecallEvents(re) => {
3910 assert_eq!(
3911 re.temporal,
3912 Some(TemporalClause::Between {
3913 start: "2026-03-01".into(),
3914 end: "2026-03-15".into()
3915 })
3916 );
3917 assert_eq!(re.entity_filter, None);
3918 }
3919 other => panic!("expected RecallEvents, got {other:?}"),
3920 }
3921 }
3922
3923 #[test]
3924 fn parse_recall_events_for_entity() {
3925 let stmt = parse(r#"RECALL EVENTS FOR "nginx""#).unwrap();
3926 match stmt {
3927 Statement::RecallEvents(re) => {
3928 assert_eq!(re.entity_filter, Some("nginx".to_string()));
3929 }
3930 other => panic!("expected RecallEvents, got {other:?}"),
3931 }
3932 }
3933
3934 #[test]
3935 fn parse_recall_events_for_entity_with_temporal() {
3936 let stmt =
3937 parse(r#"RECALL EVENTS FOR "nginx" BETWEEN "2026-03-01" AND "2026-03-15""#).unwrap();
3938 match stmt {
3939 Statement::RecallEvents(re) => {
3940 assert_eq!(re.entity_filter, Some("nginx".to_string()));
3941 assert_eq!(
3942 re.temporal,
3943 Some(TemporalClause::Between {
3944 start: "2026-03-01".into(),
3945 end: "2026-03-15".into()
3946 })
3947 );
3948 }
3949 other => panic!("expected RecallEvents, got {other:?}"),
3950 }
3951 }
3952
3953 #[test]
3954 fn parse_recall_events_where_subject() {
3955 let stmt = parse(r#"RECALL EVENTS WHERE subject = "user_login""#).unwrap();
3956 match stmt {
3957 Statement::RecallEvents(re) => {
3958 assert_eq!(re.where_clauses.len(), 1);
3959 assert_eq!(re.where_clauses[0].field, "subject");
3960 assert_eq!(
3961 re.where_clauses[0].value,
3962 ConditionValue::String("user_login".into())
3963 );
3964 }
3965 other => panic!("expected RecallEvents, got {other:?}"),
3966 }
3967 }
3968
3969 #[test]
3970 fn parse_recall_events_for_with_where_and_limit() {
3971 let stmt = parse(r#"RECALL EVENTS FOR "nginx" WHERE verb = "crashed" LIMIT 10"#).unwrap();
3972 match stmt {
3973 Statement::RecallEvents(re) => {
3974 assert_eq!(re.entity_filter, Some("nginx".to_string()));
3975 assert_eq!(re.where_clauses.len(), 1);
3976 assert_eq!(re.where_clauses[0].field, "verb");
3977 assert_eq!(re.limit, Some(10));
3978 }
3979 other => panic!("expected RecallEvents, got {other:?}"),
3980 }
3981 }
3982
3983 #[test]
3986 fn parse_recall_all_new_clauses() {
3987 let q = r#"
3988 RECALL episodic
3989 ABOUT "deployment"
3990 DEPTH FULL
3991 TOPIC "k8s"
3992 WITH PROSPECTIVE ON
3993 WITH MCFA_DEFENSE OFF
3994 WITH CONFLICTS
3995 LIMIT 20
3996 "#;
3997 let stmt = parse(q).unwrap();
3998 match stmt {
3999 Statement::Recall(r) => {
4000 assert_eq!(r.depth_mode, Some(DepthModeAst::Full));
4001 assert_eq!(r.topic, Some("k8s".to_string()));
4002 assert_eq!(r.with_prospective, Some(true));
4003 assert_eq!(r.with_mcfa, Some(false));
4004 assert!(r.with_conflicts);
4005 assert_eq!(r.limit, Some(20));
4006 }
4007 other => panic!("expected Recall, got {other:?}"),
4008 }
4009 }
4010
4011 #[test]
4012 fn parse_think_all_new_clauses() {
4013 let q = r#"
4014 THINK ABOUT "optimize HNSW"
4015 DEPTH SUMMARY
4016 WITH PROSPECTIVE OFF
4017 WITH MCFA_DEFENSE ON
4018 BUDGET 4096
4019 MODE ITERATIVE MAX_HOPS 2
4020 HYBRID
4021 "#;
4022 let stmt = parse(q).unwrap();
4023 match stmt {
4024 Statement::Think(t) => {
4025 assert_eq!(t.depth_mode, Some(DepthModeAst::Summary));
4026 assert_eq!(t.with_prospective, Some(false));
4027 assert_eq!(t.with_mcfa, Some(true));
4028 assert_eq!(t.mode, RetrievalMode::Iterative);
4029 assert_eq!(t.max_hops, Some(2));
4030 assert_eq!(t.budget, Some(4096));
4031 assert!(t.hybrid);
4032 }
4033 other => panic!("expected Think, got {other:?}"),
4034 }
4035 }
4036
4037 #[test]
4040 fn display_recall_topic() {
4041 let q = r#"RECALL episodic ABOUT "test" TOPIC "deployment""#;
4042 let stmt = parse(q).unwrap();
4043 let displayed = stmt.to_string();
4044 assert!(
4045 displayed.contains("TOPIC \"deployment\""),
4046 "got: {displayed}"
4047 );
4048 }
4049
4050 #[test]
4051 fn display_think_mode_iterative() {
4052 let q = r#"THINK ABOUT "test" MODE ITERATIVE MAX_HOPS 3"#;
4053 let stmt = parse(q).unwrap();
4054 let displayed = stmt.to_string();
4055 assert!(displayed.contains("MODE iterative"), "got: {displayed}");
4056 assert!(displayed.contains("MAX_HOPS 3"), "got: {displayed}");
4057 }
4058
4059 #[test]
4060 fn display_recall_events_for() {
4061 let q = r#"RECALL EVENTS FOR "nginx""#;
4062 let stmt = parse(q).unwrap();
4063 let displayed = stmt.to_string();
4064 assert!(displayed.contains("FOR \"nginx\""), "got: {displayed}");
4065 }
4066
4067 #[test]
4068 fn display_recall_depth_mode() {
4069 let q = r#"RECALL episodic ABOUT "test" DEPTH FULL"#;
4070 let stmt = parse(q).unwrap();
4071 let displayed = stmt.to_string();
4072 assert!(displayed.contains("DEPTH FULL"), "got: {displayed}");
4073 }
4074
4075 #[test]
4076 fn display_recall_with_clauses() {
4077 let q = r#"RECALL episodic ABOUT "test" WITH PROSPECTIVE ON WITH MCFA_DEFENSE OFF WITH CONFLICTS"#;
4078 let stmt = parse(q).unwrap();
4079 let displayed = stmt.to_string();
4080 assert!(
4081 displayed.contains("WITH PROSPECTIVE ON"),
4082 "got: {displayed}"
4083 );
4084 assert!(
4085 displayed.contains("WITH MCFA_DEFENSE OFF"),
4086 "got: {displayed}"
4087 );
4088 assert!(displayed.contains("WITH CONFLICTS"), "got: {displayed}");
4089 }
4090
4091 #[test]
4094 fn parse_think_max_hops_without_iterative_rejected() {
4095 let result = parse(r#"THINK ABOUT "test" MODE LOCAL MAX_HOPS 3"#);
4099 assert!(
4100 result.is_err(),
4101 "MAX_HOPS with LOCAL mode should be rejected"
4102 );
4103 }
4104
4105 #[test]
4108 fn parse_recall_with_provenance_depth() {
4109 let stmt = parse(r#"RECALL semantic ABOUT "test" WITH PROVENANCE DEPTH 2"#).unwrap();
4110 match stmt {
4111 Statement::Recall(r) => {
4112 assert_eq!(r.provenance_depth, Some(2));
4113 }
4114 other => panic!("expected Recall, got {other:?}"),
4115 }
4116 }
4117
4118 #[test]
4119 fn parse_recall_provenance_depth_omitted_is_none() {
4120 let stmt = parse(r#"RECALL semantic ABOUT "test""#).unwrap();
4121 match stmt {
4122 Statement::Recall(r) => {
4123 assert_eq!(r.provenance_depth, None);
4124 }
4125 other => panic!("expected Recall, got {other:?}"),
4126 }
4127 }
4128
4129 #[test]
4130 fn parse_think_with_provenance_depth() {
4131 let stmt = parse(r#"THINK ABOUT "test" WITH PROVENANCE DEPTH 3"#).unwrap();
4132 match stmt {
4133 Statement::Think(t) => {
4134 assert_eq!(t.provenance_depth, Some(3));
4135 }
4136 other => panic!("expected Think, got {other:?}"),
4137 }
4138 }
4139
4140 #[test]
4141 fn parse_recall_provenance_with_conflicts_combo() {
4142 let stmt = parse(r#"RECALL episodic ABOUT "test" WITH CONFLICTS WITH PROVENANCE DEPTH 1"#)
4143 .unwrap();
4144 match stmt {
4145 Statement::Recall(r) => {
4146 assert!(r.with_conflicts);
4147 assert_eq!(r.provenance_depth, Some(1));
4148 }
4149 other => panic!("expected Recall, got {other:?}"),
4150 }
4151 }
4152
4153 #[test]
4156 fn parse_set_tier_policy_string_value() {
4157 let stmt = parse("SET TIER_POLICY working_to_episodic_ttl = '2h'").unwrap();
4158 match stmt {
4159 Statement::SetTierPolicy(s) => {
4160 assert_eq!(s.field, "working_to_episodic_ttl");
4161 assert_eq!(s.value, TierPolicyValue::Str("2h".into()));
4162 }
4163 other => panic!("expected SetTierPolicy, got {other:?}"),
4164 }
4165 }
4166
4167 #[test]
4168 fn parse_set_tier_policy_float_value() {
4169 let stmt = parse("SET TIER_POLICY episodic_to_semantic_threshold = 0.85").unwrap();
4170 match stmt {
4171 Statement::SetTierPolicy(s) => {
4172 assert_eq!(s.field, "episodic_to_semantic_threshold");
4173 assert_eq!(s.value, TierPolicyValue::Float(0.85));
4174 }
4175 other => panic!("expected SetTierPolicy, got {other:?}"),
4176 }
4177 }
4178
4179 #[test]
4180 fn parse_set_tier_policy_integer_value() {
4181 let stmt = parse("SET TIER_POLICY working_to_episodic_ttl = 3600").unwrap();
4182 match stmt {
4183 Statement::SetTierPolicy(s) => {
4184 assert_eq!(s.field, "working_to_episodic_ttl");
4185 assert_eq!(s.value, TierPolicyValue::Int(3600));
4186 }
4187 other => panic!("expected SetTierPolicy, got {other:?}"),
4188 }
4189 }
4190
4191 #[test]
4192 fn parse_set_tier_policy_case_insensitive() {
4193 let stmt = parse("set tier_policy procedural_min_success_rate = 0.5").unwrap();
4194 match stmt {
4195 Statement::SetTierPolicy(s) => {
4196 assert_eq!(s.field, "procedural_min_success_rate");
4197 assert_eq!(s.value, TierPolicyValue::Float(0.5));
4198 }
4199 other => panic!("expected SetTierPolicy, got {other:?}"),
4200 }
4201 }
4202
4203 #[test]
4204 fn parse_set_tier_policy_display_roundtrip() {
4205 let stmt = parse("SET TIER_POLICY semantic_archive_threshold = 0.2").unwrap();
4206 let display = format!("{stmt}");
4207 assert_eq!(display, "SET TIER_POLICY semantic_archive_threshold = 0.2");
4208 }
4209
4210 #[test]
4213 fn parse_recall_from_realm_single() {
4214 let stmt = parse(r#"RECALL episodic ABOUT "test" FROM REALM "production""#).unwrap();
4215 match stmt {
4216 Statement::Recall(r) => {
4217 assert_eq!(r.from_realms, Some(vec!["production".to_string()]));
4218 }
4219 other => panic!("expected Recall, got {other:?}"),
4220 }
4221 }
4222
4223 #[test]
4224 fn parse_recall_from_realm_multiple() {
4225 let stmt =
4226 parse(r#"RECALL episodic ABOUT "test" FROM REALM "production", "staging""#).unwrap();
4227 match stmt {
4228 Statement::Recall(r) => {
4229 assert_eq!(
4230 r.from_realms,
4231 Some(vec!["production".to_string(), "staging".to_string()])
4232 );
4233 }
4234 other => panic!("expected Recall, got {other:?}"),
4235 }
4236 }
4237
4238 #[test]
4239 fn parse_recall_without_from_realm() {
4240 let stmt = parse(r#"RECALL episodic ABOUT "test""#).unwrap();
4241 match stmt {
4242 Statement::Recall(r) => assert!(r.from_realms.is_none()),
4243 other => panic!("expected Recall, got {other:?}"),
4244 }
4245 }
4246
4247 #[test]
4248 fn parse_recall_from_realm_display_roundtrip() {
4249 let stmt =
4250 parse(r#"RECALL episodic ABOUT "test" FROM REALM "production", "staging" LIMIT 10"#)
4251 .unwrap();
4252 let display = format!("{stmt}");
4253 assert!(display.contains("FROM REALM"));
4254 assert!(display.contains("\"production\""));
4255 assert!(display.contains("\"staging\""));
4256 }
4257
4258 #[test]
4261 fn parse_explain_causes_basic() {
4262 let stmt = parse(r#"EXPLAIN CAUSES "deployment failure""#).unwrap();
4263 match stmt {
4264 Statement::ExplainCauses(ec) => {
4265 assert_eq!(ec.target, "deployment failure");
4266 assert_eq!(ec.namespace, None);
4267 assert_eq!(ec.depth, None);
4268 }
4269 other => panic!("expected ExplainCauses, got {other:?}"),
4270 }
4271 }
4272
4273 #[test]
4274 fn parse_explain_causes_with_depth() {
4275 let stmt = parse(r#"EXPLAIN CAUSES "server crash" DEPTH 5"#).unwrap();
4276 match stmt {
4277 Statement::ExplainCauses(ec) => {
4278 assert_eq!(ec.target, "server crash");
4279 assert_eq!(ec.depth, Some(5));
4280 }
4281 other => panic!("expected ExplainCauses, got {other:?}"),
4282 }
4283 }
4284
4285 #[test]
4286 fn parse_explain_causes_with_namespace() {
4287 let stmt = parse(r#"EXPLAIN CAUSES "deployment failure" NAMESPACE ops"#).unwrap();
4288 match stmt {
4289 Statement::ExplainCauses(ec) => {
4290 assert_eq!(ec.target, "deployment failure");
4291 assert_eq!(ec.namespace, Some("ops".into()));
4292 assert_eq!(ec.depth, None);
4293 }
4294 other => panic!("expected ExplainCauses, got {other:?}"),
4295 }
4296 }
4297
4298 #[test]
4299 fn parse_explain_causes_full() {
4300 let stmt = parse(r#"EXPLAIN CAUSES "deployment failure" NAMESPACE ops DEPTH 3"#).unwrap();
4301 match stmt {
4302 Statement::ExplainCauses(ec) => {
4303 assert_eq!(ec.target, "deployment failure");
4304 assert_eq!(ec.namespace, Some("ops".into()));
4305 assert_eq!(ec.depth, Some(3));
4306 }
4307 other => panic!("expected ExplainCauses, got {other:?}"),
4308 }
4309 }
4310
4311 #[test]
4312 fn parse_what_if_basic() {
4313 let stmt = parse(r#"WHAT_IF "increase timeout" THEN "fewer errors""#).unwrap();
4314 match stmt {
4315 Statement::WhatIf(wi) => {
4316 assert_eq!(wi.intervention, "increase timeout");
4317 assert_eq!(wi.outcome, "fewer errors");
4318 assert_eq!(wi.namespace, None);
4319 }
4320 other => panic!("expected WhatIf, got {other:?}"),
4321 }
4322 }
4323
4324 #[test]
4325 fn parse_what_if_with_namespace() {
4326 let stmt =
4327 parse(r#"WHAT_IF "increase timeout" THEN "fewer errors" NAMESPACE prod"#).unwrap();
4328 match stmt {
4329 Statement::WhatIf(wi) => {
4330 assert_eq!(wi.intervention, "increase timeout");
4331 assert_eq!(wi.outcome, "fewer errors");
4332 assert_eq!(wi.namespace, Some("prod".into()));
4333 }
4334 other => panic!("expected WhatIf, got {other:?}"),
4335 }
4336 }
4337
4338 #[test]
4339 fn parse_counterfactual_basic() {
4340 let stmt = parse(r#"COUNTERFACTUAL "if deploy had not happened" THEN "outage""#).unwrap();
4341 match stmt {
4342 Statement::Counterfactual(cf) => {
4343 assert_eq!(cf.antecedent, "if deploy had not happened");
4344 assert_eq!(cf.consequent, "outage");
4345 assert_eq!(cf.namespace, None);
4346 }
4347 other => panic!("expected Counterfactual, got {other:?}"),
4348 }
4349 }
4350
4351 #[test]
4352 fn parse_counterfactual_with_namespace() {
4353 let stmt = parse(
4354 r#"COUNTERFACTUAL "if deploy had not happened" THEN "outage" NAMESPACE production"#,
4355 )
4356 .unwrap();
4357 match stmt {
4358 Statement::Counterfactual(cf) => {
4359 assert_eq!(cf.antecedent, "if deploy had not happened");
4360 assert_eq!(cf.consequent, "outage");
4361 assert_eq!(cf.namespace, Some("production".into()));
4362 }
4363 other => panic!("expected Counterfactual, got {other:?}"),
4364 }
4365 }
4366
4367 #[test]
4368 fn parse_explain_causes_display_roundtrip() {
4369 let stmt = parse(r#"EXPLAIN CAUSES "failure" NAMESPACE ops DEPTH 3"#).unwrap();
4370 let display = format!("{stmt}");
4371 assert!(display.contains("EXPLAIN CAUSES"));
4372 assert!(display.contains("failure"));
4373 }
4374
4375 #[test]
4376 fn parse_what_if_display_roundtrip() {
4377 let stmt = parse(r#"WHAT_IF "intervention" THEN "outcome""#).unwrap();
4378 let display = format!("{stmt}");
4379 assert!(display.contains("WHAT_IF"));
4380 assert!(display.contains("intervention"));
4381 assert!(display.contains("outcome"));
4382 }
4383
4384 #[test]
4385 fn parse_counterfactual_display_roundtrip() {
4386 let stmt = parse(r#"COUNTERFACTUAL "cause" THEN "effect""#).unwrap();
4387 let display = format!("{stmt}");
4388 assert!(display.contains("COUNTERFACTUAL"));
4389 assert!(display.contains("cause"));
4390 assert!(display.contains("effect"));
4391 }
4392}