query: peg-based query language for ad-hoc graph exploration

poc-memory query "degree > 15"
poc-memory query "key ~ 'journal.*' AND degree > 10"
poc-memory query "neighbors('identity.md') WHERE strength > 0.5"
poc-memory query "community_id = community('identity.md')" --fields degree,category

Grammar-driven: the peg definition IS the language spec. Supports
boolean logic (AND/OR/NOT), numeric and string comparison, regex
match (~), graph traversal (neighbors() with WHERE), and function
calls (community(), degree()). Output flags: --fields, --sort,
--limit, --count.

New dependency: peg 0.8 (~68KB, 2 tiny deps).
This commit is contained in:
ProofOfConcept 2026-03-03 10:55:30 -05:00
parent 71e6f15d82
commit a36449032c
5 changed files with 544 additions and 1 deletions

View file

@ -20,6 +20,7 @@ mod search;
mod similarity;
mod migrate;
mod neuro;
mod query;
mod spectral;
pub mod memory_capnp {
@ -118,6 +119,7 @@ fn main() {
"export" => cmd_export(&args[2..]),
"journal-write" => cmd_journal_write(&args[2..]),
"journal-tail" => cmd_journal_tail(&args[2..]),
"query" => cmd_query(&args[2..]),
_ => {
eprintln!("Unknown command: {}", args[1]);
usage();
@ -192,7 +194,11 @@ Commands:
import FILE [FILE...] Import markdown file(s) into the store
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)");
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\"");
}
fn cmd_search(args: &[String]) -> Result<(), String> {
@ -1615,3 +1621,78 @@ fn cmd_interference(args: &[String]) -> Result<(), String> {
}
Ok(())
}
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());
}
// 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 store = capnp_store::Store::load()?;
let graph = store.build_graph();
let results = query::execute_query(&store, &graph, &query_str, &opts)?;
if opts.count_only {
println!("{}", results.len());
return Ok(());
}
if results.is_empty() {
eprintln!("No results");
return Ok(());
}
// If --fields specified, show as TSV with header
if !opts.fields.is_empty() {
let mut header = vec!["key".to_string()];
header.extend(opts.fields.iter().cloned());
println!("{}", header.join("\t"));
for r in &results {
let mut row = vec![r.key.clone()];
for f in &opts.fields {
row.push(query::format_field(f, &r.key, &store, &graph));
}
println!("{}", row.join("\t"));
}
} else {
for r in &results {
println!("{}", r.key);
}
}
Ok(())
}