Skip to main content

helix_ast/
lib.rs

1//! Shared Helix query AST.
2//!
3//! This crate owns the public JSON contract used by SDKs and the planner. The
4//! wire format is an operation tree: each traversal builder call creates one
5//! node, and chaining wraps the previous root as that node's `input`.
6//!
7//! Modules define the stable contract boundaries:
8//!
9//! - [`value`] contains JSON/property values and mutation inputs.
10//! - [`expr`] contains predicates, expressions, and stream bounds.
11//! - [`traversal`] contains the operation-tree AST and traversal builders.
12//! - [`batch`] and [`query`] contain batch and request wire formats.
13//!
14//! ```
15//! use helix_ast::prelude::*;
16//!
17//! let query = read_batch()
18//!     .var_as(
19//!         "users",
20//!         g()
21//!             .n_with_label("User")
22//!             .where_(Predicate::eq("username", "alice"))
23//!             .limit(1),
24//!     )
25//!     .returning(["users"]);
26//! let json = sonic_rs::to_string(&query).unwrap();
27//!
28//! assert!(json.contains(r#""root":{"limit""#));
29//! assert!(json.contains(r#""input":{"where""#));
30//! assert!(json.contains(r#""eq":{"left":{"property":"username"}"#));
31//! ```
32
33#![deny(unsafe_code)]
34
35pub mod batch;
36pub mod expr;
37pub mod graph;
38pub mod index;
39pub mod prelude;
40pub mod projection;
41pub mod query;
42pub mod traversal;
43pub mod value;
44
45#[cfg(test)]
46mod tests {
47    use crate::prelude::*;
48
49    fn query_entry(entry: &BatchEntry) -> &NamedQuery {
50        match entry {
51            BatchEntry::Query(query) => query.as_ref(),
52            BatchEntry::ForEach { .. } => panic!("expected query entry"),
53        }
54    }
55
56    #[test]
57    fn traversal_builds_nested_ast_json() {
58        let batch = read_batch()
59            .var_as(
60                "users",
61                g().n_with_label("User")
62                    .where_(Predicate::eq("username", "alice"))
63                    .limit(1usize)
64                    .value_map(Some(vec!["$id", "username"])),
65            )
66            .returning(["users"]);
67
68        let query = query_entry(&batch.entries()[0]);
69        assert!(matches!(query.root, AstNode::ValueMap { .. }));
70
71        let json = sonic_rs::to_string(&QueryRequest::read(batch)).unwrap();
72        assert!(json.contains(r#""root":{"value_map":{"input":{"limit""#));
73        assert!(json.contains(r#""eq":{"left":{"property":"username"}"#));
74        assert!(!json.contains("steps"));
75    }
76
77    #[test]
78    fn sub_traversal_starts_from_context() {
79        let traversal = g()
80            .n(1u64)
81            .union(vec![sub().out(Some("FOLLOWS")).limit(10usize)]);
82        let AstNode::Union { traversals, .. } = traversal.into_ast() else {
83            panic!("expected union");
84        };
85        let AstNode::Limit { input, .. } = &*traversals[0].root else {
86            panic!("expected limit");
87        };
88        assert!(matches!(input.as_ref(), AstNode::Out { .. }));
89    }
90
91    #[test]
92    fn shortest_path_builds_terminal_ast_json() {
93        let batch = read_batch()
94            .var_as(
95                "path",
96                g().shortest_path_with(
97                    NodeRef::id(1),
98                    NodeRef::param("target"),
99                    Some("FOLLOWS"),
100                    ShortestPathDirection::Both,
101                    5,
102                ),
103            )
104            .returning(["path"]);
105
106        let query = query_entry(&batch.entries()[0]);
107        assert!(matches!(query.root, AstNode::ShortestPath { .. }));
108
109        let json = sonic_rs::to_string(&QueryRequest::read(batch)).unwrap();
110        assert!(json.contains(r#""shortest_path""#));
111        assert!(json.contains(r#""direction":"both""#));
112        assert!(json.contains(r#""max_depth":5"#));
113        assert!(json.contains(r#""target":{"param":"target"}"#));
114    }
115
116    #[test]
117    fn row_binding_builder_invariants() {
118        assert!(std::panic::catch_unwind(|| {
119            let _ = g().n(1u64).bind("");
120        })
121        .is_err());
122
123        assert!(std::panic::catch_unwind(|| {
124            let _ = BindingProjection::binding("service", "$id", "");
125        })
126        .is_err());
127
128        assert!(std::panic::catch_unwind(|| {
129            let _ = BindingProjection::coalesce(Vec::new(), "workload_id");
130        })
131        .is_err());
132
133        assert!(std::panic::catch_unwind(|| {
134            let _ = g().n(1u64).project_bindings(Vec::new());
135        })
136        .is_err());
137    }
138}