query: MCP tool uses execute_query, add double-quote strings
- MCP memory_query tool now uses execute_query path instead of
parse_stages, enabling full expression support (content ~, AND/OR,
neighbors, etc.) instead of just Expr::All
- Parser now accepts double-quoted strings ("foo") in addition to
single quotes ('foo')
- Added tests for double-quote syntax
- Removed dead resolve_field_str function from memory.rs
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
c8922c9408
commit
0612e1bc41
2 changed files with 138 additions and 58 deletions
|
|
@ -201,9 +201,22 @@ peg::parser! {
|
|||
|
||||
rule value() -> Value
|
||||
= f:fn_call() { Value::FnCall(f) }
|
||||
/ n:number() { Value::Num(n) }
|
||||
/ s:string() { Value::Str(s) }
|
||||
/ i:ident() { Value::Ident(i) }
|
||||
/ t:token() { t }
|
||||
|
||||
// Token: number or identifier, with alphanumeric fallback (e.g., "27b")
|
||||
rule token() -> Value
|
||||
= n:$(['0'..='9']+ ("." ['0'..='9']+)?) !['a'..='z' | 'A'..='Z'] {
|
||||
Value::Num(n.parse().unwrap())
|
||||
}
|
||||
/ s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']+) {
|
||||
// Try as number first, fall back to string
|
||||
if let Ok(n) = s.parse::<f64>() {
|
||||
Value::Num(n)
|
||||
} else {
|
||||
Value::Str(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
rule fn_call() -> FnCall
|
||||
= "community" _ "(" _ k:string() _ ")" { FnCall::Community(k) }
|
||||
|
|
@ -216,12 +229,19 @@ peg::parser! {
|
|||
|
||||
rule string() -> String
|
||||
= "'" s:$([^ '\'']*) "'" { s.to_string() }
|
||||
/ "\"" s:$([^ '"']*) "\"" { s.to_string() }
|
||||
|
||||
rule ident() -> String
|
||||
= s:$(['a'..='z' | 'A'..='Z' | '_']['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']*) {
|
||||
s.to_string()
|
||||
}
|
||||
|
||||
// Bare word for matching (allows digits at start, e.g. "27b")
|
||||
rule word() -> String
|
||||
= s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.']+) {
|
||||
s.to_string()
|
||||
}
|
||||
|
||||
// Glob pattern for key matching (allows * and ?)
|
||||
rule glob_pattern() -> String
|
||||
= s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '.' | '*' | '?']+) {
|
||||
|
|
@ -830,3 +850,113 @@ fn print_connectivity(results: &[QueryResult], graph: &Graph) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Tests --
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Helper to check if a query parses successfully
|
||||
fn parses(s: &str) -> bool {
|
||||
query_parser::query(s).is_ok()
|
||||
}
|
||||
|
||||
// Helper to get parse error for debugging
|
||||
fn parse_err(s: &str) -> String {
|
||||
query_parser::query(s).err().map(|e| format!("{}", e)).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generators() {
|
||||
assert!(parses("all"));
|
||||
assert!(parses("*"));
|
||||
assert!(parses("all | limit:10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_filters() {
|
||||
assert!(parses("all | type:semantic"));
|
||||
assert!(parses("all | type:episodic"));
|
||||
assert!(parses("all | provenance:observe"));
|
||||
assert!(parses("all | key:journal-*"));
|
||||
assert!(parses("all | !key:_*")); // negated key glob
|
||||
assert!(parses("all | age:>7d"));
|
||||
assert!(parses("all | not-visited:organize,86400"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pipeline_transforms() {
|
||||
assert!(parses("all | sort:weight"));
|
||||
assert!(parses("all | sort:timestamp"));
|
||||
assert!(parses("all | sort:degree"));
|
||||
assert!(parses("all | limit:20"));
|
||||
assert!(parses("all | sort:weight | limit:10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composite_sort() {
|
||||
// Weighted composite sort expressions (require 2+ terms with +)
|
||||
assert!(parses("all | sort:degree*0.5+isolation*0.3"));
|
||||
assert!(parses("all | sort:degree*0.5+isolation*0.3+recency(organize)*0.2"));
|
||||
assert!(parses("all | sort:weight*0.5+degree*0.5"));
|
||||
// Single field (no weight) falls back to simple sort
|
||||
assert!(parses("all | sort:weight"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expression_syntax() {
|
||||
// Expression comparisons (legacy syntax)
|
||||
assert!(parses("weight > 0.5"));
|
||||
assert!(parses("degree >= 10"));
|
||||
assert!(parses("key ~ 'journal.*'"));
|
||||
assert!(parses("content ~ 27b"), "alphanumeric pattern: {}", parse_err("content ~ 27b"));
|
||||
assert!(parses("content ~ qwen35"));
|
||||
// Both single and double quotes work for strings
|
||||
assert!(parses("content ~ '27b'"));
|
||||
assert!(parses("content ~ \"27b\""), "double quotes: {}", parse_err("content ~ \"27b\""));
|
||||
assert!(parses("neighbors(\"my-key\")"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boolean_expressions() {
|
||||
assert!(parses("weight > 0.5 AND degree > 10"));
|
||||
assert!(parses("key ~ 'a' OR key ~ 'b'"));
|
||||
assert!(parses("NOT weight < 0.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_parsing() {
|
||||
assert!(parses("all | age:>1d"));
|
||||
assert!(parses("all | age:>=24h"));
|
||||
assert!(parses("all | age:<30m"));
|
||||
assert!(parses("all | age:=3600s"));
|
||||
assert!(parses("all | age:>86400")); // raw seconds
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_glob_patterns() {
|
||||
assert!(parses("all | key:*"));
|
||||
assert!(parses("all | key:journal-*"));
|
||||
assert!(parses("all | key:*-2026-*"));
|
||||
assert!(parses("all | key:dream-cycle-?"));
|
||||
assert!(parses("all | !key:subconscious-*"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_pipelines() {
|
||||
assert!(parses("all | type:semantic | sort:weight | limit:50"));
|
||||
assert!(parses("all | !key:_* | sort:degree*0.5+isolation*0.5 | limit:10"));
|
||||
assert!(parses("all | provenance:observe | age:>1d | sort:timestamp | limit:20"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_stages_output() {
|
||||
// Ensure parse_stages produces expected Stage types
|
||||
let stages = parse_stages("all | type:semantic | limit:10").unwrap();
|
||||
assert_eq!(stages.len(), 3);
|
||||
assert!(matches!(stages[0], Stage::Generator(Generator::All)));
|
||||
assert!(matches!(stages[1], Stage::Filter(Filter::Type(_))));
|
||||
assert!(matches!(stages[2], Stage::Transform(Transform::Limit(10))));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue