1use serde::Serialize;
34
35use crate::engine::{knowledge_graph, GraphEdge, GraphNode, KnowledgeGraph};
36
37#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct LinksQuery {
40 pub source: NodePattern,
41 pub edge: Option<EdgePattern>,
42 pub target: Option<NodePattern>,
43 pub filters: Vec<Filter>,
44 pub returns: Vec<String>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct NodePattern {
50 pub var: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct EdgePattern {
56 pub var: Option<String>,
57 pub role: Option<String>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Filter {
63 pub var: String,
64 pub field: Field,
65 pub op: FilterOp,
66 pub value: String,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum Field {
72 Id,
73 Label,
74 Role,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum FilterOp {
80 Eq,
81 Contains,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct LinksQueryError {
87 pub message: String,
88}
89
90impl std::fmt::Display for LinksQueryError {
91 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(formatter, "{}", self.message)
93 }
94}
95
96impl std::error::Error for LinksQueryError {}
97
98fn err(message: impl Into<String>) -> LinksQueryError {
99 LinksQueryError {
100 message: message.into(),
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
106pub struct LinksQueryResult {
107 pub query: String,
108 pub nodes: Vec<GraphNode>,
109 pub edges: Vec<GraphEdge>,
110}
111
112impl LinksQueryResult {
113 #[must_use]
116 pub fn to_links_notation(&self) -> String {
117 let mut out = String::from("links_query_result\n");
118 out.push_str(" query \"");
119 out.push_str(&escape(&self.query));
120 out.push_str("\"\n");
121 for node in &self.nodes {
122 out.push_str(" node \"");
123 out.push_str(&escape(&node.id));
124 out.push_str("\"\n label \"");
125 out.push_str(&escape(&node.label));
126 out.push_str("\"\n links_notation \"");
127 out.push_str(&escape(&node.links_notation));
128 out.push_str("\"\n");
129 }
130 for edge in &self.edges {
131 out.push_str(" edge\n from \"");
132 out.push_str(&escape(&edge.from));
133 out.push_str("\"\n to \"");
134 out.push_str(&escape(&edge.to));
135 out.push_str("\"\n role \"");
136 out.push_str(&escape(&edge.role));
137 out.push_str("\"\n");
138 }
139 out
140 }
141}
142
143impl KnowledgeGraph {
144 #[must_use]
149 pub fn to_links_notation(&self) -> String {
150 let mut out = String::from("knowledge_graph\n");
151 for node in &self.nodes {
152 out.push_str(" node \"");
153 out.push_str(&escape(&node.id));
154 out.push_str("\"\n label \"");
155 out.push_str(&escape(&node.label));
156 out.push_str("\"\n links_notation \"");
157 out.push_str(&escape(&node.links_notation));
158 out.push_str("\"\n");
159 }
160 for edge in &self.edges {
161 out.push_str(" edge\n from \"");
162 out.push_str(&escape(&edge.from));
163 out.push_str("\"\n to \"");
164 out.push_str(&escape(&edge.to));
165 out.push_str("\"\n role \"");
166 out.push_str(&escape(&edge.role));
167 out.push_str("\"\n");
168 }
169 out
170 }
171}
172
173fn escape(value: &str) -> String {
174 value.replace('\\', "\\\\").replace('"', "\\\"")
175}
176
177pub fn run_links_query(query: &str) -> Result<LinksQueryResult, LinksQueryError> {
182 run_links_query_against(query, &knowledge_graph())
183}
184
185pub fn run_links_query_against(
190 query: &str,
191 graph: &KnowledgeGraph,
192) -> Result<LinksQueryResult, LinksQueryError> {
193 let parsed = parse_links_query(query)?;
194 Ok(evaluate(&parsed, query, graph))
195}
196
197pub fn parse_links_query(query: &str) -> Result<LinksQuery, LinksQueryError> {
202 let trimmed = query.trim();
203 let upper = trimmed.to_uppercase();
204 let match_start = upper
205 .find("MATCH")
206 .ok_or_else(|| err("query must contain a MATCH clause"))?;
207 let return_start = upper
208 .find("RETURN")
209 .ok_or_else(|| err("query must contain a RETURN clause"))?;
210 if return_start < match_start {
211 return Err(err("RETURN must follow MATCH"));
212 }
213
214 let after_match = &trimmed[match_start + "MATCH".len()..return_start];
215 let returns_text = &trimmed[return_start + "RETURN".len()..];
216
217 let (pattern_text, where_text) =
218 upper[match_start..return_start]
219 .find("WHERE")
220 .map_or((after_match, None), |relative| {
221 let absolute = match_start + relative;
222 (
223 &trimmed[match_start + "MATCH".len()..absolute],
224 Some(&trimmed[absolute + "WHERE".len()..return_start]),
225 )
226 });
227
228 let (source, edge, target) = parse_pattern(pattern_text.trim())?;
229 let filters = match where_text {
230 Some(text) => parse_filters(text.trim())?,
231 None => Vec::new(),
232 };
233 let returns = returns_text
234 .split(',')
235 .map(|item| {
236 item.trim()
237 .split('.')
238 .next()
239 .unwrap_or("")
240 .trim()
241 .to_owned()
242 })
243 .filter(|item| !item.is_empty())
244 .collect::<Vec<_>>();
245 if returns.is_empty() {
246 return Err(err("RETURN must name at least one variable"));
247 }
248
249 Ok(LinksQuery {
250 source,
251 edge,
252 target,
253 filters,
254 returns,
255 })
256}
257
258fn parse_pattern(
259 text: &str,
260) -> Result<(NodePattern, Option<EdgePattern>, Option<NodePattern>), LinksQueryError> {
261 let source_end = text
262 .find(')')
263 .ok_or_else(|| err("pattern must open with a node like (a)"))?;
264 let source = parse_node(&text[..=source_end])?;
265 let rest = text[source_end + 1..].trim();
266 if rest.is_empty() {
267 return Ok((source, None, None));
268 }
269
270 let arrow = "->";
272 let arrow_pos = rest
273 .find(arrow)
274 .ok_or_else(|| err("edge pattern must end with ->"))?;
275 let edge_text = rest[..arrow_pos].trim();
276 let target_text = rest[arrow_pos + arrow.len()..].trim();
277 let edge = parse_edge(edge_text)?;
278 let target = parse_node(target_text)?;
279 Ok((source, Some(edge), Some(target)))
280}
281
282fn parse_node(text: &str) -> Result<NodePattern, LinksQueryError> {
283 let inner = text
284 .trim()
285 .strip_prefix('(')
286 .and_then(|value| value.strip_suffix(')'))
287 .ok_or_else(|| err(format!("invalid node pattern: {text}")))?;
288 let var = inner.split(':').next().unwrap_or("").trim().to_owned();
289 if var.is_empty() {
290 return Err(err("node pattern needs a variable name"));
291 }
292 Ok(NodePattern { var })
293}
294
295fn parse_edge(text: &str) -> Result<EdgePattern, LinksQueryError> {
296 let inner = text
297 .trim()
298 .strip_prefix('-')
299 .unwrap_or(text)
300 .trim()
301 .strip_prefix('[')
302 .and_then(|value| value.strip_suffix(']'))
303 .ok_or_else(|| err(format!("invalid edge pattern: {text}")))?;
304 let inner = inner.trim();
305 let (var_text, role_text) = if let Some((var, role)) = inner.split_once(':') {
306 (var.trim(), Some(role.trim()))
307 } else {
308 (inner, None)
309 };
310 let var = (!var_text.is_empty()).then(|| var_text.to_owned());
311 let role = role_text.and_then(|role| (!role.is_empty()).then(|| role.to_owned()));
312 Ok(EdgePattern { var, role })
313}
314
315fn parse_filters(text: &str) -> Result<Vec<Filter>, LinksQueryError> {
316 let mut filters = Vec::new();
317 for clause in split_filters(text) {
318 let clause = clause.trim();
319 if clause.is_empty() {
320 continue;
321 }
322 filters.push(parse_filter(clause)?);
323 }
324 Ok(filters)
325}
326
327fn split_filters(text: &str) -> Vec<String> {
328 let mut parts = Vec::new();
330 let mut current = String::new();
331 let mut in_quotes = false;
332 let chars: Vec<char> = text.chars().collect();
333 let mut index = 0;
334 while index < chars.len() {
335 let ch = chars[index];
336 if ch == '"' {
337 in_quotes = !in_quotes;
338 current.push(ch);
339 index += 1;
340 continue;
341 }
342 if !in_quotes {
343 let remaining: String = chars[index..].iter().collect();
344 let upper = remaining.to_uppercase();
345 if upper.starts_with("AND") && is_boundary(chars.get(index + 3).copied()) {
346 parts.push(std::mem::take(&mut current));
347 index += 3;
348 continue;
349 }
350 }
351 current.push(ch);
352 index += 1;
353 }
354 parts.push(current);
355 parts
356}
357
358fn is_boundary(ch: Option<char>) -> bool {
359 ch.is_none_or(char::is_whitespace)
360}
361
362fn parse_filter(clause: &str) -> Result<Filter, LinksQueryError> {
363 let (op, split_at, op_len) = find_operator(clause)
367 .ok_or_else(|| err(format!("filter needs `=` or CONTAINS: {clause}")))?;
368 let lhs = clause[..split_at].trim();
369 let rhs = clause[split_at + op_len..].trim();
370
371 let (var, field_text) = lhs
372 .split_once('.')
373 .ok_or_else(|| err(format!("filter target must be `var.field`: {clause}")))?;
374 let field = match field_text.trim().to_lowercase().as_str() {
375 "id" => Field::Id,
376 "label" => Field::Label,
377 "role" => Field::Role,
378 other => return Err(err(format!("unknown field `{other}`"))),
379 };
380 let value = rhs.trim().trim_matches('"').to_owned();
381 Ok(Filter {
382 var: var.trim().to_owned(),
383 field,
384 op,
385 value,
386 })
387}
388
389fn find_operator(clause: &str) -> Option<(FilterOp, usize, usize)> {
395 let bytes = clause.as_bytes();
396 let upper = clause.to_uppercase();
397 let upper_bytes = upper.as_bytes();
398 let mut in_quotes = false;
399 let mut index = 0;
400 while index < bytes.len() {
401 let byte = bytes[index];
402 if byte == b'"' {
403 in_quotes = !in_quotes;
404 index += 1;
405 continue;
406 }
407 if !in_quotes {
408 if byte == b'=' {
409 return Some((FilterOp::Eq, index, 1));
410 }
411 if upper_bytes[index..].starts_with(b"CONTAINS") {
412 return Some((FilterOp::Contains, index, "CONTAINS".len()));
413 }
414 }
415 index += 1;
416 }
417 None
418}
419
420fn evaluate(query: &LinksQuery, source_text: &str, graph: &KnowledgeGraph) -> LinksQueryResult {
421 let mut result = LinksQueryResult {
422 query: source_text.trim().to_owned(),
423 nodes: Vec::new(),
424 edges: Vec::new(),
425 };
426
427 if query.edge.is_none() {
428 for node in &graph.nodes {
431 if node_matches(node, &query.source.var, &query.filters) {
432 push_node(&mut result.nodes, node.clone());
433 }
434 }
435 return result;
436 }
437
438 let edge_pattern = query.edge.as_ref().expect("edge present");
439 let target = query.target.as_ref().expect("target present");
440 let want_source = query.returns.iter().any(|var| var == &query.source.var);
441 let want_target = query.returns.iter().any(|var| var == &target.var);
442
443 for edge in &graph.edges {
444 if let Some(role) = &edge_pattern.role {
445 if &edge.role != role {
446 continue;
447 }
448 }
449 let from_node = graph.nodes.iter().find(|node| node.id == edge.from);
450 let to_node = graph.nodes.iter().find(|node| node.id == edge.to);
451
452 if !node_filters_match(from_node, &query.source.var, &query.filters)
453 || !node_filters_match(to_node, &target.var, &query.filters)
454 || !edge_filters_match(edge, edge_pattern.var.as_deref(), &query.filters)
455 {
456 continue;
457 }
458
459 result.edges.push(edge.clone());
460 if want_source {
461 if let Some(node) = from_node {
462 push_node(&mut result.nodes, node.clone());
463 }
464 }
465 if want_target {
466 if let Some(node) = to_node {
467 push_node(&mut result.nodes, node.clone());
468 }
469 }
470 }
471 result
472}
473
474fn push_node(nodes: &mut Vec<GraphNode>, node: GraphNode) {
475 if !nodes.iter().any(|existing| existing.id == node.id) {
476 nodes.push(node);
477 }
478}
479
480fn node_matches(node: &GraphNode, var: &str, filters: &[Filter]) -> bool {
481 filters
482 .iter()
483 .filter(|filter| filter.var == var)
484 .all(|filter| apply_node_filter(node, filter))
485}
486
487fn node_filters_match(node: Option<&GraphNode>, var: &str, filters: &[Filter]) -> bool {
488 let relevant: Vec<&Filter> = filters
489 .iter()
490 .filter(|filter| filter.var == var && !matches!(filter.field, Field::Role))
491 .collect();
492 if relevant.is_empty() {
493 return true;
494 }
495 let Some(node) = node else {
496 return false;
497 };
498 relevant
499 .iter()
500 .all(|filter| apply_node_filter(node, filter))
501}
502
503fn edge_filters_match(edge: &GraphEdge, var: Option<&str>, filters: &[Filter]) -> bool {
504 let Some(var) = var else {
505 return !filters
506 .iter()
507 .any(|filter| matches!(filter.field, Field::Role) && filter.var != "__none__");
508 };
509 filters
510 .iter()
511 .filter(|filter| filter.var == var)
512 .all(|filter| match filter.op {
513 FilterOp::Eq => edge.role == filter.value,
514 FilterOp::Contains => edge.role.contains(&filter.value),
515 })
516}
517
518fn apply_node_filter(node: &GraphNode, filter: &Filter) -> bool {
519 let haystack = match filter.field {
520 Field::Id => &node.id,
521 Field::Label => &node.label,
522 Field::Role => return true,
523 };
524 match filter.op {
525 FilterOp::Eq => haystack == &filter.value,
526 FilterOp::Contains => haystack.contains(&filter.value),
527 }
528}