sqlitegraph_cli/
dsl.rs

1use sqlitegraph::{
2    backend::BackendDirection,
3    pattern::{NodeConstraint, PatternLeg, PatternQuery},
4    pipeline::{ReasoningPipeline, ReasoningStep},
5    subgraph::SubgraphRequest,
6};
7
8#[derive(Debug, Clone)]
9pub enum DslResult {
10    Pattern(PatternQuery),
11    Pipeline(ReasoningPipeline),
12    Subgraph(SubgraphRequest),
13    Error(String),
14}
15
16pub fn parse_dsl(input: &str) -> DslResult {
17    let trimmed = input.trim();
18    if trimmed.is_empty() {
19        return DslResult::Error("empty DSL string".into());
20    }
21    if trimmed.starts_with("pattern ") {
22        return parse_pattern_pipeline(trimmed);
23    }
24    if let Some(result) = parse_hop_command(trimmed) {
25        return result;
26    }
27    if trimmed.contains("->") || trimmed.contains('*') {
28        return DslResult::Pattern(parse_repetitive_pattern(trimmed));
29    }
30    DslResult::Error(format!("unsupported DSL form: {trimmed}"))
31}
32
33fn parse_pattern_pipeline(input: &str) -> DslResult {
34    let rest = input.trim_start_matches("pattern").trim();
35    let mut segments = rest.splitn(2, "filter");
36    let pattern_part = segments.next().unwrap_or("").trim();
37    if pattern_part.is_empty() {
38        return DslResult::Error("missing pattern segment".into());
39    }
40    let query = parse_repetitive_pattern(pattern_part);
41    let mut steps = vec![ReasoningStep::Pattern(query)];
42    if let Some(filter_part) = segments.next() {
43        let text = filter_part.trim();
44        let mut tokens = text.split_whitespace();
45        let clause = tokens.next().unwrap_or("");
46        if tokens.next().is_some() {
47            return DslResult::Error("multiple filter clauses are not supported".into());
48        }
49        if let Some(kind) = clause.strip_prefix("type=") {
50            let trimmed = kind.trim();
51            if trimmed.is_empty() {
52                return DslResult::Error("unsupported filter clause".into());
53            }
54            steps.push(ReasoningStep::Filter(NodeConstraint::kind(trimmed)));
55        } else {
56            return DslResult::Error("unsupported filter clause".into());
57        }
58    }
59    DslResult::Pipeline(ReasoningPipeline { steps })
60}
61
62fn parse_repetitive_pattern(segment: &str) -> PatternQuery {
63    let mut legs = Vec::new();
64    for token in segment.split("->") {
65        let trimmed = token.trim();
66        if trimmed.is_empty() {
67            continue;
68        }
69        let (ty, count) = if let Some((name, times)) = trimmed.split_once('*') {
70            let repeats = times.trim().parse::<usize>().unwrap_or(1);
71            (name.trim(), repeats)
72        } else {
73            (trimmed, 1)
74        };
75        for _ in 0..count {
76            legs.push(PatternLeg {
77                direction: BackendDirection::Outgoing,
78                edge_type: Some(ty.to_string()),
79                constraint: None,
80            });
81        }
82    }
83    PatternQuery { root: None, legs }
84}
85
86fn parse_hop_command(input: &str) -> Option<DslResult> {
87    if let Some((prefix, suffix)) = input.split_once("-hop") {
88        let depth = prefix.trim().parse::<u32>().ok()?;
89        let mut allowed = Vec::new();
90        if let Some(filter) = suffix.trim().strip_prefix("type=") {
91            allowed.push(filter.trim().to_string());
92        }
93        return Some(DslResult::Subgraph(SubgraphRequest {
94            root: 0,
95            depth,
96            allowed_edge_types: Vec::new(),
97            allowed_node_types: allowed,
98        }));
99    }
100    None
101}