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:
parent
5c641d9f8a
commit
18face7063
2 changed files with 144 additions and 97 deletions
73
src/main.rs
73
src/main.rs
|
|
@ -195,10 +195,9 @@ Commands:
|
||||||
export [FILE|--all] Export store nodes to markdown file(s)
|
export [FILE|--all] Export store nodes to markdown file(s)
|
||||||
journal-write TEXT Write a journal entry to the store
|
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 'EXPR | stages' Query the memory graph
|
||||||
Query the memory graph with expressions
|
Stages: sort F [asc], limit N, select F,F, count
|
||||||
Examples: \"degree > 15\", \"key ~ 'journal.*'\",
|
Ex: \"degree > 15 | sort degree | limit 10\"");
|
||||||
\"neighbors('identity.md') WHERE strength > 0.5\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_search(args: &[String]) -> Result<(), String> {
|
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> {
|
fn cmd_query(args: &[String]) -> Result<(), String> {
|
||||||
if args.is_empty() {
|
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 query_str = args.join(" ");
|
||||||
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 store = capnp_store::Store::load()?;
|
||||||
let graph = store.build_graph();
|
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());
|
println!("{}", results.len());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -1675,15 +1655,20 @@ fn cmd_query(args: &[String]) -> Result<(), String> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// If --fields specified, show as TSV with header
|
// Check for select stage
|
||||||
if !opts.fields.is_empty() {
|
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()];
|
let mut header = vec!["key".to_string()];
|
||||||
header.extend(opts.fields.iter().cloned());
|
header.extend(fields.iter().cloned());
|
||||||
println!("{}", header.join("\t"));
|
println!("{}", header.join("\t"));
|
||||||
|
|
||||||
for r in &results {
|
for r in &results {
|
||||||
let mut row = vec![r.key.clone()];
|
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));
|
row.push(query::format_field(f, &r.key, &store, &graph));
|
||||||
}
|
}
|
||||||
println!("{}", row.join("\t"));
|
println!("{}", row.join("\t"));
|
||||||
|
|
|
||||||
164
src/query.rs
164
src/query.rs
|
|
@ -3,6 +3,23 @@
|
||||||
// Grammar-driven: the peg definition IS the language spec.
|
// Grammar-driven: the peg definition IS the language spec.
|
||||||
// Evaluates against node properties, graph metrics, and edge attributes.
|
// Evaluates against node properties, graph metrics, and edge attributes.
|
||||||
// Designed for ad-hoc exploration without memorizing 35+ subcommands.
|
// Designed for ad-hoc exploration without memorizing 35+ subcommands.
|
||||||
|
//
|
||||||
|
// Syntax:
|
||||||
|
// expr | stage | stage ...
|
||||||
|
//
|
||||||
|
// Stages (piped):
|
||||||
|
// sort FIELD sort descending (default for exploration)
|
||||||
|
// sort FIELD asc sort ascending
|
||||||
|
// limit N cap results
|
||||||
|
// select F,F,... output specific fields as TSV
|
||||||
|
// count just show count
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// degree > 15 | sort degree | limit 10
|
||||||
|
// category = core | select degree,weight
|
||||||
|
// neighbors('identity.md') WHERE strength > 0.5 | sort strength
|
||||||
|
// key ~ 'journal.*' AND degree > 10 | count
|
||||||
|
// * | sort weight asc | limit 20
|
||||||
|
|
||||||
use crate::capnp_store::{NodeType, Provenance, RelationType, Store};
|
use crate::capnp_store::{NodeType, Provenance, RelationType, Store};
|
||||||
use crate::graph::Graph;
|
use crate::graph::Graph;
|
||||||
|
|
@ -12,6 +29,7 @@ use regex::Regex;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Expr {
|
pub enum Expr {
|
||||||
|
All,
|
||||||
Comparison { field: String, op: CmpOp, value: Value },
|
Comparison { field: String, op: CmpOp, value: Value },
|
||||||
And(Box<Expr>, Box<Expr>),
|
And(Box<Expr>, Box<Expr>),
|
||||||
Or(Box<Expr>, Box<Expr>),
|
Or(Box<Expr>, Box<Expr>),
|
||||||
|
|
@ -38,12 +56,53 @@ pub enum CmpOp {
|
||||||
Gt, Lt, Ge, Le, Eq, Ne, Match,
|
Gt, Lt, Ge, Le, Eq, Ne, Match,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Stage {
|
||||||
|
Sort { field: String, ascending: bool },
|
||||||
|
Limit(usize),
|
||||||
|
Select(Vec<String>),
|
||||||
|
Count,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Query {
|
||||||
|
pub expr: Expr,
|
||||||
|
pub stages: Vec<Stage>,
|
||||||
|
}
|
||||||
|
|
||||||
// -- PEG grammar --
|
// -- PEG grammar --
|
||||||
|
|
||||||
peg::parser! {
|
peg::parser! {
|
||||||
pub grammar query_parser() for str {
|
pub grammar query_parser() for str {
|
||||||
rule _() = [' ' | '\t']*
|
rule _() = [' ' | '\t']*
|
||||||
|
|
||||||
|
pub rule query() -> Query
|
||||||
|
= e:expr() s:stages() { Query { expr: e, stages: s } }
|
||||||
|
|
||||||
|
rule stages() -> Vec<Stage>
|
||||||
|
= s:(_ "|" _ s:stage() { s })* { s }
|
||||||
|
|
||||||
|
rule stage() -> Stage
|
||||||
|
= "sort" _ f:field() _ a:asc_desc() { Stage::Sort { field: f, ascending: a } }
|
||||||
|
/ "limit" _ n:integer() { Stage::Limit(n) }
|
||||||
|
/ "select" _ f:field_list() { Stage::Select(f) }
|
||||||
|
/ "count" { Stage::Count }
|
||||||
|
|
||||||
|
rule asc_desc() -> bool
|
||||||
|
= "asc" { true }
|
||||||
|
/ "desc" { false }
|
||||||
|
/ { false } // default: descending
|
||||||
|
|
||||||
|
rule field_list() -> Vec<String>
|
||||||
|
= f:field() fs:(_ "," _ f:field() { f })* {
|
||||||
|
let mut v = vec![f];
|
||||||
|
v.extend(fs);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
rule integer() -> usize
|
||||||
|
= n:$(['0'..='9']+) { n.parse().unwrap() }
|
||||||
|
|
||||||
pub rule expr() -> Expr = precedence! {
|
pub rule expr() -> Expr = precedence! {
|
||||||
a:(@) _ "OR" _ b:@ { Expr::Or(Box::new(a), Box::new(b)) }
|
a:(@) _ "OR" _ b:@ { Expr::Or(Box::new(a), Box::new(b)) }
|
||||||
--
|
--
|
||||||
|
|
@ -57,6 +116,7 @@ peg::parser! {
|
||||||
f:field() _ op:cmp_op() _ v:value() {
|
f:field() _ op:cmp_op() _ v:value() {
|
||||||
Expr::Comparison { field: f, op, value: v }
|
Expr::Comparison { field: f, op, value: v }
|
||||||
}
|
}
|
||||||
|
"*" { Expr::All }
|
||||||
"(" _ e:expr() _ ")" { e }
|
"(" _ e:expr() _ ")" { e }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,7 +222,6 @@ fn rel_type_label(r: RelationType) -> &'static str {
|
||||||
|
|
||||||
// -- Comparison logic --
|
// -- Comparison logic --
|
||||||
|
|
||||||
/// Extract numeric value for comparison
|
|
||||||
fn as_num(v: &Value) -> Option<f64> {
|
fn as_num(v: &Value) -> Option<f64> {
|
||||||
match v {
|
match v {
|
||||||
Value::Num(n) => Some(*n),
|
Value::Num(n) => Some(*n),
|
||||||
|
|
@ -172,7 +231,6 @@ fn as_num(v: &Value) -> Option<f64> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract string value for comparison
|
|
||||||
fn as_str(v: &Value) -> String {
|
fn as_str(v: &Value) -> String {
|
||||||
match v {
|
match v {
|
||||||
Value::Str(s) | Value::Ident(s) => s.clone(),
|
Value::Str(s) | Value::Ident(s) => s.clone(),
|
||||||
|
|
@ -181,7 +239,6 @@ fn as_str(v: &Value) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compare two values with the given operator
|
|
||||||
fn compare(lhs: &Value, op: CmpOp, rhs: &Value) -> bool {
|
fn compare(lhs: &Value, op: CmpOp, rhs: &Value) -> bool {
|
||||||
match op {
|
match op {
|
||||||
CmpOp::Match => {
|
CmpOp::Match => {
|
||||||
|
|
@ -193,7 +250,6 @@ fn compare(lhs: &Value, op: CmpOp, rhs: &Value) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CmpOp::Eq => {
|
CmpOp::Eq => {
|
||||||
// Try numeric first, fall back to string
|
|
||||||
if let (Some(a), Some(b)) = (as_num(lhs), as_num(rhs)) {
|
if let (Some(a), Some(b)) = (as_num(lhs), as_num(rhs)) {
|
||||||
a == b
|
a == b
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -223,7 +279,6 @@ fn compare(lhs: &Value, op: CmpOp, rhs: &Value) -> bool {
|
||||||
|
|
||||||
// -- Evaluator --
|
// -- Evaluator --
|
||||||
|
|
||||||
/// Resolve function calls that return values (community ID, degree of specific node)
|
|
||||||
fn resolve_fn(f: &FnCall, store: &Store, graph: &Graph) -> Value {
|
fn resolve_fn(f: &FnCall, store: &Store, graph: &Graph) -> Value {
|
||||||
match f {
|
match f {
|
||||||
FnCall::Community(key) => {
|
FnCall::Community(key) => {
|
||||||
|
|
@ -239,7 +294,6 @@ fn resolve_fn(f: &FnCall, store: &Store, graph: &Graph) -> Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a Value, evaluating function calls
|
|
||||||
fn resolve_value(v: &Value, store: &Store, graph: &Graph) -> Value {
|
fn resolve_value(v: &Value, store: &Store, graph: &Graph) -> Value {
|
||||||
match v {
|
match v {
|
||||||
Value::FnCall(f) => resolve_fn(f, store, graph),
|
Value::FnCall(f) => resolve_fn(f, store, graph),
|
||||||
|
|
@ -247,9 +301,9 @@ fn resolve_value(v: &Value, store: &Store, graph: &Graph) -> Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate an expression against a node
|
|
||||||
fn eval_node(expr: &Expr, key: &str, store: &Store, graph: &Graph) -> bool {
|
fn eval_node(expr: &Expr, key: &str, store: &Store, graph: &Graph) -> bool {
|
||||||
match expr {
|
match expr {
|
||||||
|
Expr::All => true,
|
||||||
Expr::Comparison { field, op, value } => {
|
Expr::Comparison { field, op, value } => {
|
||||||
let lhs = match resolve_field(field, key, store, graph) {
|
let lhs = match resolve_field(field, key, store, graph) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
|
|
@ -265,11 +319,10 @@ fn eval_node(expr: &Expr, key: &str, store: &Store, graph: &Graph) -> bool {
|
||||||
eval_node(a, key, store, graph) || eval_node(b, key, store, graph)
|
eval_node(a, key, store, graph) || eval_node(b, key, store, graph)
|
||||||
}
|
}
|
||||||
Expr::Not(e) => !eval_node(e, key, store, graph),
|
Expr::Not(e) => !eval_node(e, key, store, graph),
|
||||||
Expr::Neighbors { .. } => false, // neighbors() is a top-level expression, not a predicate
|
Expr::Neighbors { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Evaluate a WHERE clause against an edge
|
|
||||||
fn eval_edge(
|
fn eval_edge(
|
||||||
expr: &Expr,
|
expr: &Expr,
|
||||||
_source: &str,
|
_source: &str,
|
||||||
|
|
@ -280,12 +333,11 @@ fn eval_edge(
|
||||||
graph: &Graph,
|
graph: &Graph,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
match expr {
|
match expr {
|
||||||
|
Expr::All => true,
|
||||||
Expr::Comparison { field, op, value } => {
|
Expr::Comparison { field, op, value } => {
|
||||||
// Edge-context fields
|
|
||||||
let lhs = match field.as_str() {
|
let lhs = match field.as_str() {
|
||||||
"strength" => Value::Num(strength as f64),
|
"strength" => Value::Num(strength as f64),
|
||||||
"rel_type" => Value::Str(rel_type_label(rel_type).to_string()),
|
"rel_type" => Value::Str(rel_type_label(rel_type).to_string()),
|
||||||
// Fall through to node fields on the target
|
|
||||||
_ => match resolve_field(field, target, store, graph) {
|
_ => match resolve_field(field, target, store, graph) {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return false,
|
None => return false,
|
||||||
|
|
@ -313,40 +365,17 @@ pub struct QueryResult {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Query options --
|
|
||||||
|
|
||||||
pub struct QueryOpts {
|
|
||||||
pub fields: Vec<String>,
|
|
||||||
pub sort_field: Option<String>,
|
|
||||||
pub limit: Option<usize>,
|
|
||||||
pub count_only: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for QueryOpts {
|
|
||||||
fn default() -> Self {
|
|
||||||
QueryOpts {
|
|
||||||
fields: Vec::new(),
|
|
||||||
sort_field: None,
|
|
||||||
limit: None,
|
|
||||||
count_only: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- Query executor --
|
// -- Query executor --
|
||||||
|
|
||||||
/// Parse and execute a query, returning matching node keys.
|
|
||||||
pub fn execute_query(
|
pub fn execute_query(
|
||||||
store: &Store,
|
store: &Store,
|
||||||
graph: &Graph,
|
graph: &Graph,
|
||||||
query_str: &str,
|
query_str: &str,
|
||||||
opts: &QueryOpts,
|
|
||||||
) -> Result<Vec<QueryResult>, String> {
|
) -> Result<Vec<QueryResult>, String> {
|
||||||
let expr = query_parser::expr(query_str)
|
let q = query_parser::query(query_str)
|
||||||
.map_err(|e| format!("Parse error: {}", e))?;
|
.map_err(|e| format!("Parse error: {}", e))?;
|
||||||
|
|
||||||
let mut results = match &expr {
|
let mut results = match &q.expr {
|
||||||
// neighbors() is a set-returning expression
|
|
||||||
Expr::Neighbors { key, filter } => {
|
Expr::Neighbors { key, filter } => {
|
||||||
let resolved = store.resolve_key(key).unwrap_or_else(|_| key.clone());
|
let resolved = store.resolve_key(key).unwrap_or_else(|_| key.clone());
|
||||||
let edges = graph.edges_of(&resolved);
|
let edges = graph.edges_of(&resolved);
|
||||||
|
|
@ -365,12 +394,11 @@ pub fn execute_query(
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
// Everything else: scan all nodes
|
|
||||||
_ => {
|
_ => {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for key in store.nodes.keys() {
|
for key in store.nodes.keys() {
|
||||||
if store.nodes[key].deleted { continue; }
|
if store.nodes[key].deleted { continue; }
|
||||||
if eval_node(&expr, key, store, graph) {
|
if eval_node(&q.expr, key, store, graph) {
|
||||||
out.push(QueryResult { key: key.clone() });
|
out.push(QueryResult { key: key.clone() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -378,26 +406,61 @@ pub fn execute_query(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort
|
// Apply pipeline stages
|
||||||
let sort_field = opts.sort_field.as_deref().unwrap_or("degree");
|
let mut has_sort = false;
|
||||||
|
for stage in &q.stages {
|
||||||
|
match stage {
|
||||||
|
Stage::Sort { field, ascending } => {
|
||||||
|
has_sort = true;
|
||||||
|
let asc = *ascending;
|
||||||
results.sort_by(|a, b| {
|
results.sort_by(|a, b| {
|
||||||
let va = resolve_field(sort_field, &a.key, store, graph)
|
let va = resolve_field(field, &a.key, store, graph)
|
||||||
.and_then(|v| as_num(&v))
|
.and_then(|v| as_num(&v));
|
||||||
.unwrap_or(0.0);
|
let vb = resolve_field(field, &b.key, store, graph)
|
||||||
let vb = resolve_field(sort_field, &b.key, store, graph)
|
.and_then(|v| as_num(&v));
|
||||||
.and_then(|v| as_num(&v))
|
match (va, vb) {
|
||||||
.unwrap_or(0.0);
|
(Some(a), Some(b)) => if asc {
|
||||||
vb.partial_cmp(&va).unwrap_or(std::cmp::Ordering::Equal)
|
a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
} else {
|
||||||
|
b.partial_cmp(&a).unwrap_or(std::cmp::Ordering::Equal)
|
||||||
|
},
|
||||||
|
// String fallback for non-numeric fields
|
||||||
|
_ => {
|
||||||
|
let sa = resolve_field(field, &a.key, store, graph)
|
||||||
|
.map(|v| as_str(&v)).unwrap_or_default();
|
||||||
|
let sb = resolve_field(field, &b.key, store, graph)
|
||||||
|
.map(|v| as_str(&v)).unwrap_or_default();
|
||||||
|
if asc { sa.cmp(&sb) } else { sb.cmp(&sa) }
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
Stage::Limit(n) => {
|
||||||
|
results.truncate(*n);
|
||||||
|
}
|
||||||
|
Stage::Select(_) | Stage::Count => {} // handled in output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Limit
|
// Default sort by degree desc if no explicit sort
|
||||||
if let Some(limit) = opts.limit {
|
if !has_sort {
|
||||||
results.truncate(limit);
|
results.sort_by(|a, b| {
|
||||||
|
let da = graph.degree(&a.key);
|
||||||
|
let db = graph.degree(&b.key);
|
||||||
|
db.cmp(&da)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract the output stages from a parsed query (for cmd_query formatting)
|
||||||
|
pub fn output_stages(query_str: &str) -> Result<Vec<Stage>, String> {
|
||||||
|
let q = query_parser::query(query_str)
|
||||||
|
.map_err(|e| format!("Parse error: {}", e))?;
|
||||||
|
Ok(q.stages)
|
||||||
|
}
|
||||||
|
|
||||||
/// Format a field value for display
|
/// Format a field value for display
|
||||||
pub fn format_field(field: &str, key: &str, store: &Store, graph: &Graph) -> String {
|
pub fn format_field(field: &str, key: &str, store: &Store, graph: &Graph) -> String {
|
||||||
match resolve_field(field, key, store, graph) {
|
match resolve_field(field, key, store, graph) {
|
||||||
|
|
@ -409,7 +472,6 @@ pub fn format_field(field: &str, key: &str, store: &Store, graph: &Graph) -> Str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Value::Str(s)) => {
|
Some(Value::Str(s)) => {
|
||||||
// Truncate content for display
|
|
||||||
if field == "content" {
|
if field == "content" {
|
||||||
let truncated: String = s.chars().take(80).collect();
|
let truncated: String = s.chars().take(80).collect();
|
||||||
if s.len() > 80 { format!("{}...", truncated) } else { truncated }
|
if s.len() > 80 { format!("{}...", truncated) } else { truncated }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue