query: replace CLI flags with pipe syntax

degree > 15 | sort degree | limit 10 | select degree,category
  * | sort weight asc | limit 20
  category = core | count

Output modifiers live in the grammar now, not in CLI flags.
Also adds * wildcard for "all nodes" and string-aware sort fallback.
This commit is contained in:
ProofOfConcept 2026-03-03 11:05:28 -05:00
parent 5c641d9f8a
commit 18face7063
2 changed files with 144 additions and 97 deletions

View file

@ -195,10 +195,9 @@ Commands:
export [FILE|--all] Export store nodes to markdown file(s)
journal-write TEXT Write a journal entry to the store
journal-tail [N] [--full] Show last N journal entries (default 20, --full for content)
query EXPR [--fields F] [--sort F] [--limit N] [--count]
Query the memory graph with expressions
Examples: \"degree > 15\", \"key ~ 'journal.*'\",
\"neighbors('identity.md') WHERE strength > 0.5\"");
query 'EXPR | stages' Query the memory graph
Stages: sort F [asc], limit N, select F,F, count
Ex: \"degree > 15 | sort degree | limit 10\"");
}
fn cmd_search(args: &[String]) -> Result<(), String> {
@ -1624,48 +1623,29 @@ fn cmd_interference(args: &[String]) -> Result<(), String> {
fn cmd_query(args: &[String]) -> Result<(), String> {
if args.is_empty() {
return Err("Usage: poc-memory query EXPR [--fields F,F,...] [--sort F] [--limit N] [--count]".into());
return Err("Usage: poc-memory query 'EXPR | stage | stage ...'\n\n\
Expressions:\n \
degree > 15 property filter\n \
key ~ 'journal.*' AND degree > 10 boolean + regex\n \
neighbors('identity.md') WHERE ... graph traversal\n \
community_id = community('key') function as value\n \
* all nodes\n\n\
Pipe stages:\n \
| sort FIELD [asc] sort (desc by default)\n \
| limit N cap results\n \
| select F,F,... output fields as TSV\n \
| count just show count".into());
}
// Parse flags — query string is the first non-flag arg
let mut opts = query::QueryOpts::default();
let mut query_str = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--fields" if i + 1 < args.len() => {
opts.fields = args[i + 1].split(',').map(|s| s.trim().to_string()).collect();
i += 2;
}
"--sort" if i + 1 < args.len() => {
opts.sort_field = Some(args[i + 1].clone());
i += 2;
}
"--limit" if i + 1 < args.len() => {
opts.limit = Some(args[i + 1].parse().map_err(|_| "invalid --limit")?);
i += 2;
}
"--count" => {
opts.count_only = true;
i += 1;
}
_ if query_str.is_none() => {
query_str = Some(args[i].clone());
i += 1;
}
_ => {
return Err(format!("unexpected argument: {}", args[i]));
}
}
}
let query_str = query_str.ok_or("missing query expression")?;
let query_str = args.join(" ");
let store = capnp_store::Store::load()?;
let graph = store.build_graph();
let results = query::execute_query(&store, &graph, &query_str, &opts)?;
let stages = query::output_stages(&query_str)?;
let results = query::execute_query(&store, &graph, &query_str)?;
if opts.count_only {
// Check for count stage
if stages.iter().any(|s| matches!(s, query::Stage::Count)) {
println!("{}", results.len());
return Ok(());
}
@ -1675,15 +1655,20 @@ fn cmd_query(args: &[String]) -> Result<(), String> {
return Ok(());
}
// If --fields specified, show as TSV with header
if !opts.fields.is_empty() {
// Check for select stage
let fields: Option<&Vec<String>> = stages.iter().find_map(|s| match s {
query::Stage::Select(f) => Some(f),
_ => None,
});
if let Some(fields) = fields {
let mut header = vec!["key".to_string()];
header.extend(opts.fields.iter().cloned());
header.extend(fields.iter().cloned());
println!("{}", header.join("\t"));
for r in &results {
let mut row = vec![r.key.clone()];
for f in &opts.fields {
for f in fields {
row.push(query::format_field(f, &r.key, &store, &graph));
}
println!("{}", row.join("\t"));