Skip to main content

reddb_server/storage/query/modes/
gremlin.rs

1//! Gremlin Traversal Parser
2//!
3//! Parses TinkerPop-style Gremlin queries like:
4//! - `g.V().hasLabel('host').out('connects')`
5//! - `g.V('host:10.0.0.1').repeat(out()).times(3).path()`
6//! - `__.out('knows').has('name', 'bob')`
7//!
8//! # Supported Steps
9//!
10//! ## Source Steps
11//! - `V()`, `V(id)` - Get vertices
12//! - `E()`, `E(id)` - Get edges
13//!
14//! ## Traversal Steps
15//! - `out(label?)`, `in(label?)`, `both(label?)` - Traverse edges
16//! - `outE(label?)`, `inE(label?)`, `bothE(label?)` - Get edges
17//! - `outV()`, `inV()`, `bothV()`, `otherV()` - Edge to vertex
18//!
19//! ## Filter Steps
20//! - `has(key, value)`, `has(key)`, `hasNot(key)`
21//! - `hasLabel(label)`, `hasId(id)`
22//! - `where(predicate)`, `filter(traversal)`
23//! - `dedup()`, `limit(n)`, `skip(n)`, `range(from, to)`
24//!
25//! ## Map Steps
26//! - `values(keys...)`, `valueMap(keys...)`
27//! - `id()`, `label()`, `properties(keys...)`
28//! - `count()`, `sum()`, `min()`, `max()`, `mean()`
29//! - `select(labels...)`, `project(keys...)`
30//! - `path()`, `simplePath()`, `cyclicPath()`
31//!
32//! ## Branch Steps
33//! - `repeat(traversal).times(n)`, `repeat(traversal).until(predicate)`
34//! - `union(traversal...)`, `choose(predicate, true_traversal, false_traversal)`
35//! - `coalesce(traversal...)`
36//!
37//! ## Side Effect Steps
38//! - `as(label)`, `by(key|traversal)`
39//! - `aggregate(label)`, `store(label)`
40//! - `group()`, `groupCount()`
41
42use crate::storage::query::ast::{
43    CompareOp, EdgeDirection, EdgePattern, FieldRef, Filter, GraphPattern, GraphQuery, NodePattern,
44    Projection, PropertyFilter, QueryExpr,
45};
46use crate::storage::schema::Value;
47
48/// Gremlin parse error
49#[derive(Debug, Clone)]
50pub struct GremlinError {
51    pub message: String,
52    pub position: usize,
53}
54
55impl std::fmt::Display for GremlinError {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        write!(f, "Gremlin error at {}: {}", self.position, self.message)
58    }
59}
60
61impl std::error::Error for GremlinError {}
62
63/// A Gremlin traversal is a sequence of steps
64#[derive(Debug, Clone)]
65pub struct GremlinTraversal {
66    /// The source (g or __)
67    pub source: TraversalSource,
68    /// Steps in the traversal
69    pub steps: Vec<GremlinStep>,
70}
71
72/// Traversal source
73#[derive(Debug, Clone, PartialEq)]
74pub enum TraversalSource {
75    /// Graph source: g.V(), g.E()
76    Graph,
77    /// Anonymous traversal: __.out()
78    Anonymous,
79}
80
81/// A single step in a Gremlin traversal
82#[derive(Debug, Clone)]
83pub enum GremlinStep {
84    // Source steps
85    V(Option<String>), // V() or V(id)
86    E(Option<String>), // E() or E(id)
87
88    // Traversal steps
89    Out(Option<String>),   // out() or out('label')
90    In(Option<String>),    // in() or in('label')
91    Both(Option<String>),  // both() or both('label')
92    OutE(Option<String>),  // outE() or outE('label')
93    InE(Option<String>),   // inE() or inE('label')
94    BothE(Option<String>), // bothE() or bothE('label')
95    OutV,                  // outV()
96    InV,                   // inV()
97    BothV,                 // bothV()
98    OtherV,                // otherV()
99
100    // Filter steps
101    Has(String, Option<GremlinValue>), // has('key') or has('key', value)
102    HasNot(String),                    // hasNot('key')
103    HasLabel(String),                  // hasLabel('label')
104    HasId(String),                     // hasId('id')
105    Where(Box<GremlinTraversal>),      // where(traversal)
106    Filter(Box<GremlinTraversal>),     // filter(traversal)
107    Dedup,                             // dedup()
108    Limit(u64),                        // limit(n)
109    Skip(u64),                         // skip(n)
110    Range(u64, u64),                   // range(from, to)
111
112    // Map steps
113    Values(Vec<String>),     // values('key1', 'key2')
114    ValueMap(Vec<String>),   // valueMap('key1', 'key2')
115    Id,                      // id()
116    Label,                   // label()
117    Properties(Vec<String>), // properties('key1', 'key2')
118    Count,                   // count()
119    Sum,                     // sum()
120    Min,                     // min()
121    Max,                     // max()
122    Mean,                    // mean()
123    Select(Vec<String>),     // select('a', 'b')
124    Project(Vec<String>),    // project('key1', 'key2')
125    Path,                    // path()
126    SimplePath,              // simplePath()
127    CyclicPath,              // cyclicPath()
128
129    // Branch steps
130    Repeat(Box<GremlinTraversal>), // repeat(traversal)
131    Times(u32),                    // times(n) - modifier for repeat
132    Until(Box<GremlinTraversal>),  // until(predicate) - modifier for repeat
133    Emit,                          // emit() - modifier for repeat
134    Union(Vec<GremlinTraversal>),  // union(t1, t2, ...)
135    Choose(
136        Box<GremlinTraversal>,
137        Box<GremlinTraversal>,
138        Option<Box<GremlinTraversal>>,
139    ),
140    Coalesce(Vec<GremlinTraversal>), // coalesce(t1, t2, ...)
141
142    // Side effect steps
143    As(String),        // as('label')
144    By(ByModifier),    // by('key') or by(traversal)
145    Aggregate(String), // aggregate('label')
146    Store(String),     // store('label')
147    Group,             // group()
148    GroupCount,        // groupCount()
149
150    // Terminal steps
151    ToList, // toList()
152    ToSet,  // toSet()
153    Next,   // next()
154    Fold,   // fold()
155}
156
157/// Value in Gremlin predicates
158#[derive(Debug, Clone, PartialEq)]
159pub enum GremlinValue {
160    String(String),
161    Integer(i64),
162    Float(f64),
163    Boolean(bool),
164    Predicate(GremlinPredicate),
165}
166
167/// Gremlin predicates for comparisons
168#[derive(Debug, Clone, PartialEq)]
169pub enum GremlinPredicate {
170    Eq(Box<GremlinValue>),                         // eq(value)
171    Neq(Box<GremlinValue>),                        // neq(value)
172    Lt(Box<GremlinValue>),                         // lt(value)
173    Lte(Box<GremlinValue>),                        // lte(value)
174    Gt(Box<GremlinValue>),                         // gt(value)
175    Gte(Box<GremlinValue>),                        // gte(value)
176    Between(Box<GremlinValue>, Box<GremlinValue>), // between(a, b)
177    Inside(Box<GremlinValue>, Box<GremlinValue>),  // inside(a, b)
178    Outside(Box<GremlinValue>, Box<GremlinValue>), // outside(a, b)
179    Within(Vec<GremlinValue>),                     // within(a, b, c)
180    Without(Vec<GremlinValue>),                    // without(a, b, c)
181    StartingWith(String),                          // startingWith('prefix')
182    EndingWith(String),                            // endingWith('suffix')
183    Containing(String),                            // containing('substring')
184    Regex(String),                                 // regex('pattern')
185}
186
187/// By modifier for grouping/ordering
188#[derive(Debug, Clone)]
189pub enum ByModifier {
190    Key(String),
191    Traversal(Box<GremlinTraversal>),
192    Order(OrderDirection),
193}
194
195/// Order direction for by()
196#[derive(Debug, Clone)]
197pub enum OrderDirection {
198    Asc,
199    Desc,
200}
201
202/// Gremlin parser
203pub struct GremlinParser<'a> {
204    input: &'a str,
205    pos: usize,
206}
207
208impl<'a> GremlinParser<'a> {
209    /// Create a new parser
210    pub fn new(input: &'a str) -> Self {
211        Self { input, pos: 0 }
212    }
213
214    /// Parse a Gremlin query string
215    pub fn parse(input: &str) -> Result<GremlinTraversal, GremlinError> {
216        let mut parser = GremlinParser::new(input);
217        parser.parse_traversal()
218    }
219
220    /// Parse a full traversal
221    fn parse_traversal(&mut self) -> Result<GremlinTraversal, GremlinError> {
222        self.skip_whitespace();
223
224        // Determine source
225        let source = if self.consume_if("g.") {
226            TraversalSource::Graph
227        } else if self.consume_if("__.") {
228            TraversalSource::Anonymous
229        } else if self.consume_if("__") {
230            // Just __ without dot - valid anonymous start
231            TraversalSource::Anonymous
232        } else {
233            return Err(self.error("Expected 'g.' or '__' at start of traversal"));
234        };
235
236        let mut steps = Vec::new();
237
238        // Parse steps
239        loop {
240            self.skip_whitespace();
241
242            if self.is_at_end() || self.peek() == Some(')') || self.peek() == Some(',') {
243                break;
244            }
245
246            // Skip dots between steps
247            self.consume_if(".");
248
249            self.skip_whitespace();
250
251            if self.is_at_end() || self.peek() == Some(')') || self.peek() == Some(',') {
252                break;
253            }
254
255            let step = self.parse_step()?;
256            steps.push(step);
257        }
258
259        Ok(GremlinTraversal { source, steps })
260    }
261
262    /// Parse a single step
263    fn parse_step(&mut self) -> Result<GremlinStep, GremlinError> {
264        let name = self.parse_identifier()?;
265
266        match name.as_str() {
267            // Source steps
268            "V" => {
269                self.expect('(')?;
270                let id = self.parse_optional_string_arg()?;
271                self.expect(')')?;
272                Ok(GremlinStep::V(id))
273            }
274            "E" => {
275                self.expect('(')?;
276                let id = self.parse_optional_string_arg()?;
277                self.expect(')')?;
278                Ok(GremlinStep::E(id))
279            }
280
281            // Traversal steps
282            "out" => {
283                self.expect('(')?;
284                let label = self.parse_optional_string_arg()?;
285                self.expect(')')?;
286                Ok(GremlinStep::Out(label))
287            }
288            "in" => {
289                self.expect('(')?;
290                let label = self.parse_optional_string_arg()?;
291                self.expect(')')?;
292                Ok(GremlinStep::In(label))
293            }
294            "both" => {
295                self.expect('(')?;
296                let label = self.parse_optional_string_arg()?;
297                self.expect(')')?;
298                Ok(GremlinStep::Both(label))
299            }
300            "outE" => {
301                self.expect('(')?;
302                let label = self.parse_optional_string_arg()?;
303                self.expect(')')?;
304                Ok(GremlinStep::OutE(label))
305            }
306            "inE" => {
307                self.expect('(')?;
308                let label = self.parse_optional_string_arg()?;
309                self.expect(')')?;
310                Ok(GremlinStep::InE(label))
311            }
312            "bothE" => {
313                self.expect('(')?;
314                let label = self.parse_optional_string_arg()?;
315                self.expect(')')?;
316                Ok(GremlinStep::BothE(label))
317            }
318            "outV" => {
319                self.expect('(')?;
320                self.expect(')')?;
321                Ok(GremlinStep::OutV)
322            }
323            "inV" => {
324                self.expect('(')?;
325                self.expect(')')?;
326                Ok(GremlinStep::InV)
327            }
328            "bothV" => {
329                self.expect('(')?;
330                self.expect(')')?;
331                Ok(GremlinStep::BothV)
332            }
333            "otherV" => {
334                self.expect('(')?;
335                self.expect(')')?;
336                Ok(GremlinStep::OtherV)
337            }
338
339            // Filter steps
340            "has" => {
341                self.expect('(')?;
342                let key = self.parse_string()?;
343                self.skip_whitespace();
344                let value = if self.consume_if(",") {
345                    self.skip_whitespace();
346                    Some(self.parse_value()?)
347                } else {
348                    None
349                };
350                self.expect(')')?;
351                Ok(GremlinStep::Has(key, value))
352            }
353            "hasNot" => {
354                self.expect('(')?;
355                let key = self.parse_string()?;
356                self.expect(')')?;
357                Ok(GremlinStep::HasNot(key))
358            }
359            "hasLabel" => {
360                self.expect('(')?;
361                let label = self.parse_string()?;
362                self.expect(')')?;
363                Ok(GremlinStep::HasLabel(label))
364            }
365            "hasId" => {
366                self.expect('(')?;
367                let id = self.parse_string()?;
368                self.expect(')')?;
369                Ok(GremlinStep::HasId(id))
370            }
371            "dedup" => {
372                self.expect('(')?;
373                self.expect(')')?;
374                Ok(GremlinStep::Dedup)
375            }
376            "limit" => {
377                self.expect('(')?;
378                let n = self.parse_integer()? as u64;
379                self.expect(')')?;
380                Ok(GremlinStep::Limit(n))
381            }
382            "skip" => {
383                self.expect('(')?;
384                let n = self.parse_integer()? as u64;
385                self.expect(')')?;
386                Ok(GremlinStep::Skip(n))
387            }
388            "range" => {
389                self.expect('(')?;
390                let from = self.parse_integer()? as u64;
391                self.expect(',')?;
392                self.skip_whitespace();
393                let to = self.parse_integer()? as u64;
394                self.expect(')')?;
395                Ok(GremlinStep::Range(from, to))
396            }
397
398            // Map steps
399            "values" => {
400                self.expect('(')?;
401                let keys = self.parse_string_list()?;
402                self.expect(')')?;
403                Ok(GremlinStep::Values(keys))
404            }
405            "valueMap" => {
406                self.expect('(')?;
407                let keys = self.parse_string_list()?;
408                self.expect(')')?;
409                Ok(GremlinStep::ValueMap(keys))
410            }
411            "id" => {
412                self.expect('(')?;
413                self.expect(')')?;
414                Ok(GremlinStep::Id)
415            }
416            "label" => {
417                self.expect('(')?;
418                self.expect(')')?;
419                Ok(GremlinStep::Label)
420            }
421            "properties" => {
422                self.expect('(')?;
423                let keys = self.parse_string_list()?;
424                self.expect(')')?;
425                Ok(GremlinStep::Properties(keys))
426            }
427            "count" => {
428                self.expect('(')?;
429                self.expect(')')?;
430                Ok(GremlinStep::Count)
431            }
432            "sum" => {
433                self.expect('(')?;
434                self.expect(')')?;
435                Ok(GremlinStep::Sum)
436            }
437            "min" => {
438                self.expect('(')?;
439                self.expect(')')?;
440                Ok(GremlinStep::Min)
441            }
442            "max" => {
443                self.expect('(')?;
444                self.expect(')')?;
445                Ok(GremlinStep::Max)
446            }
447            "mean" => {
448                self.expect('(')?;
449                self.expect(')')?;
450                Ok(GremlinStep::Mean)
451            }
452            "select" => {
453                self.expect('(')?;
454                let labels = self.parse_string_list()?;
455                self.expect(')')?;
456                Ok(GremlinStep::Select(labels))
457            }
458            "project" => {
459                self.expect('(')?;
460                let keys = self.parse_string_list()?;
461                self.expect(')')?;
462                Ok(GremlinStep::Project(keys))
463            }
464            "path" => {
465                self.expect('(')?;
466                self.expect(')')?;
467                Ok(GremlinStep::Path)
468            }
469            "simplePath" => {
470                self.expect('(')?;
471                self.expect(')')?;
472                Ok(GremlinStep::SimplePath)
473            }
474            "cyclicPath" => {
475                self.expect('(')?;
476                self.expect(')')?;
477                Ok(GremlinStep::CyclicPath)
478            }
479
480            // Branch steps
481            "repeat" => {
482                self.expect('(')?;
483                let inner = self.parse_inner_traversal()?;
484                self.expect(')')?;
485                Ok(GremlinStep::Repeat(Box::new(inner)))
486            }
487            "times" => {
488                self.expect('(')?;
489                let n = self.parse_integer()? as u32;
490                self.expect(')')?;
491                Ok(GremlinStep::Times(n))
492            }
493            "until" => {
494                self.expect('(')?;
495                let inner = self.parse_inner_traversal()?;
496                self.expect(')')?;
497                Ok(GremlinStep::Until(Box::new(inner)))
498            }
499            "emit" => {
500                self.expect('(')?;
501                self.expect(')')?;
502                Ok(GremlinStep::Emit)
503            }
504
505            // Side effect steps
506            "as" => {
507                self.expect('(')?;
508                let label = self.parse_string()?;
509                self.expect(')')?;
510                Ok(GremlinStep::As(label))
511            }
512            "aggregate" => {
513                self.expect('(')?;
514                let label = self.parse_string()?;
515                self.expect(')')?;
516                Ok(GremlinStep::Aggregate(label))
517            }
518            "store" => {
519                self.expect('(')?;
520                let label = self.parse_string()?;
521                self.expect(')')?;
522                Ok(GremlinStep::Store(label))
523            }
524            "group" => {
525                self.expect('(')?;
526                self.expect(')')?;
527                Ok(GremlinStep::Group)
528            }
529            "groupCount" => {
530                self.expect('(')?;
531                self.expect(')')?;
532                Ok(GremlinStep::GroupCount)
533            }
534
535            // Terminal steps
536            "toList" => {
537                self.expect('(')?;
538                self.expect(')')?;
539                Ok(GremlinStep::ToList)
540            }
541            "toSet" => {
542                self.expect('(')?;
543                self.expect(')')?;
544                Ok(GremlinStep::ToSet)
545            }
546            "next" => {
547                self.expect('(')?;
548                self.expect(')')?;
549                Ok(GremlinStep::Next)
550            }
551            "fold" => {
552                self.expect('(')?;
553                self.expect(')')?;
554                Ok(GremlinStep::Fold)
555            }
556
557            _ => Err(self.error(&format!("Unknown step: {}", name))),
558        }
559    }
560
561    /// Parse an inner traversal (for repeat, where, etc.)
562    fn parse_inner_traversal(&mut self) -> Result<GremlinTraversal, GremlinError> {
563        self.skip_whitespace();
564
565        // Check if it starts with __ or g.
566        if self.input[self.pos..].starts_with("__") || self.input[self.pos..].starts_with("g.") {
567            return self.parse_traversal();
568        }
569
570        // Otherwise, create anonymous traversal from steps
571        let mut steps = Vec::new();
572
573        loop {
574            self.skip_whitespace();
575
576            if self.is_at_end() || self.peek() == Some(')') {
577                break;
578            }
579
580            // Skip dots between steps
581            self.consume_if(".");
582
583            self.skip_whitespace();
584
585            if self.is_at_end() || self.peek() == Some(')') {
586                break;
587            }
588
589            let step = self.parse_step()?;
590            steps.push(step);
591        }
592
593        Ok(GremlinTraversal {
594            source: TraversalSource::Anonymous,
595            steps,
596        })
597    }
598
599    // Helper methods
600
601    fn skip_whitespace(&mut self) {
602        while let Some(c) = self.peek() {
603            if c.is_whitespace() {
604                self.pos += 1;
605            } else {
606                break;
607            }
608        }
609    }
610
611    fn peek(&self) -> Option<char> {
612        self.input[self.pos..].chars().next()
613    }
614
615    fn is_at_end(&self) -> bool {
616        self.pos >= self.input.len()
617    }
618
619    fn consume_if(&mut self, s: &str) -> bool {
620        if self.input[self.pos..].starts_with(s) {
621            self.pos += s.len();
622            true
623        } else {
624            false
625        }
626    }
627
628    fn expect(&mut self, c: char) -> Result<(), GremlinError> {
629        self.skip_whitespace();
630        if self.peek() == Some(c) {
631            self.pos += 1;
632            Ok(())
633        } else {
634            Err(self.error(&format!("Expected '{}', found {:?}", c, self.peek())))
635        }
636    }
637
638    fn parse_identifier(&mut self) -> Result<String, GremlinError> {
639        self.skip_whitespace();
640
641        let start = self.pos;
642        while let Some(c) = self.peek() {
643            if c.is_alphanumeric() || c == '_' {
644                self.pos += 1;
645            } else {
646                break;
647            }
648        }
649
650        if self.pos == start {
651            Err(self.error("Expected identifier"))
652        } else {
653            Ok(self.input[start..self.pos].to_string())
654        }
655    }
656
657    fn parse_string(&mut self) -> Result<String, GremlinError> {
658        self.skip_whitespace();
659
660        let quote = self.peek();
661        if quote != Some('\'') && quote != Some('"') {
662            return Err(self.error("Expected string"));
663        }
664        self.pos += 1;
665
666        let start = self.pos;
667        while let Some(c) = self.peek() {
668            if Some(c) == quote {
669                let s = self.input[start..self.pos].to_string();
670                self.pos += 1;
671                return Ok(s);
672            }
673            if c == '\\' {
674                self.pos += 2; // Skip escape
675            } else {
676                self.pos += 1;
677            }
678        }
679
680        Err(self.error("Unterminated string"))
681    }
682
683    fn parse_optional_string_arg(&mut self) -> Result<Option<String>, GremlinError> {
684        self.skip_whitespace();
685        if self.peek() == Some(')') {
686            Ok(None)
687        } else if self.peek() == Some('\'') || self.peek() == Some('"') {
688            Ok(Some(self.parse_string()?))
689        } else {
690            // Could be unquoted ID
691            let start = self.pos;
692            while let Some(c) = self.peek() {
693                if c.is_alphanumeric() || c == '_' || c == ':' || c == '.' || c == '-' {
694                    self.pos += 1;
695                } else {
696                    break;
697                }
698            }
699            if self.pos > start {
700                Ok(Some(self.input[start..self.pos].to_string()))
701            } else {
702                Ok(None)
703            }
704        }
705    }
706
707    fn parse_string_list(&mut self) -> Result<Vec<String>, GremlinError> {
708        let mut result = Vec::new();
709
710        self.skip_whitespace();
711        if self.peek() == Some(')') {
712            return Ok(result);
713        }
714
715        loop {
716            self.skip_whitespace();
717            if self.peek() == Some(')') {
718                break;
719            }
720
721            result.push(self.parse_string()?);
722
723            self.skip_whitespace();
724            if !self.consume_if(",") {
725                break;
726            }
727        }
728
729        Ok(result)
730    }
731
732    fn parse_integer(&mut self) -> Result<i64, GremlinError> {
733        self.skip_whitespace();
734
735        let start = self.pos;
736        if self.peek() == Some('-') {
737            self.pos += 1;
738        }
739
740        while let Some(c) = self.peek() {
741            if c.is_ascii_digit() {
742                self.pos += 1;
743            } else {
744                break;
745            }
746        }
747
748        let s = &self.input[start..self.pos];
749        s.parse()
750            .map_err(|_| self.error(&format!("Invalid integer: {}", s)))
751    }
752
753    fn parse_value(&mut self) -> Result<GremlinValue, GremlinError> {
754        self.skip_whitespace();
755
756        // String
757        if self.peek() == Some('\'') || self.peek() == Some('"') {
758            return Ok(GremlinValue::String(self.parse_string()?));
759        }
760
761        // Boolean
762        if self.consume_if("true") {
763            return Ok(GremlinValue::Boolean(true));
764        }
765        if self.consume_if("false") {
766            return Ok(GremlinValue::Boolean(false));
767        }
768
769        // Number
770        let start = self.pos;
771        if self.peek() == Some('-') {
772            self.pos += 1;
773        }
774        while let Some(c) = self.peek() {
775            if c.is_ascii_digit() || c == '.' {
776                self.pos += 1;
777            } else {
778                break;
779            }
780        }
781
782        let s = &self.input[start..self.pos];
783        if s.contains('.') {
784            let f: f64 = s
785                .parse()
786                .map_err(|_| self.error(&format!("Invalid float: {}", s)))?;
787            Ok(GremlinValue::Float(f))
788        } else {
789            let i: i64 = s
790                .parse()
791                .map_err(|_| self.error(&format!("Invalid integer: {}", s)))?;
792            Ok(GremlinValue::Integer(i))
793        }
794    }
795
796    fn error(&self, message: &str) -> GremlinError {
797        GremlinError {
798            message: message.to_string(),
799            position: self.pos,
800        }
801    }
802}
803
804impl GremlinTraversal {
805    /// Convert Gremlin traversal to QueryExpr
806    pub fn to_query_expr(&self) -> QueryExpr {
807        // Build graph pattern from steps
808        let mut nodes = Vec::new();
809        let mut edges = Vec::new();
810        let mut filters = Vec::new();
811        let mut projections = Vec::new();
812
813        let mut current_alias = "n0".to_string();
814        let mut alias_counter = 0;
815
816        for step in &self.steps {
817            match step {
818                GremlinStep::V(id) => {
819                    let mut node = NodePattern {
820                        alias: current_alias.clone(),
821                        node_label: None,
822                        properties: Vec::new(),
823                    };
824                    if let Some(id) = id {
825                        node.properties.push(PropertyFilter {
826                            name: "id".to_string(),
827                            op: CompareOp::Eq,
828                            value: Value::text(id.clone()),
829                        });
830                    }
831                    nodes.push(node);
832                }
833                GremlinStep::HasLabel(label) => {
834                    if let Some(last) = nodes.last_mut() {
835                        // Normalize common pentest aliases; otherwise pass
836                        // the user's label through unchanged.
837                        let lower = label.to_lowercase();
838                        last.node_label = Some(match lower.as_str() {
839                            "vuln" => "vulnerability".to_string(),
840                            "tech" => "technology".to_string(),
841                            "cert" => "certificate".to_string(),
842                            _ => lower,
843                        });
844                    }
845                }
846                GremlinStep::Has(key, value) => {
847                    let field_ref = FieldRef::NodeProperty {
848                        alias: current_alias.clone(),
849                        property: key.clone(),
850                    };
851                    let filter = if let Some(val) = value {
852                        Filter::Compare {
853                            field: field_ref,
854                            op: CompareOp::Eq,
855                            value: match val {
856                                GremlinValue::String(s) => Value::text(s.clone()),
857                                GremlinValue::Integer(i) => Value::Integer(*i),
858                                GremlinValue::Float(f) => Value::Float(*f),
859                                GremlinValue::Boolean(b) => Value::Boolean(*b),
860                                GremlinValue::Predicate(_) => Value::Null, // Predicates handled separately
861                            },
862                        }
863                    } else {
864                        Filter::IsNotNull(field_ref)
865                    };
866                    filters.push(filter);
867                }
868                GremlinStep::Out(label) | GremlinStep::In(label) | GremlinStep::Both(label) => {
869                    alias_counter += 1;
870                    let new_alias = format!("n{}", alias_counter);
871
872                    let direction = match step {
873                        GremlinStep::Out(_) => EdgeDirection::Outgoing,
874                        GremlinStep::In(_) => EdgeDirection::Incoming,
875                        GremlinStep::Both(_) => EdgeDirection::Both,
876                        _ => EdgeDirection::Outgoing,
877                    };
878
879                    // Normalize camelCase / shorthand to snake_case for
880                    // legacy aliases; otherwise pass label through.
881                    let edge_label = label.as_ref().map(|l| {
882                        let lower = l.to_lowercase();
883                        match lower.as_str() {
884                            "hasservice" => "has_service".to_string(),
885                            "hasendpoint" => "has_endpoint".to_string(),
886                            "usestech" => "uses_tech".to_string(),
887                            "authaccess" => "auth_access".to_string(),
888                            "affectedby" => "affected_by".to_string(),
889                            "connectsto" | "connects" => "connects_to".to_string(),
890                            "relatedto" => "related_to".to_string(),
891                            "hasuser" => "has_user".to_string(),
892                            "hascert" => "has_cert".to_string(),
893                            _ => lower,
894                        }
895                    });
896
897                    edges.push(EdgePattern {
898                        alias: None,
899                        from: current_alias.clone(),
900                        to: new_alias.clone(),
901                        edge_label,
902                        direction,
903                        min_hops: 1,
904                        max_hops: 1,
905                    });
906
907                    nodes.push(NodePattern {
908                        alias: new_alias.clone(),
909                        node_label: None,
910                        properties: Vec::new(),
911                    });
912
913                    current_alias = new_alias;
914                }
915                GremlinStep::Limit(_n) => {
916                    // Note: limit is handled at execution time, not in GraphQuery
917                    // Store in execution context if needed
918                }
919                GremlinStep::Values(keys) => {
920                    for key in keys {
921                        projections.push(Projection::from_field(FieldRef::NodeProperty {
922                            alias: current_alias.clone(),
923                            property: key.clone(),
924                        }));
925                    }
926                }
927                GremlinStep::Count => {
928                    // Count is an aggregation, add a marker projection
929                    projections.push(Projection::Field(
930                        FieldRef::NodeId {
931                            alias: current_alias.clone(),
932                        },
933                        Some("count".to_string()),
934                    ));
935                }
936                GremlinStep::As(label) => {
937                    if let Some(last) = nodes.last_mut() {
938                        last.alias = label.clone();
939                        current_alias = label.clone();
940                    }
941                }
942                _ => {}
943            }
944        }
945
946        // If no projections, return all node properties
947        if projections.is_empty() {
948            projections.push(Projection::from_field(FieldRef::NodeId {
949                alias: current_alias.clone(),
950            }));
951        }
952
953        // Fold multiple filters into nested And
954        let combined_filter = if filters.is_empty() {
955            None
956        } else {
957            let mut iter = filters.into_iter();
958            let first = iter.next().unwrap();
959            Some(iter.fold(first, |acc, f| Filter::And(Box::new(acc), Box::new(f))))
960        };
961
962        QueryExpr::Graph(GraphQuery {
963            alias: None,
964            pattern: GraphPattern { nodes, edges },
965            filter: combined_filter,
966            return_: projections,
967        })
968    }
969}
970
971#[cfg(test)]
972mod tests {
973    use super::*;
974
975    #[test]
976    fn test_parse_simple_v() {
977        let t = GremlinParser::parse("g.V()").unwrap();
978        assert_eq!(t.source, TraversalSource::Graph);
979        assert_eq!(t.steps.len(), 1);
980        assert!(matches!(t.steps[0], GremlinStep::V(None)));
981    }
982
983    #[test]
984    fn test_parse_v_with_id() {
985        let t = GremlinParser::parse("g.V('host:10.0.0.1')").unwrap();
986        assert!(matches!(&t.steps[0], GremlinStep::V(Some(id)) if id == "host:10.0.0.1"));
987    }
988
989    #[test]
990    fn test_parse_has_label() {
991        let t = GremlinParser::parse("g.V().hasLabel('host')").unwrap();
992        assert_eq!(t.steps.len(), 2);
993        assert!(matches!(&t.steps[1], GremlinStep::HasLabel(l) if l == "host"));
994    }
995
996    #[test]
997    fn test_parse_has_key_value() {
998        let t = GremlinParser::parse("g.V().has('name', 'alice')").unwrap();
999        assert!(matches!(
1000            &t.steps[1],
1001            GremlinStep::Has(k, Some(GremlinValue::String(v))) if k == "name" && v == "alice"
1002        ));
1003    }
1004
1005    #[test]
1006    fn test_parse_out() {
1007        let t = GremlinParser::parse("g.V().out('knows')").unwrap();
1008        assert!(matches!(&t.steps[1], GremlinStep::Out(Some(l)) if l == "knows"));
1009    }
1010
1011    #[test]
1012    fn test_parse_chain() {
1013        let t =
1014            GremlinParser::parse("g.V().hasLabel('host').out('connects').has('port', 22)").unwrap();
1015        assert_eq!(t.steps.len(), 4);
1016    }
1017
1018    #[test]
1019    fn test_parse_limit() {
1020        let t = GremlinParser::parse("g.V().limit(10)").unwrap();
1021        assert!(matches!(t.steps[1], GremlinStep::Limit(10)));
1022    }
1023
1024    #[test]
1025    fn test_parse_count() {
1026        let t = GremlinParser::parse("g.V().count()").unwrap();
1027        assert!(matches!(t.steps[1], GremlinStep::Count));
1028    }
1029
1030    #[test]
1031    fn test_parse_repeat_times() {
1032        let t = GremlinParser::parse("g.V().repeat(out()).times(3)").unwrap();
1033        assert_eq!(t.steps.len(), 3);
1034        assert!(matches!(&t.steps[1], GremlinStep::Repeat(_)));
1035        assert!(matches!(t.steps[2], GremlinStep::Times(3)));
1036    }
1037
1038    #[test]
1039    fn test_parse_anonymous() {
1040        let t = GremlinParser::parse("__.out('knows')").unwrap();
1041        assert_eq!(t.source, TraversalSource::Anonymous);
1042        assert!(matches!(&t.steps[0], GremlinStep::Out(Some(l)) if l == "knows"));
1043    }
1044
1045    #[test]
1046    fn test_parse_values() {
1047        let t = GremlinParser::parse("g.V().values('name', 'age')").unwrap();
1048        assert!(matches!(&t.steps[1], GremlinStep::Values(keys) if keys.len() == 2));
1049    }
1050
1051    #[test]
1052    fn test_to_query_expr() {
1053        let t = GremlinParser::parse("g.V().hasLabel('host').out('connects').limit(10)").unwrap();
1054        let expr = t.to_query_expr();
1055        assert!(matches!(expr, QueryExpr::Graph(_)));
1056    }
1057}